From 9cb28b4a31a44c79f5ccc2ecaf3b67a4b5a65a7b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 10 May 2025 00:26:00 +0900 Subject: [PATCH 001/868] MAINTAINERS: add Shubharanshu Mahapatra (Shubhranshu153) as a REVIEWER Signed-off-by: Akihiro Suda --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index 8fbc21ebdf6..d245e39c902 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -22,6 +22,7 @@ # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "manugupt1", "Manu Gupta", "manugupt1@gmail.com","FCA9 504A 4118 EA5C F466 CC30 A5C3 A8F4 E7FE 9E10" +"Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" # EMERITUS # See EMERITUS.md From bf7dac3c63cc441e3c24327dd3f3eee1c4acfe29 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 9 May 2025 09:40:45 -0700 Subject: [PATCH 002/868] CI: pin docker to a specific version Signed-off-by: apostasie --- .github/workflows/job-test-in-host.yml | 26 +++++++++-- .github/workflows/workflow-test.yml | 1 + hack/provisioning/gpg/docker | 62 ++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 hack/provisioning/gpg/docker diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index f3d73cdae91..f760c74780f 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -22,6 +22,9 @@ on: go-version: required: true type: string + docker-version: + required: true + type: string containerd-version: required: true type: string @@ -105,7 +108,18 @@ jobs: echo "::group:: configure cdi for docker" sudo mkdir -p /etc/docker sudo jq '.features.cdi = true' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp && sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json - sudo systemctl restart docker + echo "::endgroup::" + echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" + sudo apt-get update -qq + sudo apt-get install -qq ca-certificates curl + sudo install -m 0755 -d /etc/apt/keyrings + sudo cp ./hack/provisioning/gpg/docker /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt-get update -qq + sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} echo "::endgroup::" else # FIXME: this is missing runc (see top level workflow note about the state of this) @@ -129,12 +143,16 @@ jobs: # Since some arm64 platforms do provide native fallback execution for 32 bits, # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 + echo "::group:: install binfmt" + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/amd64 + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm64 + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm/v7 + echo "::endgroup::" # FIXME: remove expect when we are done removing unbuffer from tests + echo "::group:: installing test dependencies" sudo apt-get install -qq expect + echo "::endgroup::" - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index c54e9deb570..ef6748b3649 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -140,6 +140,7 @@ jobs: canary: ${{ matrix.canary && true || false }} go-version: 1.24 windows-cni-version: v0.3.1 + docker-version: 5:28.0.4-1~ubuntu.24.04~noble containerd-version: 2.1.0 # Note: these as for amd64 containerd-sha: 0e5359e957b66b679be807563a543c7416e305e3aafcf56bad90ef87a917014d diff --git a/hack/provisioning/gpg/docker b/hack/provisioning/gpg/docker new file mode 100644 index 00000000000..ee7872e5d03 --- /dev/null +++ b/hack/provisioning/gpg/docker @@ -0,0 +1,62 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFit2ioBEADhWpZ8/wvZ6hUTiXOwQHXMAlaFHcPH9hAtr4F1y2+OYdbtMuth +lqqwp028AqyY+PRfVMtSYMbjuQuu5byyKR01BbqYhuS3jtqQmljZ/bJvXqnmiVXh +38UuLa+z077PxyxQhu5BbqntTPQMfiyqEiU+BKbq2WmANUKQf+1AmZY/IruOXbnq +L4C1+gJ8vfmXQt99npCaxEjaNRVYfOS8QcixNzHUYnb6emjlANyEVlZzeqo7XKl7 +UrwV5inawTSzWNvtjEjj4nJL8NsLwscpLPQUhTQ+7BbQXAwAmeHCUTQIvvWXqw0N +cmhh4HgeQscQHYgOJjjDVfoY5MucvglbIgCqfzAHW9jxmRL4qbMZj+b1XoePEtht +ku4bIQN1X5P07fNWzlgaRL5Z4POXDDZTlIQ/El58j9kp4bnWRCJW0lya+f8ocodo +vZZ+Doi+fy4D5ZGrL4XEcIQP/Lv5uFyf+kQtl/94VFYVJOleAv8W92KdgDkhTcTD +G7c0tIkVEKNUq48b3aQ64NOZQW7fVjfoKwEZdOqPE72Pa45jrZzvUFxSpdiNk2tZ +XYukHjlxxEgBdC/J3cMMNRE1F4NCA3ApfV1Y7/hTeOnmDuDYwr9/obA8t016Yljj +q5rdkywPf4JF8mXUW5eCN1vAFHxeg9ZWemhBtQmGxXnw9M+z6hWwc6ahmwARAQAB +tCtEb2NrZXIgUmVsZWFzZSAoQ0UgZGViKSA8ZG9ja2VyQGRvY2tlci5jb20+iQI3 +BBMBCgAhBQJYrefAAhsvBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEI2BgDwO +v82IsskP/iQZo68flDQmNvn8X5XTd6RRaUH33kXYXquT6NkHJciS7E2gTJmqvMqd +tI4mNYHCSEYxI5qrcYV5YqX9P6+Ko+vozo4nseUQLPH/ATQ4qL0Zok+1jkag3Lgk +jonyUf9bwtWxFp05HC3GMHPhhcUSexCxQLQvnFWXD2sWLKivHp2fT8QbRGeZ+d3m +6fqcd5Fu7pxsqm0EUDK5NL+nPIgYhN+auTrhgzhK1CShfGccM/wfRlei9Utz6p9P +XRKIlWnXtT4qNGZNTN0tR+NLG/6Bqd8OYBaFAUcue/w1VW6JQ2VGYZHnZu9S8LMc +FYBa5Ig9PxwGQOgq6RDKDbV+PqTQT5EFMeR1mrjckk4DQJjbxeMZbiNMG5kGECA8 +g383P3elhn03WGbEEa4MNc3Z4+7c236QI3xWJfNPdUbXRaAwhy/6rTSFbzwKB0Jm +ebwzQfwjQY6f55MiI/RqDCyuPj3r3jyVRkK86pQKBAJwFHyqj9KaKXMZjfVnowLh +9svIGfNbGHpucATqREvUHuQbNnqkCx8VVhtYkhDb9fEP2xBu5VvHbR+3nfVhMut5 +G34Ct5RS7Jt6LIfFdtcn8CaSas/l1HbiGeRgc70X/9aYx/V/CEJv0lIe8gP6uDoW +FPIZ7d6vH+Vro6xuWEGiuMaiznap2KhZmpkgfupyFmplh0s6knymuQINBFit2ioB +EADneL9S9m4vhU3blaRjVUUyJ7b/qTjcSylvCH5XUE6R2k+ckEZjfAMZPLpO+/tF +M2JIJMD4SifKuS3xck9KtZGCufGmcwiLQRzeHF7vJUKrLD5RTkNi23ydvWZgPjtx +Q+DTT1Zcn7BrQFY6FgnRoUVIxwtdw1bMY/89rsFgS5wwuMESd3Q2RYgb7EOFOpnu +w6da7WakWf4IhnF5nsNYGDVaIHzpiqCl+uTbf1epCjrOlIzkZ3Z3Yk5CM/TiFzPk +z2lLz89cpD8U+NtCsfagWWfjd2U3jDapgH+7nQnCEWpROtzaKHG6lA3pXdix5zG8 +eRc6/0IbUSWvfjKxLLPfNeCS2pCL3IeEI5nothEEYdQH6szpLog79xB9dVnJyKJb +VfxXnseoYqVrRz2VVbUI5Blwm6B40E3eGVfUQWiux54DspyVMMk41Mx7QJ3iynIa +1N4ZAqVMAEruyXTRTxc9XW0tYhDMA/1GYvz0EmFpm8LzTHA6sFVtPm/ZlNCX6P1X +zJwrv7DSQKD6GGlBQUX+OeEJ8tTkkf8QTJSPUdh8P8YxDFS5EOGAvhhpMBYD42kQ +pqXjEC+XcycTvGI7impgv9PDY1RCC1zkBjKPa120rNhv/hkVk/YhuGoajoHyy4h7 +ZQopdcMtpN2dgmhEegny9JCSwxfQmQ0zK0g7m6SHiKMwjwARAQABiQQ+BBgBCAAJ +BQJYrdoqAhsCAikJEI2BgDwOv82IwV0gBBkBCAAGBQJYrdoqAAoJEH6gqcPyc/zY +1WAP/2wJ+R0gE6qsce3rjaIz58PJmc8goKrir5hnElWhPgbq7cYIsW5qiFyLhkdp +YcMmhD9mRiPpQn6Ya2w3e3B8zfIVKipbMBnke/ytZ9M7qHmDCcjoiSmwEXN3wKYI +mD9VHONsl/CG1rU9Isw1jtB5g1YxuBA7M/m36XN6x2u+NtNMDB9P56yc4gfsZVES +KA9v+yY2/l45L8d/WUkUi0YXomn6hyBGI7JrBLq0CX37GEYP6O9rrKipfz73XfO7 +JIGzOKZlljb/D9RX/g7nRbCn+3EtH7xnk+TK/50euEKw8SMUg147sJTcpQmv6UzZ +cM4JgL0HbHVCojV4C/plELwMddALOFeYQzTif6sMRPf+3DSj8frbInjChC3yOLy0 +6br92KFom17EIj2CAcoeq7UPhi2oouYBwPxh5ytdehJkoo+sN7RIWua6P2WSmon5 +U888cSylXC0+ADFdgLX9K2zrDVYUG1vo8CX0vzxFBaHwN6Px26fhIT1/hYUHQR1z +VfNDcyQmXqkOnZvvoMfz/Q0s9BhFJ/zU6AgQbIZE/hm1spsfgvtsD1frZfygXJ9f +irP+MSAI80xHSf91qSRZOj4Pl3ZJNbq4yYxv0b1pkMqeGdjdCYhLU+LZ4wbQmpCk +SVe2prlLureigXtmZfkqevRz7FrIZiu9ky8wnCAPwC7/zmS18rgP/17bOtL4/iIz +QhxAAoAMWVrGyJivSkjhSGx1uCojsWfsTAm11P7jsruIL61ZzMUVE2aM3Pmj5G+W +9AcZ58Em+1WsVnAXdUR//bMmhyr8wL/G1YO1V3JEJTRdxsSxdYa4deGBBY/Adpsw +24jxhOJR+lsJpqIUeb999+R8euDhRHG9eFO7DRu6weatUJ6suupoDTRWtr/4yGqe +dKxV3qQhNLSnaAzqW/1nA3iUB4k7kCaKZxhdhDbClf9P37qaRW467BLCVO/coL3y +Vm50dwdrNtKpMBh3ZpbB1uJvgi9mXtyBOMJ3v8RZeDzFiG8HdCtg9RvIt/AIFoHR +H3S+U79NT6i0KPzLImDfs8T7RlpyuMc4Ufs8ggyg9v3Ae6cN3eQyxcK3w0cbBwsh +/nQNfsA6uu+9H7NhbehBMhYnpNZyrHzCmzyXkauwRAqoCbGCNykTRwsur9gS41TQ +M8ssD1jFheOJf3hODnkKU+HKjvMROl1DK7zdmLdNzA1cvtZH/nCC9KPj1z8QC47S +xx+dTZSx4ONAhwbS/LN3PoKtn8LPjY9NP9uDWI+TWYquS2U+KHDrBDlsgozDbs/O +jCxcpDzNmXpWQHEtHU7649OXHP7UeNST1mCUCH5qdank0V1iejF6/CfTFU4MfcrG +YT90qFF93M3v01BbxP+EIY2/9tiIPbrd +=0YYh +-----END PGP PUBLIC KEY BLOCK----- From a6585dd1227d51ea640e130ad83662339f695413 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 9 May 2025 10:33:06 -0700 Subject: [PATCH 003/868] Carry a copy of vagrant gpg key Signed-off-by: apostasie --- .github/workflows/job-test-in-vagrant.yml | 2 +- hack/provisioning/gpg/hashicorp | 64 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 hack/provisioning/gpg/hashicorp diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 843606c0987..2c12637326e 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -35,7 +35,7 @@ jobs: # 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 - curl -fsSL --proto '=https' --tlsv1.2 https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + 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 diff --git a/hack/provisioning/gpg/hashicorp b/hack/provisioning/gpg/hashicorp new file mode 100644 index 00000000000..495865561d5 --- /dev/null +++ b/hack/provisioning/gpg/hashicorp @@ -0,0 +1,64 @@ +-----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 From 9b1cd0822be81e094043291f048e2b0a0ae0203c Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 9 May 2025 12:05:07 -0700 Subject: [PATCH 004/868] Fix flaky diff test Signed-off-by: apostasie --- cmd/nerdctl/container/container_diff_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_diff_test.go b/cmd/nerdctl/container/container_diff_test.go index dc09244a3a0..b2ab02191ab 100644 --- a/cmd/nerdctl/container/container_diff_test.go +++ b/cmd/nerdctl/container/container_diff_test.go @@ -39,7 +39,7 @@ func TestDiff(t *testing.T) { testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "touch /a; touch /bin/b; rm /bin/base64") } From 7680b12378362c6c4bcae77fff7dce19042d1719 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 9 May 2025 13:09:23 -0700 Subject: [PATCH 005/868] Clarify corner case on Tigron WithFeeder Signed-off-by: apostasie --- mod/tigron/internal/com/command.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mod/tigron/internal/com/command.go b/mod/tigron/internal/com/command.go index c8cd6c3d21b..869a2589de2 100644 --- a/mod/tigron/internal/com/command.go +++ b/mod/tigron/internal/com/command.go @@ -150,6 +150,7 @@ func (gc *Command) WithPTY(stdin, stdout, stderr bool) { // WithFeeder ensures that the provider function will be executed and its output fed to the command stdin. // WithFeeder, like Feed, can be used multiple times, and writes will be performed sequentially, in order. // This command has no effect if Run has already been called. +// Note that if the `writer` function runs a forever loop, we will deadlock and just Wait() forever on the errgroup. func (gc *Command) WithFeeder(writers ...func() io.Reader) { gc.writers = append(gc.writers, writers...) } From 38f07ca97f01547eaec6bcdb8cf880f3b6cf6ebb Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 9 May 2025 14:34:28 -0700 Subject: [PATCH 006/868] (re)-tackle CNI concurrency issues Signed-off-by: apostasie --- pkg/netutil/netutil.go | 146 ++++++++++++++--------------------------- pkg/netutil/store.go | 100 ++++++++++++++++++++++++++++ pkg/ocihook/ocihook.go | 4 +- 3 files changed, 153 insertions(+), 97 deletions(-) create mode 100644 pkg/netutil/store.go diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index e97a9125c58..cbcc27bfde9 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -38,7 +38,6 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/labels" - "github.com/containerd/nerdctl/v2/pkg/lockutil" "github.com/containerd/nerdctl/v2/pkg/netutil/nettype" subnetutil "github.com/containerd/nerdctl/v2/pkg/netutil/subnet" "github.com/containerd/nerdctl/v2/pkg/strutil" @@ -53,14 +52,7 @@ type CNIEnv struct { type CNIEnvOpt func(e *CNIEnv) error func (e *CNIEnv) ListNetworksMatch(reqs []string, allowPseudoNetwork bool) (list map[string][]*NetworkConfig, errs []error) { - var err error - - var networkConfigs []*NetworkConfig - // NOTE: we cannot lock NetconfPath directly, as Cilium (maybe others) are also locking it. - err = lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { - networkConfigs, err = e.networkConfigList() - return err - }) + networkConfigs, err := fsRead(e) if err != nil { return nil, []error{err} } @@ -188,7 +180,8 @@ func WithDefaultNetwork(bridgeIP string) CNIEnvOpt { func WithNamespace(namespace string) CNIEnvOpt { return func(e *CNIEnv) error { - if err := os.MkdirAll(filepath.Join(e.NetconfPath, namespace), 0755); err != nil { + err := fsEnsureRoot(e, namespace) + if err != nil { return err } e.Namespace = namespace @@ -201,7 +194,8 @@ func NewCNIEnv(cniPath, cniConfPath string, opts ...CNIEnvOpt) (*CNIEnv, error) Path: cniPath, NetconfPath: cniConfPath, } - if err := os.MkdirAll(e.NetconfPath, 0755); err != nil { + + if err := fsEnsureRoot(&e, ""); err != nil { return nil, err } @@ -215,25 +209,17 @@ func NewCNIEnv(cniPath, cniConfPath string, opts ...CNIEnvOpt) (*CNIEnv, error) } func (e *CNIEnv) NetworkList() ([]*NetworkConfig, error) { - var netConfigList []*NetworkConfig - var err error - fn := func() error { - netConfigList, err = e.networkConfigList() - return err - } - err = lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) - - return netConfigList, err + return fsRead(e) } func (e *CNIEnv) NetworkMap() (map[string]*NetworkConfig, error) { //nolint:revive - networks, err := e.networkConfigList() + netConfigList, err := fsRead(e) if err != nil { return nil, err } - m := make(map[string]*NetworkConfig, len(networks)) - for _, n := range networks { + m := make(map[string]*NetworkConfig, len(netConfigList)) + for _, n := range netConfigList { if original, exists := m[n.Name]; exists { log.L.Warnf("duplicate network name %q, %#v will get superseded by %#v", n.Name, original, n) } @@ -243,12 +229,12 @@ func (e *CNIEnv) NetworkMap() (map[string]*NetworkConfig, error) { //nolint:revi } func (e *CNIEnv) NetworkByNameOrID(key string) (*NetworkConfig, error) { - networks, err := e.networkConfigList() + netConfigList, err := fsRead(e) if err != nil { return nil, err } - for _, n := range networks { + for _, n := range netConfigList { if n.Name == key { return n, nil } @@ -261,12 +247,12 @@ func (e *CNIEnv) NetworkByNameOrID(key string) (*NetworkConfig, error) { } func (e *CNIEnv) filterNetworks(filterf func(*NetworkConfig) bool) ([]*NetworkConfig, error) { - networkConfigs, err := e.networkConfigList() + netConfigList, err := fsRead(e) if err != nil { return nil, err } result := []*NetworkConfig{} - for _, networkConfig := range networkConfigs { + for _, networkConfig := range netConfigList { if filterf(networkConfig) { result = append(result, networkConfig) } @@ -274,23 +260,18 @@ func (e *CNIEnv) filterNetworks(filterf func(*NetworkConfig) bool) ([]*NetworkCo return result, nil } -func (e *CNIEnv) getConfigPathForNetworkName(netName string) string { - if netName == DefaultNetworkName || e.Namespace == "" { - return filepath.Join(e.NetconfPath, "nerdctl-"+netName+".conflist") - } - return filepath.Join(e.NetconfPath, e.Namespace, "nerdctl-"+netName+".conflist") -} - func (e *CNIEnv) usedSubnets() ([]*net.IPNet, error) { usedSubnets, err := subnetutil.GetLiveNetworkSubnets() if err != nil { return nil, err } - networkConfigs, err := e.networkConfigList() + + netConfigList, err := fsRead(e) if err != nil { return nil, err } - for _, netConf := range networkConfigs { + + for _, netConf := range netConfigList { usedSubnets = append(usedSubnets, netConf.subnets()...) } return usedSubnets, nil @@ -314,44 +295,39 @@ type cniNetworkConfig struct { func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, error) { //nolint:revive var netConf *NetworkConfig - fn := func() error { - netMap, err := e.NetworkMap() - if err != nil { - return err - } + netMap, err := e.NetworkMap() + if err != nil { + return nil, err + } - if _, ok := netMap[opts.Name]; ok { - return errdefs.ErrAlreadyExists - } - ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6) - if err != nil { - return err - } - plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6) - if err != nil { - return err - } - netConf, err = e.generateNetworkConfig(opts.Name, opts.Labels, plugins) - if err != nil { - return err - } - return e.writeNetworkConfig(netConf) + // See note in fsWrite. Just because it does not exist now does not guarantee it will still not exist later. + // This is more a perf optimization at this point than a true check. + if _, ok := netMap[opts.Name]; ok { + return nil, errdefs.ErrAlreadyExists } - err := lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) + ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6) if err != nil { return nil, err } + plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6) + if err != nil { + return nil, err + } + netConf, err = e.generateNetworkConfig(opts.Name, opts.Labels, plugins) + if err != nil { + return nil, err + } + err = fsWrite(e, netConf) + + // See note above. If it exists, we got raced out by another process. Consider this to NOT be a hard error. + if err != nil && !errdefs.IsAlreadyExists(err) { + return nil, err + } return netConf, nil } func (e *CNIEnv) RemoveNetwork(net *NetworkConfig) error { - fn := func() error { - if err := os.RemoveAll(net.File); err != nil { - return err - } - return net.clean() - } - return lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) + return fsRemove(e, net) } // GetDefaultNetworkConfig checks whether the default network exists @@ -394,8 +370,8 @@ func (e *CNIEnv) GetDefaultNetworkConfig() (*NetworkConfig, error) { // Warn the user if the default network was not created by nerdctl. match := nameMatches[0] - _, statErr := os.Stat(e.getConfigPathForNetworkName(DefaultNetworkName)) - if match.NerdctlID == nil || statErr != nil { + exists, statErr := fsExists(e, DefaultNetworkName) + if match.NerdctlID == nil || statErr != nil || !exists { log.L.Warnf("default network named %q does not have an internal nerdctl ID or nerdctl-managed config file, it was most likely NOT created by nerdctl", DefaultNetworkName) } @@ -419,9 +395,12 @@ func (e *CNIEnv) ensureDefaultNetworkConfig(bridgeIP string) error { } func (e *CNIEnv) createDefaultNetworkConfig(bridgeIP string) error { - filename := e.getConfigPathForNetworkName(DefaultNetworkName) - if _, err := os.Stat(filename); err == nil { - return fmt.Errorf("already found existing network config at %q, cannot create new network named %q", filename, DefaultNetworkName) + exist, err := fsExists(e, DefaultNetworkName) + if err != nil && !os.IsNotExist(err) { + return err + } + if exist { + return fmt.Errorf("already found existing network config, cannot create new network named %q", DefaultNetworkName) } bridgeCIDR := DefaultCIDR @@ -443,7 +422,7 @@ func (e *CNIEnv) createDefaultNetworkConfig(bridgeIP string) error { Labels: []string{fmt.Sprintf("%s=true", labels.NerdctlDefaultNetwork)}, } - _, err := e.CreateNetwork(opts) + _, err = e.CreateNetwork(opts) if err != nil && !errdefs.IsAlreadyExists(err) { return err } @@ -490,31 +469,6 @@ func (e *CNIEnv) generateNetworkConfig(name string, labels []string, plugins []C }, nil } -// writeNetworkConfig writes NetworkConfig file to cni config path. -func (e *CNIEnv) writeNetworkConfig(net *NetworkConfig) error { - filename := e.getConfigPathForNetworkName(net.Name) - if _, err := os.Stat(filename); err == nil { - return errdefs.ErrAlreadyExists - } - return os.WriteFile(filename, net.Bytes, 0644) -} - -// networkConfigList loads config from dir if dir exists. -func (e *CNIEnv) networkConfigList() ([]*NetworkConfig, error) { - common, err := libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) - if err != nil { - return nil, err - } - namespaced := []string{} - if e.Namespace != "" { - namespaced, err = libcni.ConfFiles(filepath.Join(e.NetconfPath, e.Namespace), []string{".conf", ".conflist", ".json"}) - if err != nil { - return nil, err - } - } - return cniLoad(append(common, namespaced...)) -} - func wrapCNIError(fileName string, err error) error { return fmt.Errorf("failed marshalling json out of network configuration file %q: %w\n"+ "For details on the schema, see https://pkg.go.dev/github.com/containernetworking/cni/libcni#NetworkConfigList", fileName, err) diff --git a/pkg/netutil/store.go b/pkg/netutil/store.go new file mode 100644 index 00000000000..4d07db28fa9 --- /dev/null +++ b/pkg/netutil/store.go @@ -0,0 +1,100 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package netutil + +import ( + "os" + "path/filepath" + + "github.com/containernetworking/cni/libcni" + + "github.com/containerd/errdefs" + + "github.com/containerd/nerdctl/v2/pkg/lockutil" +) + +// NOTE: libcni is not safe to use concurrently - or at least delegates concurrency management to the consumer. +// Furthermore, CNIEnv (prior to this) is assuming the filesystem is ACID and other TOCTOU faults. +// This small set of methods here are meant to isolate CNIEnv entirely from the filesystem. +// This is NOT proper - we should instead use the Store implementation, which is the generic abstraction for ACID +// operations - but for now that will do, waiting for a full rewrite of CNIEnv. + +func fsEnsureRoot(e *CNIEnv, namespace string) error { + path := e.NetconfPath + if namespace != "" { + path = filepath.Join(e.NetconfPath, namespace) + } + return os.MkdirAll(path, 0755) +} + +func fsRemove(e *CNIEnv, net *NetworkConfig) error { + fn := func() error { + if err := os.RemoveAll(net.File); err != nil { + return err + } + return net.clean() + } + return lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) +} + +func fsExists(e *CNIEnv, name string) (bool, error) { + fi, err := os.Stat(getConfigPathForNetworkName(e, name)) + return !os.IsNotExist(err) && !fi.IsDir(), err +} + +func fsWrite(e *CNIEnv, net *NetworkConfig) error { + filename := getConfigPathForNetworkName(e, net.Name) + // FIXME: note that this is still problematic. + // Concurrent access may independently first figure out that a given network is missing, and while the lock + // here will prevent concurrent writes, one of the routines will fail. + // Consuming code MUST account for that scenario. + return lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + if _, err := os.Stat(filename); err == nil { + return errdefs.ErrAlreadyExists + } + return os.WriteFile(filename, net.Bytes, 0644) + }) +} + +func fsRead(e *CNIEnv) ([]*NetworkConfig, error) { + var nc []*NetworkConfig + var err error + err = lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + namespaced := []string{} + var common []string + common, err = libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) + if err != nil { + return err + } + if e.Namespace != "" { + namespaced, err = libcni.ConfFiles(filepath.Join(e.NetconfPath, e.Namespace), []string{".conf", ".conflist", ".json"}) + if err != nil { + return err + } + } + nc, err = cniLoad(append(common, namespaced...)) + return err + }) + return nc, err +} + +func getConfigPathForNetworkName(e *CNIEnv, netName string) string { + if netName == DefaultNetworkName || e.Namespace == "" { + return filepath.Join(e.NetconfPath, "nerdctl-"+netName+".conflist") + } + return filepath.Join(e.NetconfPath, e.Namespace, "nerdctl-"+netName+".conflist") +} diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 2807b23e9d8..d035a4ad968 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -103,11 +103,13 @@ func Run(stdin io.Reader, stderr io.Writer, event, dataStore, cniPath, cniNetcon // This below is a stopgap solution that just enforces a global lock // Note this here is probably not enough, as concurrent CNI operations may happen outside of the scope of ocihooks // through explicit calls to Remove, etc. + // Finally note that this is not the same (albeit similar) as libcni filesystem manipulation locking, + // hence the independent lock err = os.MkdirAll(cniNetconfPath, 0o700) if err != nil { return err } - lock, err := lockutil.Lock(filepath.Join(cniNetconfPath, ".nerdctl.lock")) + lock, err := lockutil.Lock(filepath.Join(cniNetconfPath, ".cni-concurrency.lock")) if err != nil { return err } From ad8be25f8d6ecb182bba2ea6b84299f6c4bf349f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 22:37:33 +0000 Subject: [PATCH 007/868] build(deps): bump github.com/vishvananda/netlink Bumps [github.com/vishvananda/netlink](https://github.com/vishvananda/netlink) from 1.3.1-0.20250303224720-0e7078ed04c8 to 1.3.1. - [Release notes](https://github.com/vishvananda/netlink/releases) - [Commits](https://github.com/vishvananda/netlink/commits/v1.3.1) --- updated-dependencies: - dependency-name: github.com/vishvananda/netlink dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 40c88753935..5a44ed35b05 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined github.com/spf13/cobra v1.9.1 //gomodjail:unconfined github.com/spf13/pflag v1.0.6 //gomodjail:unconfined - github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8 //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.uber.org/mock v0.5.2 diff --git a/go.sum b/go.sum index 5455c1222c1..ac3bb575873 100644 --- a/go.sum +++ b/go.sum @@ -311,9 +311,8 @@ github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiY github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= -github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8 h1:Y4egeTrP7sccowz2GWTJVtHlwkZippgBTpUmMteFUWQ= -github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= From 3c376a817e6b825df192a4d3fdb65404e1276f81 Mon Sep 17 00:00:00 2001 From: fahed dorgaa Date: Fri, 9 May 2025 11:15:01 +0200 Subject: [PATCH 008/868] fix: avoid adding extraneous line feed when tail logs Signed-off-by: fahed dorgaa --- cmd/nerdctl/container/container_logs_test.go | 28 ++++++++++++++++++++ pkg/logging/json_logger_test.go | 3 ++- pkg/logging/jsonfile/jsonfile.go | 2 +- pkg/logging/logging.go | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 0ea37dab378..12bf2568a5b 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -394,6 +394,34 @@ func TestLogsWithDetails(t *testing.T) { testCase.Run(t) } +func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { + testCase := nerdtest.Setup() + // This test verifies that `nerdctl logs -f` does not add extraneous line feeds + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Create a container that outputs a message without a trailing newline + // and then sleeps to keep the container running for the logs -f command + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-c", "printf 'Hello without newline'; sleep 5") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Use logs -f to follow the logs + // The container will exit after 5 seconds, so we don't need an explicit timeout + return helpers.Command("logs", "-f", data.Identifier()) + } + + // Verify that the output is exactly "Hello without newline" without any additional line feeds + testCase.Expected = test.Expects(0, nil, expect.Equals("Hello without newline")) + + testCase.Run(t) +} + func TestLogsWithStartContainer(t *testing.T) { testCase := nerdtest.Setup() diff --git a/pkg/logging/json_logger_test.go b/pkg/logging/json_logger_test.go index 7d0be36285d..7b41c10a68c 100644 --- a/pkg/logging/json_logger_test.go +++ b/pkg/logging/json_logger_test.go @@ -73,6 +73,7 @@ func TestReadRotatedJSONLog(t *testing.T) { time.Sleep(1 * time.Millisecond) logData, _ := json.Marshal(log) file.Write(logData) + file.Write([]byte("\n")) if line == 5 { file.Close() @@ -104,7 +105,7 @@ func TestReadRotatedJSONLog(t *testing.T) { close(containerStopped) if expectedStdout != stdoutBuf.String() { - t.Errorf("expected: %s, acoutal: %s", expectedStdout, stdoutBuf.String()) + t.Errorf("expected: %s, actual: %s", expectedStdout, stdoutBuf.String()) } } diff --git a/pkg/logging/jsonfile/jsonfile.go b/pkg/logging/jsonfile/jsonfile.go index a1693cda0d7..6e6f7984eda 100644 --- a/pkg/logging/jsonfile/jsonfile.go +++ b/pkg/logging/jsonfile/jsonfile.go @@ -54,7 +54,7 @@ func Encode(stdout <-chan string, stderr <-chan string, writer io.Writer) error Stream: name, } for logEntry := range dataChan { - e.Log = logEntry + "\n" + e.Log = logEntry e.Time = time.Now().UTC() encMu.Lock() encErr := enc.Encode(e) diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index eab10cbd726..3030660cf0e 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -255,7 +255,7 @@ func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore, addres var s string s, err = r.ReadString('\n') if len(s) > 0 { - dataChan <- strings.TrimSuffix(s, "\n") + dataChan <- s } if err != nil && err != io.EOF { From 87a976dc6354b73c0fa7bb400885b14dbd4741af Mon Sep 17 00:00:00 2001 From: fahed dorgaa Date: Sat, 10 May 2025 01:57:17 +0200 Subject: [PATCH 009/868] test: add delay to ensure logs are available before following Signed-off-by: fahed dorgaa --- cmd/nerdctl/container/container_logs_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 12bf2568a5b..deac334f6e2 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -413,6 +413,8 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use logs -f to follow the logs // The container will exit after 5 seconds, so we don't need an explicit timeout + // Arbitrary, but we need to wait until the logs show up + time.Sleep(3 * time.Second) return helpers.Command("logs", "-f", data.Identifier()) } From 1ec8f840e53a06920a07f69e2c46a21b4e6a1362 Mon Sep 17 00:00:00 2001 From: zhaixiaojuan Date: Mon, 25 Sep 2023 23:36:35 -0400 Subject: [PATCH 010/868] Add loong64 artifact Signed-off-by: zhaixiaojuan --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 3544c484612..65ec10c1c9a 100644 --- a/Makefile +++ b/Makefile @@ -268,6 +268,9 @@ artifacts: clean GOOS=linux GOARCH=arm GOARM=7 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm-v7.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* + GOOS=linux GOARCH=loong64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries + tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-loong64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* + GOOS=linux GOARCH=ppc64le make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-ppc64le.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/* From a5d170183ce47d49f97213505aca1ce36d87d082 Mon Sep 17 00:00:00 2001 From: fahed dorgaa Date: Sat, 10 May 2025 14:44:27 +0200 Subject: [PATCH 011/868] test: remove unnecessary sleep in container setup for log tests Signed-off-by: fahed dorgaa --- cmd/nerdctl/container/container_logs_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index deac334f6e2..d6011260e4e 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -401,9 +401,8 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { // Create a container that outputs a message without a trailing newline - // and then sleeps to keep the container running for the logs -f command helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, - "sh", "-c", "printf 'Hello without newline'; sleep 5") + "sh", "-c", "printf 'Hello without newline'") } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -412,7 +411,6 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use logs -f to follow the logs - // The container will exit after 5 seconds, so we don't need an explicit timeout // Arbitrary, but we need to wait until the logs show up time.Sleep(3 * time.Second) return helpers.Command("logs", "-f", data.Identifier()) From dcd19765b4ec99b3d78afaee80ffd0014bae98e9 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 10 May 2025 10:32:42 -0700 Subject: [PATCH 012/868] Add loong64 as a build target + completion Signed-off-by: apostasie --- .github/workflows/job-build.yml | 3 ++- cmd/nerdctl/completion/completion.go | 1 + docs/multi-platform.md | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 63c3c3e309d..2bf843e2ab6 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -88,9 +88,10 @@ jobs: build linux arm64 build windows build freebsd + # These architectures are not released, but we still verify that we can at least compile build darwin build linux arm 6 - # These architectures are not released, but we still verify that we can at least compile + build linux loong64 build linux ppc64le build linux riscv64 build linux s390x diff --git a/cmd/nerdctl/completion/completion.go b/cmd/nerdctl/completion/completion.go index 7718c1bb063..e12e2375a40 100644 --- a/cmd/nerdctl/completion/completion.go +++ b/cmd/nerdctl/completion/completion.go @@ -164,6 +164,7 @@ func Platforms(cmd *cobra.Command, args []string, toComplete string) ([]string, "riscv64", "ppc64le", "s390x", + "loong64", "386", "arm", // alias of "linux/arm/v7" "linux/arm/v6", // "arm/v6" is invalid (interpreted as OS="arm", Arch="v7") diff --git a/docs/multi-platform.md b/docs/multi-platform.md index 5c2e05b9239..e70b5f18c7f 100644 --- a/docs/multi-platform.md +++ b/docs/multi-platform.md @@ -16,6 +16,7 @@ $ sudo nerdctl run --privileged --rm tonistiigi/binfmt:master --install all $ ls -1 /proc/sys/fs/binfmt_misc/qemu* /proc/sys/fs/binfmt_misc/qemu-aarch64 /proc/sys/fs/binfmt_misc/qemu-arm +/proc/sys/fs/binfmt_misc/qemu-loongarch64 /proc/sys/fs/binfmt_misc/qemu-mips64 /proc/sys/fs/binfmt_misc/qemu-mips64el /proc/sys/fs/binfmt_misc/qemu-ppc64le From 8cc057fb077a3e200340fa7cb58d131e420fe5f4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 12 May 2025 18:31:18 +0900 Subject: [PATCH 013/868] CI: test `make artifacts` on every PR A preparation toward fixing issue 4241 Signed-off-by: Akihiro Suda --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a920a20a52..bf1ec924b48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' - 'test-action-release-*' + pull_request: + paths-ignore: + - '**.md' env: GOTOOLCHAIN: local @@ -53,6 +56,7 @@ jobs: with: subject-path: _output/* - name: "Create release" + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | From 58a5fe058879ba7f791ffaea78da17634ee7106e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 12 May 2025 18:40:17 +0900 Subject: [PATCH 014/868] Dockerfile: fix cross-compiling gomodjail Fix issue 4241 Signed-off-by: Akihiro Suda --- .../ghcr-image-build-and-publish.yml | 1 + .github/workflows/release.yml | 3 +++ Dockerfile | 20 +++++++++++++------ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 47084653b93..969eb67229f 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -33,6 +33,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf1ec924b48..b68c6395047 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,9 @@ jobs: attestations: write # for provenances steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # FIXME: setup-qemu-action is depended by `gomodjail pack` + - name: "Set up QEMU" + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: "Install go" uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: diff --git a/Dockerfile b/Dockerfile index 533f16eba18..d0c218b8f34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,6 +107,16 @@ ENV CGO_ENABLED=1 RUN GO=xx-go make static && \ xx-verify --static bypass4netns && cp -a bypass4netns bypass4netnsd /out/${TARGETARCH} +FROM build-base-debian AS build-gomodjail +ARG GOMODJAIL_VERSION +ARG TARGETARCH +RUN git clone --quiet --depth 1 --branch "${GOMODJAIL_VERSION%@*}" https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail +WORKDIR /go/src/github.com/AkihiroSuda/gomodjail +RUN git-checkout-tag-with-hash.sh ${GOMODJAIL_VERSION} && \ + mkdir -p /out/${TARGETARCH} +RUN GO=xx-go make STATIC=1 && \ + xx-verify --static _output/bin/gomodjail && cp -a _output/bin/gomodjail /out/${TARGETARCH} + FROM build-base-debian AS build-kubo ARG KUBO_VERSION ARG TARGETARCH @@ -234,12 +244,8 @@ RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION/@BINARY}; \ rm -f "${fname}" /out/bin/rootlesskit-docker-proxy && \ echo "- RootlessKit: ${ROOTLESSKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG GOMODJAIL_VERSION -RUN git clone https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail && \ - cd /go/src/github.com/AkihiroSuda/gomodjail && \ - git-checkout-tag-with-hash.sh "${GOMODJAIL_VERSION}" && \ - make STATIC=1 && \ - cp -a _output/bin/gomodjail /out/bin/ && \ - echo "- gomodjail: ${GOMODJAIL_VERSION}" >> /out/share/doc/nerdctl-full/README.md +COPY --from=build-gomodjail /out/${TARGETARCH:-amd64}/* /out/bin/ +RUN echo "- gomodjail: ${GOMODJAIL_VERSION}" >> /out/share/doc/nerdctl-full/README.md RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \ echo "## License" >> /out/share/doc/nerdctl-full/README.md && \ @@ -254,6 +260,8 @@ COPY . /go/src/github.com/containerd/nerdctl RUN { echo "# nerdctl (full distribution)"; echo "- nerdctl: $(cd /go/src/github.com/containerd/nerdctl && git describe --tags)"; cat /out/share/doc/nerdctl-full/README.md; } > /out/share/doc/nerdctl-full/README.md.new; mv /out/share/doc/nerdctl-full/README.md.new /out/share/doc/nerdctl-full/README.md WORKDIR /go/src/github.com/containerd/nerdctl RUN BINDIR=/out/bin make binaries install +# FIXME: `gomodjail pack` depends on QEMU for non-native architecture +# TODO: gomodjail should provide a plain shell script that utilizes `zip(1)` for packing the self-extract archive, without running `gomodjail pack`.. RUN /out/bin/gomodjail pack --go-mod=/go/src/github.com/containerd/nerdctl/go.mod /out/bin/nerdctl && \ cp -a nerdctl.gomodjail /out/bin/ COPY README.md /out/share/doc/nerdctl/ From 8586da2ae3e2cd377a79b4bbc1ea49ca96732e98 Mon Sep 17 00:00:00 2001 From: Subash Kotha Date: Fri, 2 May 2025 11:56:16 -0700 Subject: [PATCH 015/868] feat: add --no-stdin flag to container attach Signed-off-by: Subash Kotha --- cmd/nerdctl/container/container_attach.go | 14 ++++++- .../container/container_attach_linux_test.go | 41 +++++++++++++++++++ docs/command-reference.md | 3 +- pkg/cmd/container/attach.go | 10 +++-- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/container/container_attach.go b/cmd/nerdctl/container/container_attach.go index 958c7c4b7ec..5fd004ae36e 100644 --- a/cmd/nerdctl/container/container_attach.go +++ b/cmd/nerdctl/container/container_attach.go @@ -17,6 +17,8 @@ package container import ( + "io" + "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" @@ -56,6 +58,7 @@ Caveats: SilenceErrors: true, } cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") + cmd.Flags().Bool("no-stdin", false, "Do not attach STDIN") return cmd } @@ -68,9 +71,18 @@ func attachOptions(cmd *cobra.Command) (types.ContainerAttachOptions, error) { if err != nil { return types.ContainerAttachOptions{}, err } + noStdin, err := cmd.Flags().GetBool("no-stdin") + if err != nil { + return types.ContainerAttachOptions{}, err + } + + var stdin io.Reader + if !noStdin { + stdin = cmd.InOrStdin() + } return types.ContainerAttachOptions{ GOptions: globalOptions, - Stdin: cmd.InOrStdin(), + Stdin: stdin, Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), DetachKeys: detachKeys, diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index 083fd4a194e..88ab8bb5430 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -211,3 +211,44 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { testCase.Run(t) } + +func TestAttachNoStdin(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("run", "-it", "--detach-keys=ctrl-p,ctrl-q", "--name", data.Identifier(), + testutil.CommonImage, "sleep", "5") + cmd.WithPseudoTTY() + cmd.Feed(bytes.NewReader([]byte{16, 17})) // Ctrl-p, Ctrl-q to detach (https://en.wikipedia.org/wiki/C0_and_C1_control_codes) + cmd.Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) + }, + }) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("attach", "--no-stdin", data.Identifier()) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("should-not-appear\n")) + cmd.Feed(bytes.NewReader([]byte{16, 17})) + return cmd + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, // Since it's a normal exit and not detach. + Output: func(stdout string, info string, t *testing.T) { + logs := helpers.Capture("logs", data.Identifier()) + assert.Assert(t, !strings.Contains(logs, "should-not-appear")) + }, + } + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index f95d5db1b4a..60db48e2b43 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -686,8 +686,9 @@ Usage: `nerdctl attach CONTAINER` Flags: - :whale: `--detach-keys`: Override the default detach keys +- :whale: `--no-stdin`: Do not attach STDIN -Unimplemented `docker attach` flags: `--no-stdin`, `--sig-proxy` +Unimplemented `docker attach` flags: `--sig-proxy` ### :whale: nerdctl container prune diff --git a/pkg/cmd/container/attach.go b/pkg/cmd/container/attach.go index 31a1523e9e9..a6295c76cb2 100644 --- a/pkg/cmd/container/attach.go +++ b/pkg/cmd/container/attach.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "io" "golang.org/x/term" @@ -114,9 +115,12 @@ func Attach(ctx context.Context, client *containerd.Client, req string, options } io.Cancel() } - in, err := consoleutil.NewDetachableStdin(con, options.DetachKeys, closer) - if err != nil { - return err + var in io.Reader + if options.Stdin != nil { + in, err = consoleutil.NewDetachableStdin(con, options.DetachKeys, closer) + if err != nil { + return err + } } opt = cio.WithStreams(in, con, nil) } else { From 368d2e27c23e752f78a1df07ad4d455e24bb3ac4 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 14 May 2025 08:59:44 -0700 Subject: [PATCH 016/868] Consolidate filesystem ops into pkg Signed-off-by: apostasie --- cmd/nerdctl/main.go | 4 +- pkg/composer/lock.go | 6 +- pkg/internal/filesystem/atomic.go | 39 +++++++++ pkg/internal/filesystem/consts.go | 21 +++++ pkg/internal/filesystem/errors.go | 23 +++++ .../filesystem}/lockutil_unix.go | 2 +- .../filesystem}/lockutil_windows.go | 2 +- pkg/internal/filesystem/path.go | 47 ++++++++++ pkg/internal/filesystem/path_test.go | 85 +++++++++++++++++++ .../filesystem/path_unix.go} | 8 +- .../filesystem/path_windows.go} | 11 ++- pkg/logging/logging.go | 6 +- pkg/netutil/store.go | 8 +- pkg/ocihook/ocihook.go | 6 +- pkg/store/filestore.go | 54 ++++-------- pkg/store/filestore_test.go | 59 ------------- pkg/testutil/testutil.go | 6 +- 17 files changed, 261 insertions(+), 126 deletions(-) create mode 100644 pkg/internal/filesystem/atomic.go create mode 100644 pkg/internal/filesystem/consts.go create mode 100644 pkg/internal/filesystem/errors.go rename pkg/{lockutil => internal/filesystem}/lockutil_unix.go (98%) rename pkg/{lockutil => internal/filesystem}/lockutil_windows.go (99%) create mode 100644 pkg/internal/filesystem/path.go create mode 100644 pkg/internal/filesystem/path_test.go rename pkg/{store/filestore_unix.go => internal/filesystem/path_unix.go} (75%) rename pkg/{store/filestore_windows.go => internal/filesystem/path_windows.go} (78%) diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 5223a68a959..88d753a170c 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -248,12 +248,12 @@ Config file ($NERDCTL_TOML): %s } // Since we store containers' stateful information on the filesystem per namespace, we need namespaces to be - // valid, safe path segments. This is enforced by store.ValidatePathComponent. + // valid, safe path segments. // Note that the container runtime will further enforce additional restrictions on namespace names // (containerd treats namespaces as valid identifiers - eg: alphanumericals + dash, starting with a letter) // See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names for // considerations about path segments identifiers. - if err = store.ValidatePathComponent(globalOptions.Namespace); err != nil { + if err = store.IsFilesystemSafe(globalOptions.Namespace); err != nil { return err } if appNeedsRootlessParentMain(cmd, args) { diff --git a/pkg/composer/lock.go b/pkg/composer/lock.go index 8fedda7bfc4..9006eca4bb3 100644 --- a/pkg/composer/lock.go +++ b/pkg/composer/lock.go @@ -20,7 +20,7 @@ import ( "os" "github.com/containerd/nerdctl/v2/pkg/clientutil" - "github.com/containerd/nerdctl/v2/pkg/lockutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) //nolint:unused @@ -39,10 +39,10 @@ func Lock(dataRoot string, address string) error { if err != nil { return err } - locked, err = lockutil.Lock(dataStore) + locked, err = filesystem.Lock(dataStore) return err } func Unlock() error { - return lockutil.Unlock(locked) + return filesystem.Unlock(locked) } diff --git a/pkg/internal/filesystem/atomic.go b/pkg/internal/filesystem/atomic.go new file mode 100644 index 00000000000..fc45def648c --- /dev/null +++ b/pkg/internal/filesystem/atomic.go @@ -0,0 +1,39 @@ +/* + 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 filesystem + +import ( + "os" + "path/filepath" +) + +func AtomicWrite(parent string, fileName string, perm os.FileMode, data []byte) error { + dest := filepath.Join(parent, fileName) + temp := filepath.Join(parent, ".temp."+fileName) + + err := os.WriteFile(temp, data, perm) + if err != nil { + return err + } + + err = os.Rename(temp, dest) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/internal/filesystem/consts.go b/pkg/internal/filesystem/consts.go new file mode 100644 index 00000000000..32c30246ef1 --- /dev/null +++ b/pkg/internal/filesystem/consts.go @@ -0,0 +1,21 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filesystem + +const ( + pathComponentMaxLength = 255 +) diff --git a/pkg/internal/filesystem/errors.go b/pkg/internal/filesystem/errors.go new file mode 100644 index 00000000000..c2ed4a14918 --- /dev/null +++ b/pkg/internal/filesystem/errors.go @@ -0,0 +1,23 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filesystem + +import "errors" + +var ( + ErrInvalidPath = errors.New("invalid path") +) diff --git a/pkg/lockutil/lockutil_unix.go b/pkg/internal/filesystem/lockutil_unix.go similarity index 98% rename from pkg/lockutil/lockutil_unix.go rename to pkg/internal/filesystem/lockutil_unix.go index c4655c58b6c..8d50b15b071 100644 --- a/pkg/lockutil/lockutil_unix.go +++ b/pkg/internal/filesystem/lockutil_unix.go @@ -16,7 +16,7 @@ limitations under the License. */ -package lockutil +package filesystem import ( "fmt" diff --git a/pkg/lockutil/lockutil_windows.go b/pkg/internal/filesystem/lockutil_windows.go similarity index 99% rename from pkg/lockutil/lockutil_windows.go rename to pkg/internal/filesystem/lockutil_windows.go index 205efde83f5..8dc6e3eb47d 100644 --- a/pkg/lockutil/lockutil_windows.go +++ b/pkg/internal/filesystem/lockutil_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package lockutil +package filesystem import ( "fmt" diff --git a/pkg/internal/filesystem/path.go b/pkg/internal/filesystem/path.go new file mode 100644 index 00000000000..d0b99df7f40 --- /dev/null +++ b/pkg/internal/filesystem/path.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 filesystem + +import ( + "errors" + "strings" +) + +var ( + errForbiddenChars = errors.New("forbidden characters in path component") + errForbiddenKeywords = errors.New("forbidden keywords in path component") + errInvalidPathTooLong = errors.New("path component must be shorter than 256 characters") + errInvalidPathEmpty = errors.New("path component cannot be empty") +) + +// ValidatePathComponent will enforce os specific filename restrictions on a single path component. +func ValidatePathComponent(pathComponent string) error { + // https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits + if len(pathComponent) > pathComponentMaxLength { + return errors.Join(ErrInvalidPath, errInvalidPathTooLong) + } + + if strings.TrimSpace(pathComponent) == "" { + return errors.Join(ErrInvalidPath, errInvalidPathEmpty) + } + + if err := validatePlatformSpecific(pathComponent); err != nil { + return errors.Join(ErrInvalidPath, err) + } + + return nil +} diff --git a/pkg/internal/filesystem/path_test.go b/pkg/internal/filesystem/path_test.go new file mode 100644 index 00000000000..fa3d2b2fbf2 --- /dev/null +++ b/pkg/internal/filesystem/path_test.go @@ -0,0 +1,85 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filesystem_test + +import ( + "fmt" + "runtime" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" +) + +func TestFilesystemRestrictions(t *testing.T) { + t.Parallel() + + invalid := []string{ + "/", + "/start", + "mid/dle", + "end/", + ".", + "..", + "", + fmt.Sprintf("A%0255s", "A"), + } + + valid := []string{ + fmt.Sprintf("A%0254s", "A"), + "test", + "test-hyphen", + ".start.dot", + "mid.dot", + "∞", + } + + if runtime.GOOS == "windows" { + invalid = append(invalid, []string{ + "\\start", + "mid\\dle", + "end\\", + "\\", + "\\.", + "com².whatever", + "lpT2", + "Prn.", + "nUl", + "AUX", + "AA", + "A:A", + "A\"A", + "A|A", + "A?A", + "A*A", + "end.dot.", + "end.space ", + }...) + } + + for _, v := range invalid { + err := filesystem.ValidatePathComponent(v) + assert.ErrorIs(t, err, filesystem.ErrInvalidPath, v) + } + + for _, v := range valid { + err := filesystem.ValidatePathComponent(v) + assert.NilError(t, err, v) + } +} diff --git a/pkg/store/filestore_unix.go b/pkg/internal/filesystem/path_unix.go similarity index 75% rename from pkg/store/filestore_unix.go rename to pkg/internal/filesystem/path_unix.go index b694b6fc744..4db2bd42e48 100644 --- a/pkg/store/filestore_unix.go +++ b/pkg/internal/filesystem/path_unix.go @@ -16,14 +16,14 @@ limitations under the License. */ -package store +package filesystem import ( "fmt" "regexp" ) -// Note that Darwin has different restrictions - though, we do not support Darwin at this point... +// Note that Darwin has different restrictions on colons. // https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names var ( disallowedKeywords = regexp.MustCompile(`^([.]|[.][.])$`) @@ -32,11 +32,11 @@ var ( func validatePlatformSpecific(pathComponent string) error { if reservedCharacters.MatchString(pathComponent) { - return fmt.Errorf("identifier %q cannot contain any of the following characters: %q", pathComponent, reservedCharacters) + return fmt.Errorf("%w: %q (%q)", errForbiddenChars, pathComponent, reservedCharacters) } if disallowedKeywords.MatchString(pathComponent) { - return fmt.Errorf("identifier %q cannot be any of the reserved keywords: %q", pathComponent, disallowedKeywords) + return fmt.Errorf("%w: %q (%q)", errForbiddenKeywords, pathComponent, disallowedKeywords) } return nil diff --git a/pkg/store/filestore_windows.go b/pkg/internal/filesystem/path_windows.go similarity index 78% rename from pkg/store/filestore_windows.go rename to pkg/internal/filesystem/path_windows.go index 7c599803cb5..1853d3aed21 100644 --- a/pkg/store/filestore_windows.go +++ b/pkg/internal/filesystem/path_windows.go @@ -14,9 +14,10 @@ limitations under the License. */ -package store +package filesystem import ( + "errors" "fmt" "regexp" ) @@ -26,19 +27,21 @@ import ( var ( disallowedKeywords = regexp.MustCompile(`(?i)^(con|prn|nul|aux|com[1-9¹²³]|lpt[1-9¹²³])([.].*)?$`) reservedCharacters = regexp.MustCompile(`[\x{0}-\x{1f}<>:"/\\|?*]`) + + errNoEndingSpaceDot = errors.New("component cannot end with a space or dot") ) func validatePlatformSpecific(pathComponent string) error { if reservedCharacters.MatchString(pathComponent) { - return fmt.Errorf("identifier %q cannot contain any of the following characters: %q", pathComponent, reservedCharacters) + return fmt.Errorf("%w: %q (%q)", errForbiddenChars, pathComponent, reservedCharacters) } if disallowedKeywords.MatchString(pathComponent) { - return fmt.Errorf("identifier %q cannot be any of the reserved keywords: %q", pathComponent, disallowedKeywords) + return fmt.Errorf("%w: %q (%q)", errForbiddenKeywords, pathComponent, disallowedKeywords) } if pathComponent[len(pathComponent)-1:] == "." || pathComponent[len(pathComponent)-1:] == " " { - return fmt.Errorf("identifier %q cannot end with a space or dot", pathComponent) + return fmt.Errorf("%w: %q", errNoEndingSpaceDot, pathComponent) } return nil diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 3030660cf0e..65cc1ddd31e 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -38,7 +38,7 @@ import ( "github.com/containerd/errdefs" "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/pkg/lockutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) const ( @@ -160,7 +160,7 @@ func getLockPath(dataStore, ns, id string) string { // WaitForLogger waits until the logger has finished executing and processing container logs func WaitForLogger(dataStore, ns, id string) error { - return lockutil.WithDirLock(getLockPath(dataStore, ns, id), func() error { + return filesystem.WithDirLock(getLockPath(dataStore, ns, id), func() error { return nil }) } @@ -314,7 +314,7 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) { // the logger will obtain an exclusive lock on a file until the container is // stopped and the driver has finished processing all output, // so that waiting log viewers can be signalled when the process is complete. - return lockutil.WithDirLock(loggerLock, func() error { + return filesystem.WithDirLock(loggerLock, func() error { if err := ready(); err != nil { return err } diff --git a/pkg/netutil/store.go b/pkg/netutil/store.go index 4d07db28fa9..627468e8f85 100644 --- a/pkg/netutil/store.go +++ b/pkg/netutil/store.go @@ -24,7 +24,7 @@ import ( "github.com/containerd/errdefs" - "github.com/containerd/nerdctl/v2/pkg/lockutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) // NOTE: libcni is not safe to use concurrently - or at least delegates concurrency management to the consumer. @@ -48,7 +48,7 @@ func fsRemove(e *CNIEnv, net *NetworkConfig) error { } return net.clean() } - return lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) + return filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) } func fsExists(e *CNIEnv, name string) (bool, error) { @@ -62,7 +62,7 @@ func fsWrite(e *CNIEnv, net *NetworkConfig) error { // Concurrent access may independently first figure out that a given network is missing, and while the lock // here will prevent concurrent writes, one of the routines will fail. // Consuming code MUST account for that scenario. - return lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + return filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { if _, err := os.Stat(filename); err == nil { return errdefs.ErrAlreadyExists } @@ -73,7 +73,7 @@ func fsWrite(e *CNIEnv, net *NetworkConfig) error { func fsRead(e *CNIEnv) ([]*NetworkConfig, error) { var nc []*NetworkConfig var err error - err = lockutil.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + err = filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { namespaced := []string{} var common []string common, err = libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index d035a4ad968..79385396f59 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -38,8 +38,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/bypass4netnsutil" "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" - "github.com/containerd/nerdctl/v2/pkg/lockutil" "github.com/containerd/nerdctl/v2/pkg/namestore" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/netutil/nettype" @@ -109,11 +109,11 @@ func Run(stdin io.Reader, stderr io.Writer, event, dataStore, cniPath, cniNetcon if err != nil { return err } - lock, err := lockutil.Lock(filepath.Join(cniNetconfPath, ".cni-concurrency.lock")) + lock, err := filesystem.Lock(filepath.Join(cniNetconfPath, ".cni-concurrency.lock")) if err != nil { return err } - defer lockutil.Unlock(lock) + defer filesystem.Unlock(lock) opts, err := newHandlerOpts(&state, dataStore, cniPath, cniNetconfPath, bridgeIP) if err != nil { diff --git a/pkg/store/filestore.go b/pkg/store/filestore.go index 312155230fa..2ad3982eb4b 100644 --- a/pkg/store/filestore.go +++ b/pkg/store/filestore.go @@ -21,10 +21,9 @@ import ( "fmt" "os" "path/filepath" - "strings" "sync" - "github.com/containerd/nerdctl/v2/pkg/lockutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) // TODO: implement a read-lock in lockutil, in addition to the current exclusive write-lock @@ -75,7 +74,7 @@ type fileStore struct { func (vs *fileStore) Lock() error { vs.mutex.Lock() - dirFile, err := lockutil.Lock(vs.dir) + dirFile, err := filesystem.Lock(vs.dir) if err != nil { return errors.Join(ErrLockFailure, err) } @@ -96,7 +95,7 @@ func (vs *fileStore) Release() error { vs.locked = nil }() - if err := lockutil.Unlock(vs.locked); err != nil { + if err := filesystem.Unlock(vs.locked); err != nil { return errors.Join(ErrLockFailure, err) } @@ -194,7 +193,11 @@ func (vs *fileStore) Set(data []byte, key ...string) error { } } - return atomicWrite(parent, fileName, vs.filePerm, data) + if err := filesystem.AtomicWrite(parent, fileName, vs.filePerm, data); err != nil { + return errors.Join(ErrSystemFailure, err) + } + + return nil } func (vs *fileStore) List(key ...string) ([]string, error) { @@ -204,8 +207,8 @@ func (vs *fileStore) List(key ...string) ([]string, error) { // Unlike Get, Set and Delete, List can have zero length key for _, k := range key { - if err := ValidatePathComponent(k); err != nil { - return nil, err + if err := filesystem.ValidatePathComponent(k); err != nil { + return nil, errors.Join(ErrInvalidArgument, err) } } @@ -333,24 +336,6 @@ func (vs *fileStore) GroupSize(key ...string) (int64, error) { return size, nil } -// ValidatePathComponent will enforce os specific filename restrictions on a single path component -func ValidatePathComponent(pathComponent string) error { - // https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits - if len(pathComponent) > 255 { - return errors.Join(ErrInvalidArgument, errors.New("identifiers must be stricly shorter than 256 characters")) - } - - if strings.TrimSpace(pathComponent) == "" { - return errors.Join(ErrInvalidArgument, errors.New("identifier cannot be empty")) - } - - if err := validatePlatformSpecific(pathComponent); err != nil { - return errors.Join(ErrInvalidArgument, err) - } - - return nil -} - // validateAllPathComponents will enforce validation for a slice of components func validateAllPathComponents(pathComponent ...string) error { if len(pathComponent) == 0 { @@ -358,26 +343,17 @@ func validateAllPathComponents(pathComponent ...string) error { } for _, key := range pathComponent { - if err := ValidatePathComponent(key); err != nil { - return err + if err := filesystem.ValidatePathComponent(key); err != nil { + return errors.Join(ErrInvalidArgument, err) } } return nil } -func atomicWrite(parent string, fileName string, perm os.FileMode, data []byte) error { - dest := filepath.Join(parent, fileName) - temp := filepath.Join(parent, ".temp."+fileName) - - err := os.WriteFile(temp, data, perm) - if err != nil { - return errors.Join(ErrSystemFailure, err) - } - - err = os.Rename(temp, dest) - if err != nil { - return errors.Join(ErrSystemFailure, err) +func IsFilesystemSafe(identifier string) error { + if err := filesystem.ValidatePathComponent(identifier); err != nil { + return errors.Join(ErrInvalidArgument, err) } return nil diff --git a/pkg/store/filestore_test.go b/pkg/store/filestore_test.go index 58f4eebeef0..2496b96cbb1 100644 --- a/pkg/store/filestore_test.go +++ b/pkg/store/filestore_test.go @@ -17,8 +17,6 @@ package store import ( - "fmt" - "runtime" "testing" "time" @@ -220,60 +218,3 @@ func TestFileStoreConcurrent(t *testing.T) { }) assert.NilError(t, lErr, "locking should not error") } - -func TestFileStoreFilesystemRestrictions(t *testing.T) { - invalid := []string{ - "/", - "/start", - "mid/dle", - "end/", - ".", - "..", - "", - fmt.Sprintf("A%0255s", "A"), - } - - valid := []string{ - fmt.Sprintf("A%0254s", "A"), - "test", - "test-hyphen", - ".start.dot", - "mid.dot", - "∞", - } - - if runtime.GOOS == "windows" { - invalid = append(invalid, []string{ - "\\start", - "mid\\dle", - "end\\", - "\\", - "\\.", - "com².whatever", - "lpT2", - "Prn.", - "nUl", - "AUX", - "AA", - "A:A", - "A\"A", - "A|A", - "A?A", - "A*A", - "end.dot.", - "end.space ", - }...) - } - - for _, v := range invalid { - err := ValidatePathComponent(v) - assert.ErrorIs(t, err, ErrInvalidArgument, v) - } - - for _, v := range valid { - err := ValidatePathComponent(v) - assert.NilError(t, err, v) - } - -} diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index dd57f2821e7..14d322de07b 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -44,7 +44,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" - "github.com/containerd/nerdctl/v2/pkg/lockutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -524,14 +524,14 @@ func M(m *testing.M) { os.Chmod(filepath.Dir(testLockFile), 0o777) // Acquire lock - lock, err := lockutil.Lock(filepath.Dir(testLockFile)) + lock, err := filesystem.Lock(filepath.Dir(testLockFile)) if err != nil { log.L.WithError(err).Errorf("failed acquiring testing lock %q", filepath.Dir(testLockFile)) return 1 } // Release... - defer lockutil.Unlock(lock) + defer filesystem.Unlock(lock) // Create marker file err = os.WriteFile(testLockFile, []byte("prevent testing from running in parallel for subpackages integration tests"), 0o666) From ff0e8ec80710a127f22eb6cac00b5d36bb19b3b8 Mon Sep 17 00:00:00 2001 From: apostasie Date: Thu, 15 May 2025 10:15:49 -0700 Subject: [PATCH 017/868] Cleanup leftover on build test Signed-off-by: apostasie --- cmd/nerdctl/builder/builder_build_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 09ff4bfc8b4..839fd0d6e01 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -110,6 +110,7 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("ignored")) + helpers.Anyhow("rmi", "-f", data.Identifier()) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, From d9cf5285101092f27d304d1898538ceb88e8c660 Mon Sep 17 00:00:00 2001 From: apostasie Date: Thu, 15 May 2025 10:39:06 -0700 Subject: [PATCH 018/868] Cleanup containers tests Signed-off-by: apostasie --- .../container/container_exec_linux_test.go | 3 + .../container_run_mount_linux_test.go | 2 + .../container_run_network_linux_test.go | 272 +++++++----------- 3 files changed, 104 insertions(+), 173 deletions(-) diff --git a/cmd/nerdctl/container/container_exec_linux_test.go b/cmd/nerdctl/container/container_exec_linux_test.go index 5ff812d9429..9eafe7939bd 100644 --- a/cmd/nerdctl/container/container_exec_linux_test.go +++ b/cmd/nerdctl/container/container_exec_linux_test.go @@ -65,6 +65,9 @@ func TestExecTTY(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + data.Labels().Set("container_name", data.Identifier()) } diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index c941d8d39fb..397ccf12969 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -352,6 +352,8 @@ func TestRunBindMountBind(t *testing.T) { "top", ) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container")) + // Save host rwDir location and container id for subtests data.Labels().Set("container", data.Identifier("container")) data.Labels().Set("rwDir", rwDir) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 46b9057e11a..f8a93aaa6a2 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -361,11 +361,8 @@ func TestRunWithInvalidPortThenCleanUp(t *testing.T) { testCase.SubTests = []*test.Case{ { Description: "Run a container with invalid ports, and then clean up.", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "--data-root", data.Temp().Path(), "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--data-root", data.Temp().Path(), "--rm", "--name", data.Identifier(), "-p", "22200-22299:22200-22299", testutil.CommonImage) + return helpers.Command("run", "--data-root", data.Temp().Path(), "--rm", "-p", "22200-22299:22200-22299", testutil.CommonImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -518,158 +515,104 @@ func TestSharedNetworkSetup(t *testing.T) { testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName1", data.Identifier("-container1")) - containerName1 := data.Labels().Get("containerName1") - helpers.Ensure("run", "-d", "--name", containerName1, - testutil.NginxAlpineImage) + data.Labels().Set("container1", data.Identifier("container1")) + helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), + testutil.CommonImage, "sleep", "inf") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier("-container1")) + helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, SubTests: []*test.Case{ { Description: "Test network is shared", NoParallel: true, // The validation involves starting of the main container: container1 Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, - "--network=container:"+data.Labels().Get("containerName1"), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure( + "run", "-d", "--name", data.Identifier("container2"), + "--network=container:"+data.Labels().Get("container1"), testutil.NginxAlpineImage) - return cmd + data.Labels().Set("container2", data.Identifier("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - containerName2 := data.Identifier() - assert.Assert(t, strings.Contains(helpers.Capture("exec", containerName2, "wget", "-qO-", "http://127.0.0.1:80"), testutil.NginxAlpineIndexHTMLSnippet), info) - helpers.Ensure("restart", data.Labels().Get("containerName1")) - helpers.Ensure("stop", "--time=1", containerName2) - helpers.Ensure("start", containerName2) - assert.Assert(t, strings.Contains(helpers.Capture("exec", containerName2, "wget", "-qO-", "http://127.0.0.1:80"), testutil.NginxAlpineIndexHTMLSnippet), info) + SubTests: []*test.Case{ + { + NoParallel: true, + Description: "Test network is shared", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") + }, - } + Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), + }, + { + NoParallel: true, + Description: "Test network is shared after restart", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("restart", data.Labels().Get("container1")) + helpers.Ensure("stop", "--time=1", data.Labels().Get("container2")) + helpers.Ensure("start", data.Labels().Get("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("container2")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") + + }, + Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), + }, }, }, { Description: "Test uts is supported in shared network", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--uts", "host", - "--network=container:"+data.Labels().Get("containerName1"), - testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - } + return helpers.Command("run", "--rm", "--uts", "host", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage) }, + Expected: test.Expects(0, nil, nil), }, { Description: "Test dns is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--dns", "0.1.2.3", - "--network=container:"+data.Labels().Get("containerName1"), - testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } + return helpers.Command("run", "--rm", "--dns", "0.1.2.3", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage) }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test dns options is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "--name", containerName2, "--dns-option", "attempts:5", - "--network=container:"+data.Labels().Get("containerName1"), - testutil.AlpineImage, "cat", "/etc/resolv.conf") - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - // The Option doesnt throw an error but is never inserted to the resolv.conf - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, !strings.Contains(stdout, "attempts:5"), info) - }, - } + return helpers.Command("run", "--rm", "--dns-option", "attempts:5", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage, "cat", "/etc/resolv.conf") }, + // The Option doesn't throw an error but is never inserted to the resolv.conf + Expected: test.Expects(0, nil, expect.DoesNotContain("attempts:5")), }, { Description: "Test publish is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--publish", "80:8080", - "--network=container:"+data.Labels().Get("containerName1"), + return helpers.Command("run", "--rm", "--publish", "80:8080", + "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test hostname is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--hostname", "test", - "--network=container:"+data.Labels().Get("containerName1"), + return helpers.Command("run", "--rm", "--hostname", "test", + "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, }, } @@ -682,15 +625,15 @@ func TestSharedNetworkWithNone(t *testing.T) { Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), "--network", "none", - testutil.NginxAlpineImage) + testutil.CommonImage, "sleep", "inf") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container1")) - helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "-d", "--name", data.Identifier("container2"), - "--network=container:"+data.Identifier("container1"), testutil.NginxAlpineImage) + return helpers.Command("run", "--rm", + "--network=container:"+data.Identifier("container1"), testutil.CommonImage) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } @@ -914,13 +857,14 @@ func TestNoneNetworkHostName(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, Setup: func(data test.Data, helpers test.Helpers) { - output := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", "none", testutil.NginxAlpineImage) + output := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", "none", testutil.CommonImage, "sleep", "inf") assert.Assert(helpers.T(), len(output) > 12, output) data.Labels().Set("hostname", output[:12]) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Identifier(), "cat", "/etc/hostname") @@ -939,20 +883,20 @@ func TestHostNetworkHostName(t *testing.T) { testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName1", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{ + Output: func(stdout, info string, t *testing.T) { + data.Labels().Set("hostHostname", stdout) + }, + }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("cat", "/etc/hostname") + return helpers.Command("run", "--rm", + "--network", "host", + testutil.AlpineImage, "cat", "/etc/hostname") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - hostname := stdout - assert.Assert(t, strings.Compare(strings.TrimSpace(helpers.Capture("run", "--name", data.Identifier(), "--network", "host", testutil.AlpineImage, "cat", "/etc/hostname")), strings.TrimSpace(hostname)) == 0, info) - }, + Output: expect.Equals(data.Labels().Get("hostHostname")), } }, } @@ -963,27 +907,18 @@ func TestNoneNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName1", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "-d", "--name", data.Identifier(), "--network", "none", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - out := helpers.Capture("exec", data.Identifier(), "cat", "/etc/resolv.conf") - assert.Assert(t, strings.Contains(out, "0.1.2.3"), info) - assert.Assert(t, strings.Contains(out, "example.com"), info) - assert.Assert(t, strings.Contains(out, "attempts:5"), info) - assert.Assert(t, strings.Contains(out, "timeout:3"), info) - - }, - } - }, + return helpers.Command("run", "--rm", + "--network", "none", + "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", + testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(0, nil, expect.Contains( + "0.1.2.3", + "example.com", + "attempts:5", + "timeout:3", + )), } testCase.Run(t) } @@ -992,27 +927,18 @@ func TestHostNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName1", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "-d", "--name", data.Identifier(), "--network", "host", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - out := helpers.Capture("exec", data.Identifier(), "cat", "/etc/resolv.conf") - assert.Assert(t, strings.Contains(out, "0.1.2.3"), info) - assert.Assert(t, strings.Contains(out, "example.com"), info) - assert.Assert(t, strings.Contains(out, "attempts:5"), info) - assert.Assert(t, strings.Contains(out, "timeout:3"), info) - - }, - } - }, + return helpers.Command("run", "--rm", + "--network", "host", + "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", + testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(0, nil, expect.Contains( + "0.1.2.3", + "example.com", + "attempts:5", + "timeout:3", + )), } testCase.Run(t) } From 3388ef6a2252f3feb6fe6e04a1ca1462393b84f1 Mon Sep 17 00:00:00 2001 From: apostasie Date: Thu, 15 May 2025 12:01:02 -0700 Subject: [PATCH 019/868] Re-enable tests following fixes Signed-off-by: apostasie --- cmd/nerdctl/image/image_list_test.go | 6 ++---- cmd/nerdctl/volume/volume_list_test.go | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 6eba01c84c5..96b04c3faa7 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -269,15 +269,13 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" }, { Description: "since=non-exists-image", - Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3511"), Command: test.Command("images", "--filter", "since=non-exists-image"), - Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("No such image: ")}, nil), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such image: ")}, nil), }, { Description: "before=non-exists-image", - Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3511"), Command: test.Command("images", "--filter", "before=non-exists-image"), - Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("No such image: ")}, nil), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such image: ")}, nil), }, }, } diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index d666595abc6..8dca19fa584 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -282,8 +282,6 @@ func TestVolumeLsFilter(t *testing.T) { }, { Description: "Retrieving name=volume1 and name=volume2", - // Nerdctl filter behavior is broken - Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3452"), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Labels().Get("vol1"), "--filter", "name="+data.Labels().Get("vol2")) }, From d4cfdd94c5f6dc92bca57264b610d5379ec3ee86 Mon Sep 17 00:00:00 2001 From: apostasie Date: Thu, 15 May 2025 13:52:55 -0700 Subject: [PATCH 020/868] Fix use on non unique identifier Signed-off-by: apostasie --- cmd/nerdctl/container/container_commit_linux_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index e1a167c0633..a2b26dbd88e 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -53,8 +53,8 @@ func TestKubeCommitSave(t *testing.T) { } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - helpers.Ensure("commit", data.Labels().Get("containerID"), "testcommitsave") - return helpers.Command("save", "testcommitsave") + helpers.Ensure("commit", data.Labels().Get("containerID"), data.Identifier("testcommitsave")) + return helpers.Command("save", data.Identifier("testcommitsave")) } testCase.Expected = test.Expects(0, nil, nil) From d28b664b37319643856ef4efe14e3a10fb353bb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 22:04:43 +0000 Subject: [PATCH 021/868] build(deps): bump docker/build-push-action from 6.16.0 to 6.17.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.17.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/14487ce63c7a62a4a324b0bfb37086795e31c6c1...1dc73863535b631f98b2378be8619f83b136f4a0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 6.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 969eb67229f..300fd4574f5 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: context: . platforms: linux/amd64,linux/arm64 From cd1a24243ecbdde5c0777f5fd65a0fe77d55eda4 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 18:49:07 -0700 Subject: [PATCH 022/868] Relax --runtime restrictions Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_up_test.go | 7 +------ pkg/cmd/container/run_runtime.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/compose/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go index e162ee6806d..eb17c7151c9 100644 --- a/cmd/nerdctl/compose/compose_up_test.go +++ b/cmd/nerdctl/compose/compose_up_test.go @@ -27,7 +27,6 @@ import ( "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/1942 @@ -48,11 +47,7 @@ services: c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d") expected := icmd.Expected{ ExitCode: 1, - Err: `exec: \"invalid\": executable file not found in $PATH`, - } - // Docker expected err is different - if nerdtest.IsDocker() { - expected.Err = `unknown or invalid runtime name: invalid` + Err: `invalid runtime name`, } c.Assert(expected) } diff --git a/pkg/cmd/container/run_runtime.go b/pkg/cmd/container/run_runtime.go index b6d0ac4965e..ac03e7b5b8c 100644 --- a/pkg/cmd/container/run_runtime.go +++ b/pkg/cmd/container/run_runtime.go @@ -18,6 +18,7 @@ package container import ( "context" + "os/exec" "strings" "github.com/opencontainers/runtime-spec/specs-go" @@ -49,8 +50,14 @@ func generateRuntimeCOpts(cgroupManager, runtimeStr string) ([]containerd.NewCon runtimeOpts = nil } } else { - // runtimeStr is a runc binary - runcOpts.BinaryName = runtimeStr + // runtimeStr may be a runc binary - check that it exists + // if it does not, treat it as a runtime + ex, err := exec.LookPath(runtimeStr) + if err != nil { + runtime = runtimeStr + } else { + runcOpts.BinaryName = ex + } } } o := containerd.WithRuntime(runtime, runtimeOpts) From c0e32debaea7ea88746fc8ee39e101d8e6bf5b03 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 20:56:16 -0700 Subject: [PATCH 023/868] Add support for compose AdditionalContexts Signed-off-by: apostasie --- pkg/composer/serviceparser/build.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/composer/serviceparser/build.go b/pkg/composer/serviceparser/build.go index c13b264301a..d797d0690ba 100644 --- a/pkg/composer/serviceparser/build.go +++ b/pkg/composer/serviceparser/build.go @@ -34,7 +34,7 @@ import ( func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName string) (*Build, error) { if unknown := reflectutil.UnknownNonEmptyFields(c, - "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", + "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", "AdditionalContexts", ); len(unknown) > 0 { log.L.Warnf("Ignoring: build: %+v", unknown) } @@ -72,6 +72,10 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st b.BuildArgs = append(b.BuildArgs, "--cache-from="+s) } + for k, v := range c.AdditionalContexts { + b.BuildArgs = append(b.BuildArgs, "--build-context="+k+"="+v) + } + if c.Target != "" { b.BuildArgs = append(b.BuildArgs, "--target="+c.Target) } From d166c3b0d8d2656b7448b8049aaaf94d7e0161d7 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 21:18:09 -0700 Subject: [PATCH 024/868] Use only one build base in Dockerfile Signed-off-by: apostasie --- Dockerfile | 69 +++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0c218b8f34..e66d165188d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,11 +55,13 @@ ARG KUBO_VERSION=v0.34.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base-debian +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base COPY --from=xx / / ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ + make \ git \ + curl \ dpkg-dev ARG TARGETARCH # libbtrfs: for containerd @@ -74,10 +76,10 @@ RUN xx-apt-get update -qq && xx-apt-get install -qq --no-install-recommends \ RUN git config --global advice.detachedHead false ADD hack/git-checkout-tag-with-hash.sh /usr/local/bin/ -FROM build-base-debian AS build-containerd +FROM build-base AS build-containerd ARG TARGETARCH ARG CONTAINERD_VERSION -RUN git clone --quiet --depth 1 --branch "${CONTAINERD_VERSION%@*}" https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd +RUN git clone --quiet --depth 1 --branch "${CONTAINERD_VERSION%%@*}" https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd WORKDIR /go/src/github.com/containerd/containerd RUN git-checkout-tag-with-hash.sh ${CONTAINERD_VERSION} && \ mkdir -p /out /out/$TARGETARCH && \ @@ -85,10 +87,10 @@ RUN git-checkout-tag-with-hash.sh ${CONTAINERD_VERSION} && \ RUN GO=xx-go make STATIC=1 && \ cp -a bin/containerd bin/containerd-shim-runc-v2 bin/ctr /out/$TARGETARCH -FROM build-base-debian AS build-runc +FROM build-base AS build-runc ARG RUNC_VERSION ARG TARGETARCH -RUN git clone --quiet --depth 1 --branch "${RUNC_VERSION%@*}" https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc +RUN git clone --quiet --depth 1 --branch "${RUNC_VERSION%%@*}" https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc WORKDIR /go/src/github.com/opencontainers/runc RUN git-checkout-tag-with-hash.sh ${RUNC_VERSION} && \ mkdir -p /out @@ -96,10 +98,10 @@ ENV CGO_ENABLED=1 RUN GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make static && \ xx-verify --static runc && cp -v -a runc /out/runc.${TARGETARCH} -FROM build-base-debian AS build-bypass4netns +FROM build-base AS build-bypass4netns ARG BYPASS4NETNS_VERSION ARG TARGETARCH -RUN git clone --quiet --depth 1 --branch "${BYPASS4NETNS_VERSION%@*}" https://github.com/rootless-containers/bypass4netns.git /go/src/github.com/rootless-containers/bypass4netns +RUN git clone --quiet --depth 1 --branch "${BYPASS4NETNS_VERSION%%@*}" https://github.com/rootless-containers/bypass4netns.git /go/src/github.com/rootless-containers/bypass4netns WORKDIR /go/src/github.com/rootless-containers/bypass4netns RUN git-checkout-tag-with-hash.sh ${BYPASS4NETNS_VERSION} && \ mkdir -p /out/${TARGETARCH} @@ -107,20 +109,20 @@ ENV CGO_ENABLED=1 RUN GO=xx-go make static && \ xx-verify --static bypass4netns && cp -a bypass4netns bypass4netnsd /out/${TARGETARCH} -FROM build-base-debian AS build-gomodjail +FROM build-base AS build-gomodjail ARG GOMODJAIL_VERSION ARG TARGETARCH -RUN git clone --quiet --depth 1 --branch "${GOMODJAIL_VERSION%@*}" https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail +RUN git clone --quiet --depth 1 --branch "${GOMODJAIL_VERSION%%@*}" https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail WORKDIR /go/src/github.com/AkihiroSuda/gomodjail RUN git-checkout-tag-with-hash.sh ${GOMODJAIL_VERSION} && \ mkdir -p /out/${TARGETARCH} RUN GO=xx-go make STATIC=1 && \ xx-verify --static _output/bin/gomodjail && cp -a _output/bin/gomodjail /out/${TARGETARCH} -FROM build-base-debian AS build-kubo +FROM build-base AS build-kubo ARG KUBO_VERSION ARG TARGETARCH -RUN git clone --quiet --depth 1 --branch "${KUBO_VERSION%@*}" https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo +RUN git clone --quiet --depth 1 --branch "${KUBO_VERSION%%@*}" https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo WORKDIR /go/src/github.com/ipfs/kubo RUN git-checkout-tag-with-hash.sh ${KUBO_VERSION} && \ mkdir -p /out/${TARGETARCH} @@ -129,11 +131,6 @@ RUN xx-go --wrap && \ make build && \ xx-verify --static cmd/ipfs/ipfs && cp -a cmd/ipfs/ipfs /out/${TARGETARCH} -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build-base -RUN apk add --no-cache make git curl -RUN git config --global advice.detachedHead false -ADD hack/git-checkout-tag-with-hash.sh /usr/local/bin/ - FROM build-base AS build-minimal RUN BINDIR=/out/bin make binaries install # We do not set CMD to `go test` here, because it requires systemd @@ -148,12 +145,12 @@ RUN mkdir -p /out/share/doc/nerdctl-full && touch /out/share/doc/nerdctl-full/RE ARG CONTAINERD_VERSION COPY --from=build-containerd /out/${TARGETARCH:-amd64}/* /out/bin/ COPY --from=build-containerd /out/containerd.service /out/lib/systemd/system/containerd.service -RUN echo "- containerd: ${CONTAINERD_VERSION/@*}" >> /out/share/doc/nerdctl-full/README.md +RUN echo "- containerd: ${CONTAINERD_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG RUNC_VERSION COPY --from=build-runc /out/runc.${TARGETARCH:-amd64} /out/bin/runc -RUN echo "- runc: ${RUNC_VERSION/@*}" >> /out/share/doc/nerdctl-full/README.md +RUN echo "- runc: ${RUNC_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG CNI_PLUGINS_VERSION -RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION/@BINARY}; \ +RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION%%@*}; \ fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VERSION}.tgz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/cni-plugins-${CNI_PLUGINS_VERSION}" | sha256sum -c && \ @@ -162,7 +159,7 @@ RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION/@BINARY}; \ rm -f "${fname}" && \ echo "- CNI plugins: ${CNI_PLUGINS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BUILDKIT_VERSION -RUN BUILDKIT_VERSION=${BUILDKIT_VERSION/@BINARY}; \ +RUN BUILDKIT_VERSION=${BUILDKIT_VERSION%%@*}; \ fname="buildkit-${BUILDKIT_VERSION}.${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildkit-${BUILDKIT_VERSION}" | sha256sum -c && \ @@ -177,7 +174,7 @@ RUN cd /out/lib/systemd/system && \ echo "" >> buildkit.service && \ echo "# This file was converted from containerd.service, with \`sed -E '${sedcomm}'\`" >> buildkit.service ARG STARGZ_SNAPSHOTTER_VERSION -RUN STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION/@BINARY}; \ +RUN STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \ curl -o "stargz-snapshotter.service" -fsSL --proto '=https' --tlsv1.2 "https://raw.githubusercontent.com/containerd/stargz-snapshotter/${STARGZ_SNAPSHOTTER_VERSION}/script/config/etc/systemd/system/stargz-snapshotter.service" && \ @@ -188,13 +185,13 @@ RUN STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION/@BINARY}; \ mv stargz-snapshotter.service /out/lib/systemd/system/stargz-snapshotter.service && \ echo "- Stargz Snapshotter: ${STARGZ_SNAPSHOTTER_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG IMGCRYPT_VERSION -RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%@*}" https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \ +RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%%@*}" https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \ cd /go/src/github.com/containerd/imgcrypt && \ git-checkout-tag-with-hash.sh "${IMGCRYPT_VERSION}" && \ CGO_ENABLED=0 make && DESTDIR=/out make install && \ - echo "- imgcrypt: ${IMGCRYPT_VERSION/@*}" >> /out/share/doc/nerdctl-full/README.md + echo "- imgcrypt: ${IMGCRYPT_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG SLIRP4NETNS_VERSION -RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION/@BINARY}; \ +RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION%%@*}; \ fname="slirp4netns-$(cat /target_uname_m)" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/slirp4netns-${SLIRP4NETNS_VERSION}" | sha256sum -c && \ @@ -203,9 +200,9 @@ RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION/@BINARY}; \ echo "- slirp4netns: ${SLIRP4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BYPASS4NETNS_VERSION COPY --from=build-bypass4netns /out/${TARGETARCH:-amd64}/* /out/bin/ -RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION/@*}" >> /out/share/doc/nerdctl-full/README.md +RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG FUSE_OVERLAYFS_VERSION -RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION/@BINARY}; \ +RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION%%@*}; \ fname="fuse-overlayfs-$(cat /target_uname_m)" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/fuse-overlayfs-${FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ @@ -213,22 +210,24 @@ RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION/@BINARY}; \ chmod +x /out/bin/fuse-overlayfs && \ echo "- fuse-overlayfs: ${FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG CONTAINERD_FUSE_OVERLAYFS_VERSION -RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION/@BINARY}; \ - fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION/v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ +RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION%%@*}; \ + fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION##*v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" && \ echo "- containerd-fuse-overlayfs: ${CONTAINERD_FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG TINI_VERSION -RUN TINI_VERSION=${TINI_VERSION/@BINARY}; \ +RUN TINI_VERSION=${TINI_VERSION%%@*}; \ fname="tini-static-${TARGETARCH:-amd64}" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/tini-${TINI_VERSION}" | sha256sum -c && \ cp -a "${fname}" /out/bin/tini && chmod +x /out/bin/tini && \ echo "- Tini: ${TINI_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BUILDG_VERSION -RUN BUILDG_VERSION=${BUILDG_VERSION/@BINARY}; \ +# FIXME: this is a mildly-confusing approach. Buildkit will perform some "smart" replacement at build time and output +# confusing debugging information, eg: BUILDG_VERSION will appear as if the original ARG value was used. +RUN BUILDG_VERSION=${BUILDG_VERSION%%@*}; \ fname="buildg-${BUILDG_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildg-${BUILDG_VERSION}" | sha256sum -c && \ @@ -236,7 +235,7 @@ RUN BUILDG_VERSION=${BUILDG_VERSION/@BINARY}; \ rm -f "${fname}" && \ echo "- buildg: ${BUILDG_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG ROOTLESSKIT_VERSION -RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION/@BINARY}; \ +RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION%%@*}; \ fname="rootlesskit-$(cat /target_uname_m).tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/rootlesskit-${ROOTLESSKIT_VERSION}" | sha256sum -c && \ @@ -249,10 +248,10 @@ RUN echo "- gomodjail: ${GOMODJAIL_VERSION}" >> /out/share/doc/nerdctl-full/READ RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \ echo "## License" >> /out/share/doc/nerdctl-full/README.md && \ - echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/${SLIRP4NETNS_VERSION/@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ - echo "- bin/fuse-overlayfs: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/containers/fuse-overlayfs/blob/${FUSE_OVERLAYFS_VERSION/@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ + echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/${SLIRP4NETNS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ + echo "- bin/fuse-overlayfs: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/containers/fuse-overlayfs/blob/${FUSE_OVERLAYFS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/{runc,bypass4netns,bypass4netnsd}: Apache License 2.0, statically linked with libseccomp ([LGPL 2.1](https://github.com/seccomp/libseccomp/blob/main/LICENSE), source code available at https://github.com/seccomp/libseccomp/)" >> /out/share/doc/nerdctl-full/README.md && \ - echo "- bin/tini: [MIT License](https://github.com/krallin/tini/blob/${TINI_VERSION/@*}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \ + echo "- bin/tini: [MIT License](https://github.com/krallin/tini/blob/${TINI_VERSION%%@*}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- Other files: [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)" >> /out/share/doc/nerdctl-full/README.md FROM build-dependencies AS build-full @@ -310,7 +309,7 @@ RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ git \ make # We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" -COPY --from=build-base-debian /usr/local/go /usr/local/go +COPY --from=build-base /usr/local/go /usr/local/go ARG TARGETARCH ENV PATH=/usr/local/go/bin:$PATH ARG GOTESTSUM_VERSION From cbd4ef2360ef6bdeed518eb71fc6b379494cea61 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 17 May 2025 07:27:47 +0000 Subject: [PATCH 025/868] test: update test logic in TestParsePortsLabel The current implementation does not compare []cni.PortMapping{} obtained from labelMap and ParsePortsLabel() and want. Therefore, this commit will update so that the evaluation is performed. Signed-off-by: Hayato Kiwata --- pkg/portutil/portutil_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/portutil/portutil_test.go b/pkg/portutil/portutil_test.go index d14c79786c3..16ad1fd9430 100644 --- a/pkg/portutil/portutil_test.go +++ b/pkg/portutil/portutil_test.go @@ -178,10 +178,10 @@ func TestParsePortsLabel(t *testing.T) { }, want: []cni.PortMapping{ { - HostPort: 3000, - ContainerPort: 8080, + HostPort: 12345, + ContainerPort: 10000, Protocol: "tcp", - HostIP: "127.0.0.1", + HostIP: "0.0.0.0", }, }, wantErr: false, @@ -219,7 +219,7 @@ func TestParsePortsLabel(t *testing.T) { } if !reflect.DeepEqual(got, tt.want) { if len(got) == len(tt.want) { - if len(got) > 1 { + if len(got) > 0 { var hostPorts []int32 var containerPorts []int32 for _, value := range got { @@ -235,6 +235,14 @@ func TestParsePortsLabel(t *testing.T) { if (hostPorts[len(hostPorts)-1] - hostPorts[0]) != (containerPorts[len(hostPorts)-1] - containerPorts[0]) { t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) } + sort.Slice(got, func(i, j int) bool { + return got[i].HostPort < got[j].HostPort + }) + for i := 0; i < len(got); i++ { + if got[i].HostPort != tt.want[i].HostPort || got[i].ContainerPort != tt.want[i].ContainerPort || got[i].Protocol != tt.want[i].Protocol || got[i].HostIP != tt.want[i].HostIP { + t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) + } + } } } else { t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) From 1c74a704e30a1a3a0f6e18ce0c67a3ea55eb7ec4 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 17 May 2025 14:25:14 +0000 Subject: [PATCH 026/868] test: update test logic in TestTestParseFlagPWithPlatformSpec This test does not run in the rootfull Linux environment. Also, there are some cases where the test does not run. Therefore, this commit modifies all tests to perform the evaluation. Note that when the host ports are not specified in the -p option of `nerdctl run`, ports are randomly assigned, but it is not known what port number will be assigned in each test. Therefore, this commit modifies not to compare host ports. Signed-off-by: Hayato Kiwata --- pkg/portutil/portutil_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pkg/portutil/portutil_test.go b/pkg/portutil/portutil_test.go index 16ad1fd9430..8b79deb192d 100644 --- a/pkg/portutil/portutil_test.go +++ b/pkg/portutil/portutil_test.go @@ -29,7 +29,7 @@ import ( ) func TestTestParseFlagPWithPlatformSpec(t *testing.T) { - if runtime.GOOS != "Linux" || rootlessutil.IsRootless() { + if runtime.GOOS != "linux" || rootlessutil.IsRootless() { t.Skip("no non-Linux platform or rootless mode in Linux are not supported yet") } type args struct { @@ -48,7 +48,6 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { }, want: []cni.PortMapping{ { - HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", HostIP: "0.0.0.0", @@ -63,13 +62,11 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { }, want: []cni.PortMapping{ { - HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", HostIP: "0.0.0.0", }, { - HostPort: 3001, ContainerPort: 3001, Protocol: "tcp", HostIP: "0.0.0.0", @@ -92,13 +89,11 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { }, want: []cni.PortMapping{ { - HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", HostIP: "0.0.0.0", }, { - HostPort: 3001, ContainerPort: 3001, Protocol: "tcp", HostIP: "0.0.0.0", @@ -113,15 +108,13 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { }, want: []cni.PortMapping{ { - HostPort: 3000, ContainerPort: 3000, - Protocol: "tcp", + Protocol: "udp", HostIP: "0.0.0.0", }, { - HostPort: 3001, ContainerPort: 3001, - Protocol: "tcp", + Protocol: "udp", HostIP: "0.0.0.0", }, }, @@ -138,7 +131,7 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { } if !reflect.DeepEqual(got, tt.want) { if len(got) == len(tt.want) { - if len(got) > 1 { + if len(got) > 0 { var hostPorts []int32 var containerPorts []int32 for _, value := range got { @@ -154,6 +147,14 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { if (hostPorts[len(hostPorts)-1] - hostPorts[0]) != (containerPorts[len(hostPorts)-1] - containerPorts[0]) { t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) } + sort.Slice(got, func(i, j int) bool { + return got[i].HostPort < got[j].HostPort + }) + for i := 0; i < len(got); i++ { + if got[i].ContainerPort != tt.want[i].ContainerPort || got[i].Protocol != tt.want[i].Protocol || got[i].HostIP != tt.want[i].HostIP { + t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) + } + } } } else { t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) From 721e285b0d23301444f73d2952a00aa7dc639651 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 13:59:44 -0700 Subject: [PATCH 027/868] Revamp lock Signed-off-by: apostasie --- pkg/internal/filesystem/consts.go | 1 + pkg/internal/filesystem/errors.go | 3 + pkg/internal/filesystem/lock.go | 125 +++++++++ pkg/internal/filesystem/lock_test.go | 273 ++++++++++++++++++++ pkg/internal/filesystem/lock_unix.go | 59 +++++ pkg/internal/filesystem/lock_windows.go | 58 +++++ pkg/internal/filesystem/lockutil_unix.go | 78 ------ pkg/internal/filesystem/lockutil_windows.go | 65 ----- pkg/logging/logging.go | 9 +- pkg/netutil/store.go | 6 +- 10 files changed, 524 insertions(+), 153 deletions(-) create mode 100644 pkg/internal/filesystem/lock.go create mode 100644 pkg/internal/filesystem/lock_test.go create mode 100644 pkg/internal/filesystem/lock_unix.go create mode 100644 pkg/internal/filesystem/lock_windows.go delete mode 100644 pkg/internal/filesystem/lockutil_unix.go delete mode 100644 pkg/internal/filesystem/lockutil_windows.go diff --git a/pkg/internal/filesystem/consts.go b/pkg/internal/filesystem/consts.go index 32c30246ef1..b17236822d2 100644 --- a/pkg/internal/filesystem/consts.go +++ b/pkg/internal/filesystem/consts.go @@ -17,5 +17,6 @@ package filesystem const ( + lockPermission = 0o600 pathComponentMaxLength = 255 ) diff --git a/pkg/internal/filesystem/errors.go b/pkg/internal/filesystem/errors.go index c2ed4a14918..311f0c719bf 100644 --- a/pkg/internal/filesystem/errors.go +++ b/pkg/internal/filesystem/errors.go @@ -19,5 +19,8 @@ package filesystem import "errors" var ( + ErrLockFail = errors.New("failed to acquire lock") + ErrUnlockFail = errors.New("failed to release lock") + ErrLockIsNil = errors.New("nil lock") ErrInvalidPath = errors.New("invalid path") ) diff --git a/pkg/internal/filesystem/lock.go b/pkg/internal/filesystem/lock.go new file mode 100644 index 00000000000..82339c20a58 --- /dev/null +++ b/pkg/internal/filesystem/lock.go @@ -0,0 +1,125 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Portions from internal go +// +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:LICENSE + +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/cmd/go/internal/lockedfile/internal/filelock/filelock.go + +package filesystem + +import ( + "errors" + "os" + "runtime" +) + +// Lock places an advisory write lock on the file, blocking until it can be locked. +// +// If Lock returns nil, no other process will be able to place a read or write lock on the file until +// this process exits, closes f, or calls Unlock on it. +func Lock(path string) (file *os.File, err error) { + return commonlock(path, writeLock) +} + +// ReadOnlyLock places an advisory read lock on the file, blocking until it can be locked. +// +// If ReadOnlyLock returns nil, no other process will be able to place a write lock on +// the file until this process exits, closes f, or calls Unlock on it. +func ReadOnlyLock(path string) (file *os.File, err error) { + return commonlock(path, readLock) +} + +func commonlock(path string, mode lockType) (file *os.File, err error) { + defer func() { + if err != nil { + err = errors.Join(ErrLockFail, err, file.Close()) + } + }() + + if runtime.GOOS == "windows" { + // LockFileEx does not work on directories, so check what we have first. + // If that is a dir, swap out the path for a sidecar file instead (not inside the directory). + // Note that this cannot be done in platform specific implementation without moving all the fd Open and Close + // logic over there, which is undesirable. + if sl, err := os.Stat(path); err == nil && sl.IsDir() { + path = path + ".nerdctl.lock" + } + } + + file, err = os.Open(path) + if errors.Is(err, os.ErrNotExist) { + file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, lockPermission) + } + + if err != nil { + return nil, err + } + + if err = platformSpecificLock(file, mode); err != nil { + return nil, errors.Join(err, file.Close()) + } + + return file, nil +} + +// Unlock removes an advisory lock placed on f by this process. +func Unlock(lock *os.File) error { + if lock == nil { + return ErrLockIsNil + } + + if err := errors.Join(platformSpecificUnlock(lock), lock.Close()); err != nil { + return errors.Join(ErrUnlockFail, err) + } + + return nil +} + +// WithLock executes the provided function after placing a write lock on `path`. +// The lock is released once the function has been run, regardless of outcome. +func WithLock(path string, function func() error) (err error) { + file, err := Lock(path) + if err != nil { + return err + } + + defer func() { + err = errors.Join(Unlock(file), err) + }() + + return function() +} + +// WithReadOnlyLock executes the provided function after placing a read lock on `path`. +// The lock is released once the function has been run, regardless of outcome. +func WithReadOnlyLock(path string, function func() error) (err error) { + file, err := ReadOnlyLock(path) + if err != nil { + return err + } + + defer func() { + err = errors.Join(Unlock(file), err) + }() + + return function() +} diff --git a/pkg/internal/filesystem/lock_test.go b/pkg/internal/filesystem/lock_test.go new file mode 100644 index 00000000000..e3405357be1 --- /dev/null +++ b/pkg/internal/filesystem/lock_test.go @@ -0,0 +1,273 @@ +/* + 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 filesystem_test + +import ( + "os" + "sync" + "sync/atomic" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" +) + +const ( + mainroutine1 uint32 = 11 + mainroutine2 uint32 = 12 + routine1 uint32 = 1 + routine2 uint32 = 2 + routine3 uint32 = 3 +) + +func TestLockDir(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + // Lock acquisition + file, err := filesystem.Lock(tempDir) + assert.NilError(t, err, "acquiring a lock should succeed") + err = filesystem.Unlock(file) + assert.NilError(t, err, "releasing a lock should succeed") + + file, err = filesystem.ReadOnlyLock(tempDir) + assert.NilError(t, err, "acquiring a read-only lock should succeed") + file2, err := filesystem.ReadOnlyLock(tempDir) + assert.NilError(t, err, "acquiring another read-only lock should succeed") + err = filesystem.Unlock(file) + assert.NilError(t, err, "releasing a read-only lock should succeed") + err = filesystem.Unlock(file2) + assert.NilError(t, err, "releasing another read-only lock should succeed") +} + +func TestLockFile(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + lock, err := os.CreateTemp(tempDir, "lockfile") + assert.NilError(t, err, "creating temp file should succeed") + defer lock.Close() + // Lock acquisition + file, err := filesystem.Lock(lock.Name()) + assert.NilError(t, err, "acquiring a lock should succeed") + err = filesystem.Unlock(file) + assert.NilError(t, err, "releasing a lock should succeed") + + file, err = filesystem.ReadOnlyLock(lock.Name()) + assert.NilError(t, err, "acquiring a read-only lock should succeed") + file2, err := filesystem.ReadOnlyLock(lock.Name()) + assert.NilError(t, err, "acquiring another read-only lock should succeed") + err = filesystem.Unlock(file) + assert.NilError(t, err, "releasing a read-only lock should succeed") + err = filesystem.Unlock(file2) + assert.NilError(t, err, "releasing another read-only lock should succeed") +} + +func TestLockWriteConcurrent(t *testing.T) { + t.Parallel() + + var waitGroup sync.WaitGroup + + var concurrentKey uint32 + + tempDir := t.TempDir() + + waitGroup.Add(2) + + // Start a lock, set the key, sleep 1s and confirm the key is still the same + go func() { + defer waitGroup.Done() + + lErr := filesystem.WithLock(tempDir, func() error { + atomic.StoreUint32(&concurrentKey, routine1) + + time.Sleep(1 * time.Second) + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine1) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + // Wait 0.5s, start another lock, set the key, sleep 1s and confirm the key is still the same + go func() { + defer waitGroup.Done() + + time.Sleep(500 * time.Millisecond) + + lErr := filesystem.WithLock(tempDir, func() error { + atomic.StoreUint32(&concurrentKey, routine2) + + time.Sleep(1 * time.Second) + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine2) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + // Start a lock, set the key, wait 1s, confirm the key is still the same + lErr := filesystem.WithLock(tempDir, func() error { + atomic.StoreUint32(&concurrentKey, mainroutine1) + + time.Sleep(1 * time.Second) + assert.Equal(t, atomic.LoadUint32(&concurrentKey), mainroutine1) + + return nil + }) + assert.NilError(t, lErr, "locking should not error") + + // Wait 0.75s, start a lock, set the key, sleep 1s, confirm the key is unchanged + time.Sleep(750 * time.Millisecond) + + lErr = filesystem.WithLock(tempDir, func() error { + atomic.StoreUint32(&concurrentKey, mainroutine2) + + time.Sleep(1 * time.Second) + assert.Equal(t, atomic.LoadUint32(&concurrentKey), mainroutine2) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + + waitGroup.Wait() +} + +func TestLockMultiRead(t *testing.T) { + t.Parallel() + + var waitGroup sync.WaitGroup + + var concurrentKey uint32 + + tempDir := t.TempDir() + + waitGroup.Add(3) + + // Start a readonly lock immediately + // Then wait 1s inside the lock - confirm the key got changed by the second read routine + go func() { + t.Log("Entering routine 1") + + defer waitGroup.Done() + + lErr := filesystem.WithReadOnlyLock(tempDir, func() error { + t.Log("Entering routine 1 read lock") + + atomic.StoreUint32(&concurrentKey, routine1) + + time.Sleep(1 * time.Second) + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine2) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + // Wait 0.5s before locking, then change the key + go func() { + t.Log("Entering routine 2") + + defer waitGroup.Done() + + time.Sleep(500 * time.Millisecond) + + lErr := filesystem.WithReadOnlyLock(tempDir, func() error { + t.Log("Entering routine 2 read lock") + + atomic.StoreUint32(&concurrentKey, routine2) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + time.Sleep(50 * time.Millisecond) + // Start a write lock, confirm we have waited for the read locks to finish, change the key + go func() { + t.Log("Entering routine 3") + + defer waitGroup.Done() + + lErr := filesystem.WithLock(tempDir, func() error { + t.Log("Entering routine 3 write lock") + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine2) + + atomic.StoreUint32(&concurrentKey, routine3) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + waitGroup.Wait() +} + +func TestLockWriteBlocksRead(t *testing.T) { + t.Parallel() + + var waitGroup sync.WaitGroup + + var concurrentKey uint32 + + tempDir := t.TempDir() + + waitGroup.Add(2) + + // Start a lock, set the key, sleep 1s and confirm the key is still the same + go func() { + defer waitGroup.Done() + + lErr := filesystem.WithLock(tempDir, func() error { + time.Sleep(1 * time.Second) + + atomic.StoreUint32(&concurrentKey, routine1) + + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine1) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + time.Sleep(50 * time.Millisecond) + + // Start a readonly lock immediately + // Confirm the key has been set by the write lock + go func() { + defer waitGroup.Done() + + lErr := filesystem.WithReadOnlyLock(tempDir, func() error { + assert.Equal(t, atomic.LoadUint32(&concurrentKey), routine1) + + return nil + }) + + assert.NilError(t, lErr, "locking should not error") + }() + + waitGroup.Wait() +} diff --git a/pkg/internal/filesystem/lock_unix.go b/pkg/internal/filesystem/lock_unix.go new file mode 100644 index 00000000000..4f37a2fa368 --- /dev/null +++ b/pkg/internal/filesystem/lock_unix.go @@ -0,0 +1,59 @@ +//go:build unix + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Portions from internal go +// +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:LICENSE + +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/cmd/go/internal/lockedfile/internal/filelock/filelock_unix.go + +package filesystem + +import ( + "errors" + "os" + "syscall" +) + +type lockType int16 + +const ( + readLock lockType = syscall.LOCK_SH + writeLock lockType = syscall.LOCK_EX +) + +func platformSpecificLock(file *os.File, lockType lockType) error { + var err error + + for { + err = syscall.Flock(int(file.Fd()), int(lockType)) + if !errors.Is(err, syscall.EINTR) { + break + } + } + + return err +} + +func platformSpecificUnlock(file *os.File) error { + return syscall.Flock(int(file.Fd()), syscall.LOCK_UN) +} diff --git a/pkg/internal/filesystem/lock_windows.go b/pkg/internal/filesystem/lock_windows.go new file mode 100644 index 00000000000..b81d71d0ff3 --- /dev/null +++ b/pkg/internal/filesystem/lock_windows.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Portions from internal go +// +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:LICENSE + +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/cmd/go/internal/lockedfile/internal/filelock/filelock_windows.go + +package filesystem + +import ( + "os" + + "golang.org/x/sys/windows" +) + +type lockType uint32 + +const ( + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx + readLock lockType = 0 + writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK + + reserved = 0 + allBytes = ^uint32(0) +) + +func platformSpecificLock(file *os.File, lockType lockType) error { + return windows.LockFileEx( + windows.Handle(file.Fd()), + uint32(lockType), + reserved, + allBytes, + allBytes, + new(windows.Overlapped)) +} + +func platformSpecificUnlock(file *os.File) error { + return windows.UnlockFileEx(windows.Handle(file.Fd()), reserved, allBytes, allBytes, new(windows.Overlapped)) +} diff --git a/pkg/internal/filesystem/lockutil_unix.go b/pkg/internal/filesystem/lockutil_unix.go deleted file mode 100644 index 8d50b15b071..00000000000 --- a/pkg/internal/filesystem/lockutil_unix.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build unix - -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package filesystem - -import ( - "fmt" - "os" - - "golang.org/x/sys/unix" - - "github.com/containerd/log" -) - -func WithDirLock(dir string, fn func() error) error { - _ = os.MkdirAll(dir, 0700) - dirFile, err := os.Open(dir) - if err != nil { - return err - } - defer dirFile.Close() - if err := flock(dirFile, unix.LOCK_EX); err != nil { - return fmt.Errorf("failed to lock %q: %w", dir, err) - } - defer func() { - if err := flock(dirFile, unix.LOCK_UN); err != nil { - log.L.WithError(err).Errorf("failed to unlock %q", dir) - } - }() - return fn() -} - -func flock(f *os.File, flags int) error { - fd := int(f.Fd()) - for { - err := unix.Flock(fd, flags) - if err == nil || err != unix.EINTR { - return err - } - } -} - -func Lock(dir string) (*os.File, error) { - _ = os.MkdirAll(dir, 0700) - dirFile, err := os.Open(dir) - if err != nil { - return nil, err - } - - if err = flock(dirFile, unix.LOCK_EX); err != nil { - return nil, err - } - - return dirFile, nil -} - -func Unlock(locked *os.File) error { - defer func() { - _ = locked.Close() - }() - - return flock(locked, unix.LOCK_UN) -} diff --git a/pkg/internal/filesystem/lockutil_windows.go b/pkg/internal/filesystem/lockutil_windows.go deleted file mode 100644 index 8dc6e3eb47d..00000000000 --- a/pkg/internal/filesystem/lockutil_windows.go +++ /dev/null @@ -1,65 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package filesystem - -import ( - "fmt" - "os" - - "golang.org/x/sys/windows" - - "github.com/containerd/log" -) - -func WithDirLock(dir string, fn func() error) error { - dirFile, err := os.OpenFile(dir+".lock", os.O_CREATE, 0644) - if err != nil { - return err - } - defer dirFile.Close() - // see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx - if err = windows.LockFileEx(windows.Handle(dirFile.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil { - return fmt.Errorf("failed to lock %q: %w", dir, err) - } - - defer func() { - if err := windows.UnlockFileEx(windows.Handle(dirFile.Fd()), 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil { - log.L.WithError(err).Errorf("failed to unlock %q", dir) - } - }() - return fn() -} - -func Lock(dir string) (*os.File, error) { - dirFile, err := os.OpenFile(dir+".lock", os.O_CREATE, 0644) - if err != nil { - return nil, err - } - // see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx - if err = windows.LockFileEx(windows.Handle(dirFile.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil { - return nil, fmt.Errorf("failed to lock %q: %w", dir, err) - } - return dirFile, nil -} - -func Unlock(locked *os.File) error { - defer func() { - _ = locked.Close() - }() - - return windows.UnlockFileEx(windows.Handle(locked.Fd()), 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)) -} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 65cc1ddd31e..e60b28c23c7 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -160,7 +160,7 @@ func getLockPath(dataStore, ns, id string) string { // WaitForLogger waits until the logger has finished executing and processing container logs func WaitForLogger(dataStore, ns, id string) error { - return filesystem.WithDirLock(getLockPath(dataStore, ns, id), func() error { + return filesystem.WithLock(getLockPath(dataStore, ns, id), func() error { return nil }) } @@ -305,16 +305,11 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) { } loggerLock := getLockPath(dataStore, config.Namespace, config.ID) - f, err := os.Create(loggerLock) - if err != nil { - return err - } - defer f.Close() // the logger will obtain an exclusive lock on a file until the container is // stopped and the driver has finished processing all output, // so that waiting log viewers can be signalled when the process is complete. - return filesystem.WithDirLock(loggerLock, func() error { + return filesystem.WithLock(loggerLock, func() error { if err := ready(); err != nil { return err } diff --git a/pkg/netutil/store.go b/pkg/netutil/store.go index 627468e8f85..c1fbe3744bd 100644 --- a/pkg/netutil/store.go +++ b/pkg/netutil/store.go @@ -48,7 +48,7 @@ func fsRemove(e *CNIEnv, net *NetworkConfig) error { } return net.clean() } - return filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) + return filesystem.WithLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), fn) } func fsExists(e *CNIEnv, name string) (bool, error) { @@ -62,7 +62,7 @@ func fsWrite(e *CNIEnv, net *NetworkConfig) error { // Concurrent access may independently first figure out that a given network is missing, and while the lock // here will prevent concurrent writes, one of the routines will fail. // Consuming code MUST account for that scenario. - return filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + return filesystem.WithLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { if _, err := os.Stat(filename); err == nil { return errdefs.ErrAlreadyExists } @@ -73,7 +73,7 @@ func fsWrite(e *CNIEnv, net *NetworkConfig) error { func fsRead(e *CNIEnv) ([]*NetworkConfig, error) { var nc []*NetworkConfig var err error - err = filesystem.WithDirLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { + err = filesystem.WithReadOnlyLock(filepath.Join(e.NetconfPath, ".nerdctl.lock"), func() error { namespaced := []string{} var common []string common, err = libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) From 2c26839d137d5715dfed4aab7cf85f2577cde60c Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 6 May 2025 14:20:46 -0700 Subject: [PATCH 028/868] Rewrite some compose tests Signed-off-by: apostasie --- .../compose/compose_build_linux_test.go | 2 - .../compose/compose_create_linux_test.go | 6 - .../compose/compose_images_linux_test.go | 163 ++++++++---------- .../compose/compose_pull_linux_test.go | 4 - .../compose/compose_restart_linux_test.go | 4 - cmd/nerdctl/compose/compose_rm_linux_test.go | 101 +++++++---- .../compose/compose_start_linux_test.go | 88 ++++++---- .../compose/compose_stop_linux_test.go | 73 +++++--- cmd/nerdctl/compose/compose_top_linux_test.go | 53 ++++-- cmd/nerdctl/compose/compose_up_test.go | 84 +++++---- cmd/nerdctl/compose/compose_version_test.go | 23 ++- 11 files changed, 352 insertions(+), 249 deletions(-) diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index 2c967a331e2..d0092f0a9df 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -46,8 +46,6 @@ services: svc0: build: . image: %s - ports: - - 8080:80 depends_on: - svc1 svc1: diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index c1a94dfd2c6..58e92514b82 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -32,8 +32,6 @@ import ( func TestComposeCreate(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -87,8 +85,6 @@ services: func TestComposeCreateDependency(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -152,8 +148,6 @@ func TestComposeCreatePull(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_images_linux_test.go b/cmd/nerdctl/compose/compose_images_linux_test.go index f9f7f475186..ae4d9f8eb16 100644 --- a/cmd/nerdctl/compose/compose_images_linux_test.go +++ b/cmd/nerdctl/compose/compose_images_linux_test.go @@ -17,24 +17,26 @@ package compose import ( - "encoding/json" "fmt" - "strings" "testing" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeImages(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 + container_name: wordpress environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -44,6 +46,7 @@ services: - wordpress:/var/www/html db: image: %s + container_name: db environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser @@ -57,95 +60,71 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - wordpressImageName := strings.Split(testutil.WordpressImage, ":")[0] - dbImageName := strings.Split(testutil.MariaDBImage, ":")[0] - - // check one service image - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "db").AssertOutContains(dbImageName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "db").AssertOutNotContains(wordpressImageName) - - // check all service images - base.ComposeCmd("-f", comp.YAMLFullPath(), "images").AssertOutContains(dbImageName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images").AssertOutContains(wordpressImageName) -} + wordpressImageName, _ := referenceutil.Parse(testutil.WordpressImage) + dbImageName, _ := referenceutil.Parse(testutil.MariaDBImage) -func TestComposeImagesJson(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' + testCase := nerdtest.Setup() -services: - wordpress: - image: %s - container_name: wordpress - ports: - - 8080:80 - environment: - WORDPRESS_DB_HOST: db - WORDPRESS_DB_USER: exampleuser - WORDPRESS_DB_PASSWORD: examplepass - WORDPRESS_DB_NAME: exampledb - volumes: - - wordpress:/var/www/html - db: - image: %s - container_name: db - environment: - MYSQL_DATABASE: exampledb - MYSQL_USER: exampleuser - MYSQL_PASSWORD: examplepass - MYSQL_RANDOM_ROOT_PASSWORD: '1' - volumes: - - db:/var/lib/mysql - -volumes: - wordpress: - db: -`, testutil.WordpressImage, testutil.MariaDBImage) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + } - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - assertHandler := func(svc string, count int, fields ...string) func(stdout string) error { - return func(stdout string) error { - // 1. check json output can be unmarshalled back to printables. - var printables []composeContainerPrintable - if err := json.Unmarshal([]byte(stdout), &printables); err != nil { - return fmt.Errorf("[service: %s]failed to unmarshal json output from `compose images`: %s", svc, stdout) - } - // 2. check #printables matches expected count. - if len(printables) != count { - return fmt.Errorf("[service: %s]unmarshal generates %d printables, expected %d: %s", svc, len(printables), count, stdout) - } - // 3. check marshalled json string has all expected substrings. - for _, field := range fields { - if !strings.Contains(stdout, field) { - return fmt.Errorf("[service: %s]marshalled json output doesn't have expected string (%s): %s", svc, field, stdout) - } - } - return nil - } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } - // check other formats are not supported - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "yaml").AssertFail() - // check all services are up (can be marshalled and unmarshalled) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "json"). - AssertOutWithFunc(assertHandler("all", 2, `"ContainerName":"wordpress"`, `"ContainerName":"db"`)) + testCase.SubTests = []*test.Case{ + { + Description: "images db", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "db") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains(dbImageName.Name()), + expect.DoesNotContain(wordpressImageName.Name()), + )), + }, + { + Description: "images", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(dbImageName.Name(), wordpressImageName.Name())), + }, + { + Description: "images --format yaml", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "yaml") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "images --format json", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + assert.Equal(t, len(printables), 2) + }), + expect.Contains(`"ContainerName":"wordpress"`, `"ContainerName":"db"`), + )), + }, + { + Description: "images --format json wordpress", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json", "wordpress") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + assert.Equal(t, len(printables), 1) + }), + expect.Contains(`"ContainerName":"wordpress"`), + )), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "json", "wordpress"). - AssertOutWithFunc(assertHandler("wordpress", 1, `"ContainerName":"wordpress"`)) + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_pull_linux_test.go b/cmd/nerdctl/compose/compose_pull_linux_test.go index 64e267baa24..e0c79325326 100644 --- a/cmd/nerdctl/compose/compose_pull_linux_test.go +++ b/cmd/nerdctl/compose/compose_pull_linux_test.go @@ -26,14 +26,10 @@ import ( func TestComposePullWithService(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_restart_linux_test.go b/cmd/nerdctl/compose/compose_restart_linux_test.go index 6d5fe1fdedc..1e2ca2c5c61 100644 --- a/cmd/nerdctl/compose/compose_restart_linux_test.go +++ b/cmd/nerdctl/compose/compose_restart_linux_test.go @@ -26,13 +26,9 @@ import ( func TestComposeRestart(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_rm_linux_test.go b/cmd/nerdctl/compose/compose_rm_linux_test.go index 948ea9e119d..58d149693a9 100644 --- a/cmd/nerdctl/compose/compose_rm_linux_test.go +++ b/cmd/nerdctl/compose/compose_rm_linux_test.go @@ -18,23 +18,22 @@ package compose import ( "fmt" + "regexp" "testing" - "time" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeRemove(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -58,27 +57,71 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - // no stopped containers - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") - // remove one stopped service - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutNotContains("wordpress") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") - // remove all services with `--stop` - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "-s").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutNotContains("db") + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "All services are still up", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + comp := expect.Match(regexp.MustCompile("Up|running")) + comp(wp, "", t) + comp(db, "", t) + }, + } + }, + }, + { + Description: "Remove stopped service", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "wordpress") + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + expect.DoesNotContain("wordpress")(wp, "", t) + expect.Match(regexp.MustCompile("Up|running"))(db, "", t) + }, + } + }, + }, + { + Description: "Remove all services with stop", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f", "-s") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + expect.DoesNotContain("db")(db, "", t) + }, + } + }, + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go index 11c1581cd92..bfb001ad5c9 100644 --- a/cmd/nerdctl/compose/compose_start_linux_test.go +++ b/cmd/nerdctl/compose/compose_start_linux_test.go @@ -18,16 +18,18 @@ package compose import ( "fmt" + "regexp" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStart(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -37,50 +39,68 @@ services: command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } - // calling `compose start` after all services up has no effect. - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "start") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "stop", "--timeout", "1", "svc0") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "kill", "svc1") + } - // `compose start`` can start a stopped/killed service container - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "--timeout", "1", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "svc1").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Up", "running") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Up", "running") -} + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") + } -func TestComposeStartFailWhenServicePause(t *testing.T) { - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: nil, + Output: func(stdout, info string, t *testing.T) { + svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") + svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") + comp := expect.Match(regexp.MustCompile("Up|running")) + comp(svc0, "", t) + comp(svc1, "", t) + }, + } } - var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' + testCase.Run(t) +} +func TestComposeStartFailWhenServicePause(t *testing.T) { + var dockerComposeYAML = fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.CGroup + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "pause", "svc0") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) - // `compose start` cannot start a paused service container - base.ComposeCmd("-f", comp.YAMLFullPath(), "pause", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertFail() + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_stop_linux_test.go b/cmd/nerdctl/compose/compose_stop_linux_test.go index e10b16ff7b2..ac346b90507 100644 --- a/cmd/nerdctl/compose/compose_stop_linux_test.go +++ b/cmd/nerdctl/compose/compose_stop_linux_test.go @@ -18,22 +18,22 @@ package compose import ( "fmt" + "regexp" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStop(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -57,21 +57,50 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - // stop should (only) stop the given service. - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") - - // `--timeout` arg should work properly. - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "--timeout", "5", "wordpress").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress", "-a").AssertOutContainsAny("Exit", "exited") - + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.SubTests = []*test.Case{ + { + Description: "stop db", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "db") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), + }, + { + Description: "wordpress is still running", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Up|running"))), + }, + { + Description: "stop wordpress", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "--timeout", "5", "wordpress") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_top_linux_test.go b/cmd/nerdctl/compose/compose_top_linux_test.go index a0474c51b0b..9620aa113c1 100644 --- a/cmd/nerdctl/compose/compose_top_linux_test.go +++ b/cmd/nerdctl/compose/compose_top_linux_test.go @@ -20,20 +20,16 @@ import ( "fmt" "testing" - "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeTop(t *testing.T) { - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -42,15 +38,36 @@ services: image: %s `, testutil.CommonImage, testutil.NginxAlpineImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() + + testCase.Require = require.All(nerdtest.CgroupsAccessible) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.SubTests = []*test.Case{ + { + Description: "svc0 contains sleep infinity", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc0") + }, + Expected: test.Expects(0, nil, expect.Contains("sleep infinity")), + }, + { + Description: "svc1 contains sleep nginx", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc1") + }, + Expected: test.Expects(0, nil, expect.Contains("nginx")), + }, + } - // a running container should have the process command in output - base.ComposeCmd("-f", comp.YAMLFullPath(), "top", "svc0").AssertOutContains("sleep infinity") - base.ComposeCmd("-f", comp.YAMLFullPath(), "top", "svc1").AssertOutContains("nginx") + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go index eb17c7151c9..48f1b5c688f 100644 --- a/cmd/nerdctl/compose/compose_up_test.go +++ b/cmd/nerdctl/compose/compose_up_test.go @@ -17,66 +17,88 @@ package compose import ( + "errors" "fmt" - "os" - "path/filepath" - "runtime" "testing" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" + + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/1942 func TestComposeUpDetailedError(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("FIXME: test does not work on Windows yet (runtime \"io.containerd.runc.v2\" binary not installed \"containerd-shim-runc-v2.exe\": file does not exist)") - } - base := testutil.NewBase(t) dockerComposeYAML := fmt.Sprintf(` services: foo: image: %s runtime: invalid `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d") - expected := icmd.Expected{ - ExitCode: 1, - Err: `invalid runtime name`, + testCase := nerdtest.Setup() + + // "FIXME: test does not work on Windows yet (runtime \"io.containerd.runc.v2\" binary not installed \"containerd-shim-runc-v2.exe\": file does not exist) + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") } - c.Assert(expected) + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + } + + testCase.Expected = test.Expects( + 1, + []error{errors.New(`invalid runtime name`)}, + nil, + ) + + testCase.Run(t) } // https://github.com/containerd/nerdctl/issues/1652 func TestComposeUpBindCreateHostPath(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip(`FIXME: no support for Windows path: (error: "volume target must be an absolute path, got \"/mnt\")`) - } + testCase := nerdtest.Setup() - base := testutil.NewBase(t) + // `FIXME: no support for Windows path: (error: "volume target must be an absolute path, got \"/mnt\")` + testCase.Require = require.Not(require.Windows) - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var dockerComposeYAML = fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi >/mnt/test" volumes: - # ./foo should be automatically created - - ./foo:/mnt -`, testutil.CommonImage) + # tempdir/foo should be automatically created + - %s:/mnt +`, testutil.CommonImage, data.Temp().Path("foo")) + + data.Temp().Save(dockerComposeYAML, "compose.yaml") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: nil, + Output: func(stdout, info string, t *testing.T) { + assert.Equal(t, data.Temp().Load("foo", "test"), "hi\n") + }, + } + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() - testFile := filepath.Join(comp.Dir(), "foo", "test") - testB, err := os.ReadFile(testFile) - assert.NilError(t, err) - assert.Equal(t, "hi\n", string(testB)) + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_version_test.go b/cmd/nerdctl/compose/compose_version_test.go index af3028b3d65..04cdd244052 100644 --- a/cmd/nerdctl/compose/compose_version_test.go +++ b/cmd/nerdctl/compose/compose_version_test.go @@ -19,20 +19,29 @@ package compose import ( "testing" - "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeVersion(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version").AssertOutContains("Compose version ") + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version") + testCase.Expected = test.Expects(0, nil, expect.Contains("Compose version ")) + testCase.Run(t) } func TestComposeVersionShort(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version", "--short").AssertOK() + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version", "--short") + testCase.Expected = test.Expects(0, nil, nil) + testCase.Run(t) } func TestComposeVersionJson(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version", "--format", "json").AssertOutContains("{\"version\":\"") + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version", "--format", "json") + testCase.Expected = test.Expects(0, nil, expect.Contains("{\"version\":\"")) + testCase.Run(t) } From a91b1bb35bd26877873702b2315b08b3a2a0050e Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 7 May 2025 08:53:37 -0700 Subject: [PATCH 029/868] Reduce RunSigProxyContainer refresh frequency Signed-off-by: apostasie --- pkg/testutil/nerdtest/utilities_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/testutil/nerdtest/utilities_linux.go b/pkg/testutil/nerdtest/utilities_linux.go index bc652564f83..017dd1adb00 100644 --- a/pkg/testutil/nerdtest/utilities_linux.go +++ b/pkg/testutil/nerdtest/utilities_linux.go @@ -68,7 +68,7 @@ func RunSigProxyContainer(signal os.Signal, exitOnSignal bool, args []string, da if strings.Contains(out, ready) { break } - time.Sleep(100 * time.Millisecond) + time.Sleep(1 * time.Second) } return cmd From 8bd7a7dc998c2dcc5960baf7b58be1fcdce9f663 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 7 May 2025 08:54:02 -0700 Subject: [PATCH 030/868] Rewrite logs tests and fix flakyness Signed-off-by: apostasie --- cmd/nerdctl/container/container_logs_test.go | 486 ++++++++++++------- 1 file changed, 304 insertions(+), 182 deletions(-) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index d6011260e4e..632ce955949 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -19,8 +19,7 @@ package container import ( "errors" "fmt" - "io" - "os/exec" + "regexp" "runtime" "strconv" "strings" @@ -28,7 +27,6 @@ import ( "time" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" @@ -39,41 +37,86 @@ import ( ) func TestLogs(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) const expected = `foo -bar` +bar +` - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - - //test since / until flag - time.Sleep(3 * time.Second) - base.Cmd("logs", "--since", "1s", containerName).AssertOutNotContains(expected) - base.Cmd("logs", "--since", "10s", containerName).AssertOutContains(expected) - base.Cmd("logs", "--until", "10s", containerName).AssertOutNotContains(expected) - base.Cmd("logs", "--until", "1s", containerName).AssertOutContains(expected) + testCase := nerdtest.Setup() - // Ensure follow flag works as expected: - base.Cmd("logs", "-f", containerName).AssertOutContains("bar") - base.Cmd("logs", "-f", containerName).AssertOutContains("foo") + if runtime.GOOS == "windows" { + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } - //test timestamps flag - base.Cmd("logs", "-t", containerName).AssertOutContains(time.Now().UTC().Format("2006-01-02")) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - //test tail flag - base.Cmd("logs", "-n", "all", containerName).AssertOutContains(expected) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--quiet", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar;") + data.Labels().Set("cID", data.Identifier()) + } - base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error { - if !(stdout == "bar\n" || stdout == "") { - return fmt.Errorf("expected %q or %q, got %q", "bar", "", stdout) - } - return nil - }) + testCase.SubTests = []*test.Case{ + { + Description: "since 1s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "1s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), + }, + { + Description: "since 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "until 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--until", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), + }, + { + Description: "until 1s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--until", "1s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "follow", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "timestamp", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-t", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Contains(time.Now().UTC().Format("2006-01-02"))), + }, + { + Description: "tail flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-n", "all", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "tail flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-n", "1", data.Labels().Get("cID")) + }, + // FIXME: why? + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))), + }, + } - base.Cmd("rm", "-f", containerName).AssertOK() + testCase.Run(t) } // Tests whether `nerdctl logs` properly separates stdout/stderr output @@ -81,8 +124,13 @@ bar` func TestLogsOutStreamsSeparated(t *testing.T) { testCase := nerdtest.Setup() + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "echo stdout1; echo stderr1 >&2; echo stdout2; echo stderr2 >&2") } @@ -91,8 +139,6 @@ func TestLogsOutStreamsSeparated(t *testing.T) { } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - // Arbitrary, but we need to wait until the logs show up - time.Sleep(3 * time.Second) return helpers.Command("logs", data.Identifier()) } @@ -105,116 +151,165 @@ func TestLogsOutStreamsSeparated(t *testing.T) { } func TestLogsWithInheritedFlags(t *testing.T) { - // Seen flaky with Docker - t.Parallel() - base := testutil.NewBase(t) - for k, v := range base.Args { - if strings.HasPrefix(v, "--namespace=") { - base.Args[k] = "-n=" + testutil.Namespace - } + testCase := nerdtest.Setup() + + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("-n="+testutil.Namespace, "run", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") } - containerName := testutil.Identifier(t) - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - // It appears this test flakes out with Docker seeing only "foo\n" - // Tentatively adding a pause in case this is just slow - time.Sleep(time.Second) - // test rootCmd alias `-n` already used in logs subcommand - base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error { - if !(stdout == "bar\n" || stdout == "") { - return fmt.Errorf("expected %q or %q, got %q", "bar", "", stdout) - } - return nil - }) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("-n="+testutil.Namespace, "logs", "-n", "1", data.Identifier()) + } + + // FIXME: why? + testCase.Expected = test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))) + + testCase.Run(t) } func TestLogsOfJournaldDriver(t *testing.T) { - testutil.RequireExecutable(t, "journalctl") - journalctl, _ := exec.LookPath("journalctl") - res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) - if res.ExitCode != 0 { - t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) - } + const expected = `foo +bar +` - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + testCase := nerdtest.Setup() - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--network", "none", "--log-driver", "journald", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() + testCase.Require = require.All( + require.Binary("journalctl"), + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + works := false + cmd := helpers.Custom("journalctl", "-xe") + cmd.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout, info string, t *testing.T) { + if stdout != "" { + works = true + } + }, + }) + return works, "Journactl to return data for the current user" + }, + }, + ) - time.Sleep(3 * time.Second) - base.Cmd("logs", containerName).AssertOutContains("bar") - // Run logs twice, make sure that the logs are not removed - base.Cmd("logs", containerName).AssertOutContains("foo") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - base.Cmd("logs", "--since", "5s", containerName).AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "bar") { - return fmt.Errorf("expected bar, got %s", stdout) - } - if !strings.Contains(stdout, "foo") { - return fmt.Errorf("expected foo, got %s", stdout) - } - return nil - }) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--network", "none", "--log-driver", "journald", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + data.Labels().Set("cID", data.Identifier()) + } - base.Cmd("rm", "-f", containerName).AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "logs", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Labels().Get("cID")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(expected)), + }, + { + Description: "logs --since 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("foo", "bar")), + }, + } } func TestLogsWithFailingContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar; exit 42; echo baz").AssertOK() - time.Sleep(3 * time.Second) - // AssertOutContains also asserts that the exit code of the logs command == 0, - // even when the container is failing - base.Cmd("logs", "-f", containerName).AssertOutContains("bar") - base.Cmd("logs", "-f", containerName).AssertOutNotContains("baz") - base.Cmd("rm", "-f", containerName).AssertOK() + const expected = `foo +bar +` + + testCase := nerdtest.Setup() + + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar; exit 42; echo baz") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Equals(expected)) + + testCase.Run(t) } func TestLogsWithRunningContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).Run() expected := make([]string, 10) for i := 0; i < 10; i++ { expected[i] = fmt.Sprint(i + 1) } - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done").AssertOK() - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) + testCase := nerdtest.Setup() + + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains(expected[0], expected[1:]...)) + + testCase.Run(t) } func TestLogsWithoutNewlineOrEOF(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: test does not work on Windows yet because containerd doesn't send an exit event appropriately after task exit on Windows") // FIXME: nerdctl behavior does not match docker - test disabled for nerdctl until we fix testCase.Require = require.All( require.Linux, - nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4201"), ) + testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "printf", "'Hello World!\nThere is no newline'") + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "printf", "'Hello World!\nThere is no newline'") } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - // FIXME: arbitrary timeouts are by nature a problem. - time.Sleep(5 * time.Second) return helpers.Command("logs", "-f", data.Identifier()) } + testCase.Expected = test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")) + testCase.Run(t) } @@ -222,19 +317,44 @@ func TestLogsAfterRestartingContainer(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("FIXME: test does not work on Windows yet. Restarting a container fails with: failed to create shim task: hcs::CreateComputeSystem : The requested operation for attach namespace failed.: unknown") } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "printf", "'Hello World!\nThere is no newline'").AssertOK() - expected := []string{"Hello World!", "There is no newline"} - time.Sleep(3 * time.Second) - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) - // restart and check logs again - base.Cmd("start", containerName) - time.Sleep(3 * time.Second) - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, + "printf", "'Hello World!\nThere is no newline'") + data.Labels().Set("cID", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.SubTests = []*test.Case{ + { + Description: "logs -f works", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")), + }, + { + Description: "logs -f works after restart", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("start", data.Labels().Get("cID")) + // FIXME: this is inherently flaky + time.Sleep(5 * time.Second) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline''Hello World!\nThere is no newline'")), + }, + } + + testCase.Run(t) } func TestLogsWithForegroundContainers(t *testing.T) { @@ -256,10 +376,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo", "bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactive", @@ -272,10 +389,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo", "bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "PTY", @@ -290,10 +404,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo", "bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactivePTY", @@ -308,69 +419,88 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo", "bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, } } -func TestTailFollowRotateLogs(t *testing.T) { - // FIXME this is flaky by nature... 2 lines is arbitrary, 10000 ms is arbitrary, and both are some sort of educated - // guess that things will mostly always kinda work maybe... - // Furthermore, parallelizing will put pressure on the daemon which might be even slower in answering, increasing - // the risk of transient failure. - // This test needs to be rethought entirely - // t.Parallel() - if runtime.GOOS == "windows" { - t.Skip("tail log is not supported on Windows") - } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - +func TestLogsTailFollowRotate(t *testing.T) { + // FIXME this is flaky by nature... the number of lines is arbitrary, the wait is arbitrary, + // and both are some sort of educated guess that things will mostly always kinda work maybe... const sampleJSONLog = `{"log":"A\n","stream":"stdout","time":"2024-04-11T12:01:09.800288974Z"}` const linesPerFile = 200 - defer base.Cmd("rm", "-f", containerName).Run() - base.Cmd("run", "-d", "--log-driver", "json-file", - "--log-opt", fmt.Sprintf("max-size=%d", len(sampleJSONLog)*linesPerFile), - "--log-opt", "max-file=10", - "--name", containerName, testutil.CommonImage, - "sh", "-euc", "while true; do echo A; usleep 100; done").AssertOK() - - tailLogCmd := base.Cmd("logs", "-f", containerName) - tailLogCmd.Timeout = 1000 * time.Millisecond - logRun := tailLogCmd.Run() - tailLogs := strings.Split(strings.TrimSpace(logRun.Stdout()), "\n") - for _, line := range tailLogs { - if line != "" { - assert.Equal(t, "A", line) - } + testCase := nerdtest.Setup() + + // tail log is not supported on Windows + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--log-driver", "json-file", + "--log-opt", fmt.Sprintf("max-size=%d", len(sampleJSONLog)*linesPerFile), + "--log-opt", "max-file=10", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euc", "while true; do echo A; usleep 100; done") + // FIXME: ... inherently racy... + time.Sleep(5 * time.Second) } - assert.Equal(t, true, len(tailLogs) > linesPerFile, logRun.Stderr()) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("logs", "-f", data.Identifier()) + // FIXME: this is flaky by nature. We assume that the container has started and will output enough in 5 seconds. + cmd.WithTimeout(5 * time.Second) + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout, info string, t *testing.T) { + tailLogs := strings.Split(strings.TrimSpace(stdout), "\n") + for _, line := range tailLogs { + if line != "" { + assert.Equal(t, "A", line) + } + } + + assert.Assert(t, len(tailLogs) > linesPerFile, fmt.Sprintf("expected %d lines or more, found %d", linesPerFile, len(tailLogs))) + }) + + testCase.Run(t) } -func TestNoneLoggerHasNoLogURI(t *testing.T) { + +func TestLogsNoneLoggerHasNoLogURI(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), "--log-driver", "none", testutil.CommonImage, "sh", "-euxc", "echo foo") } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) } + testCase.Expected = test.Expects(1, nil, nil) + testCase.Run(t) } func TestLogsWithDetails(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: this is not working on windows. There is some deep issue with windows logs: + // https://github.com/containerd/nerdctl/issues/4237 + if runtime.GOOS == "windows" { + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--log-driver", "json-file", + helpers.Ensure("run", "--log-driver", "json-file", "--log-opt", "max-size=10m", "--log-opt", "max-file=3", "--log-opt", "env=ENV", @@ -401,7 +531,7 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { // Create a container that outputs a message without a trailing newline - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-c", "printf 'Hello without newline'") } @@ -411,8 +541,6 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Use logs -f to follow the logs - // Arbitrary, but we need to wait until the logs show up - time.Sleep(3 * time.Second) return helpers.Command("logs", "-f", data.Identifier()) } @@ -425,7 +553,7 @@ func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { func TestLogsWithStartContainer(t *testing.T) { testCase := nerdtest.Setup() - // For windows we havent added support for dual logging so not adding the test. + // Windows does not support dual logging. testCase.Require = require.Not(require.Windows) testCase.SubTests = []*test.Case{ @@ -434,34 +562,28 @@ func TestLogsWithStartContainer(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) cmd.WithPseudoTTY() - cmd.WithFeeder(func() io.Reader { - return strings.NewReader("echo foo\nexit\n") + cmd.Feed(strings.NewReader("echo foo\nexit\n")) + cmd.Run(&test.Expected{ + ExitCode: 0, }) + cmd = helpers.Command("start", "-ia", data.Identifier()) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo bar\nexit\n")) cmd.Run(&test.Expected{ ExitCode: 0, }) - }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("start", "-ia", data.Identifier()) - cmd.WithPseudoTTY() - cmd.WithFeeder(func() io.Reader { - return strings.NewReader("echo bar\nexit\n") - }) - cmd.Run(&test.Expected{ - ExitCode: 0, - }) - cmd = helpers.Command("logs", data.Identifier()) - - return cmd + return helpers.Command("logs", data.Identifier()) }, Expected: test.Expects(0, nil, expect.Contains("foo", "bar")), }, { + // FIXME: is this test safe or could it be racy? Description: "Test logs are captured after stopping and starting a non-interactive container and continue capturing new logs", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sh", "-c", "while true; do echo foo; sleep 1; done") From 0365d394416da17aaed3bf0e853583dad2cb08db Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 18 May 2025 08:17:37 +0000 Subject: [PATCH 031/868] test: refactor TestTestParseFlagPWithPlatformSpec and TestParsePortsLabel Signed-off-by: Hayato Kiwata --- pkg/portutil/portutil_test.go | 100 +++++++++++++--------------------- 1 file changed, 37 insertions(+), 63 deletions(-) diff --git a/pkg/portutil/portutil_test.go b/pkg/portutil/portutil_test.go index 8b79deb192d..55930e07f3b 100644 --- a/pkg/portutil/portutil_test.go +++ b/pkg/portutil/portutil_test.go @@ -22,6 +22,8 @@ import ( "sort" "testing" + "gotest.tools/v3/assert" + "github.com/containerd/go-cni" "github.com/containerd/nerdctl/v2/pkg/labels" @@ -124,45 +126,30 @@ func TestTestParseFlagPWithPlatformSpec(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseFlagP(tt.args.s) - t.Log(err) - if (err != nil) != tt.wantErr { - t.Errorf("ParseFlagP() error = %v, wantErr %v", err, tt.wantErr) - return + if err != nil { + t.Log(err) + assert.Equal(t, true, tt.wantErr) } if !reflect.DeepEqual(got, tt.want) { - if len(got) == len(tt.want) { - if len(got) > 0 { - var hostPorts []int32 - var containerPorts []int32 - for _, value := range got { - hostPorts = append(hostPorts, value.HostPort) - containerPorts = append(containerPorts, value.ContainerPort) - } - sort.Slice(hostPorts, func(i, j int) bool { - return i < j - }) - sort.Slice(containerPorts, func(i, j int) bool { - return i < j - }) - if (hostPorts[len(hostPorts)-1] - hostPorts[0]) != (containerPorts[len(hostPorts)-1] - containerPorts[0]) { - t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) - } - sort.Slice(got, func(i, j int) bool { - return got[i].HostPort < got[j].HostPort - }) - for i := 0; i < len(got); i++ { - if got[i].ContainerPort != tt.want[i].ContainerPort || got[i].Protocol != tt.want[i].Protocol || got[i].HostIP != tt.want[i].HostIP { - t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) - } - } + assert.Equal(t, len(got), len(tt.want)) + if len(got) > 0 { + sort.Slice(got, func(i, j int) bool { + return got[i].HostPort < got[j].HostPort + }) + assert.Equal( + t, + got[len(got)-1].HostPort-got[0].HostPort, + got[len(got)-1].ContainerPort-got[0].ContainerPort, + ) + for i := range len(got) { + assert.Equal(t, got[i].ContainerPort, tt.want[i].ContainerPort) + assert.Equal(t, got[i].Protocol, tt.want[i].Protocol) + assert.Equal(t, got[i].HostIP, tt.want[i].HostIP) } - } else { - t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) } } }) } - } func TestParsePortsLabel(t *testing.T) { @@ -213,40 +200,27 @@ func TestParsePortsLabel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParsePortsLabel(tt.labelMap) - t.Log(err) - if (err != nil) != tt.wantErr { - t.Errorf("ParsePortsLabel() error = %v, wantErr %v", err, tt.wantErr) - return + if err != nil { + t.Log(err) + assert.Equal(t, true, tt.wantErr) } if !reflect.DeepEqual(got, tt.want) { - if len(got) == len(tt.want) { - if len(got) > 0 { - var hostPorts []int32 - var containerPorts []int32 - for _, value := range got { - hostPorts = append(hostPorts, value.HostPort) - containerPorts = append(containerPorts, value.ContainerPort) - } - sort.Slice(hostPorts, func(i, j int) bool { - return i < j - }) - sort.Slice(containerPorts, func(i, j int) bool { - return i < j - }) - if (hostPorts[len(hostPorts)-1] - hostPorts[0]) != (containerPorts[len(hostPorts)-1] - containerPorts[0]) { - t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) - } - sort.Slice(got, func(i, j int) bool { - return got[i].HostPort < got[j].HostPort - }) - for i := 0; i < len(got); i++ { - if got[i].HostPort != tt.want[i].HostPort || got[i].ContainerPort != tt.want[i].ContainerPort || got[i].Protocol != tt.want[i].Protocol || got[i].HostIP != tt.want[i].HostIP { - t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) - } - } + assert.Equal(t, len(got), len(tt.want)) + if len(got) > 0 { + sort.Slice(got, func(i, j int) bool { + return got[i].HostPort < got[j].HostPort + }) + assert.Equal( + t, + got[len(got)-1].HostPort-got[0].HostPort, + got[len(got)-1].ContainerPort-got[0].ContainerPort, + ) + for i := range len(got) { + assert.Equal(t, got[i].HostPort, tt.want[i].HostPort) + assert.Equal(t, got[i].ContainerPort, tt.want[i].ContainerPort) + assert.Equal(t, got[i].Protocol, tt.want[i].Protocol) + assert.Equal(t, got[i].HostIP, tt.want[i].HostIP) } - } else { - t.Errorf("ParsePortsLabel() = %v, want %v", got, tt.want) } } }) From 42367649a4aa9337f2b8d4b2642e7c654fa300ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 22:25:23 +0000 Subject: [PATCH 032/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.6.2 to 2.6.3. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.6.2...v2.6.3) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5a44ed35b05..1d8c95def7d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.6.2 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.6.3 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.4 //gomodjail:unconfined diff --git a/go.sum b/go.sum index ac3bb575873..59b8b8d4686 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.6.2 h1:31uZNNLeRrKjtUCc56CzPpPykW1Tm6SxLn4gx9Jjzqw= -github.com/compose-spec/compose-go/v2 v2.6.2/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= +github.com/compose-spec/compose-go/v2 v2.6.3 h1:zfW1Qp605ESySyth/zR+6yLr55XE0AiOAUlZLHKMoW0= +github.com/compose-spec/compose-go/v2 v2.6.3/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From 901769d8ef2ae7fec7d75e38e5cd8693d64a3f30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 09:41:06 +0000 Subject: [PATCH 033/868] build(deps): bump github.com/containerd/console from 1.0.4 to 1.0.5 Bumps [github.com/containerd/console](https://github.com/containerd/console) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/containerd/console/releases) - [Commits](https://github.com/containerd/console/compare/v1.0.4...v1.0.5) --- updated-dependencies: - dependency-name: github.com/containerd/console dependency-version: 1.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d8c95def7d..ea6b0779fb3 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/compose-spec/compose-go/v2 v2.6.3 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined - github.com/containerd/console v1.0.4 //gomodjail:unconfined + github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 github.com/containerd/containerd/v2 v2.1.0 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 59b8b8d4686..88dfd2a2495 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY1 github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= github.com/containerd/containerd/v2 v2.1.0 h1:lS6iJ/CwZrxYxKd6zWBz5LR7xOlMVQC78z68YtizUAM= From 11054f701938d8071b96f075837011b6ddb15b61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 09:41:09 +0000 Subject: [PATCH 034/868] build(deps): bump github.com/fluent/fluent-logger-golang Bumps [github.com/fluent/fluent-logger-golang](https://github.com/fluent/fluent-logger-golang) from 1.9.0 to 1.10.0. - [Changelog](https://github.com/fluent/fluent-logger-golang/blob/master/CHANGELOG.md) - [Commits](https://github.com/fluent/fluent-logger-golang/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: github.com/fluent/fluent-logger-golang dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 9 ++++----- go.sum | 14 ++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 1d8c95def7d..c6d7f9bffe2 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.23.0 +go 1.23.5 require ( github.com/Masterminds/semver/v3 v3.3.1 @@ -38,7 +38,7 @@ require ( github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.18.0 //gomodjail:unconfined - github.com/fluent/fluent-logger-golang v1.9.0 + github.com/fluent/fluent-logger-golang v1.10.0 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.2.1 github.com/ipfs/go-cid v0.5.0 @@ -75,7 +75,6 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cilium/ebpf v0.16.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect @@ -116,7 +115,7 @@ require ( github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect - github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined @@ -126,7 +125,7 @@ require ( github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect //gomodjail:unconfined github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect - github.com/tinylib/msgp v1.2.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect github.com/vbatts/tar-split v0.11.6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index 59b8b8d4686..18d6546c325 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWS github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= @@ -108,8 +106,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluent/fluent-logger-golang v1.9.0 h1:zUdY44CHX2oIUc7VTNZc+4m+ORuO/mldQDA7czhWXEg= -github.com/fluent/fluent-logger-golang v1.9.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU= +github.com/fluent/fluent-logger-golang v1.10.0 h1:JcLj8u3WclQv2juHGKTSzBRM5vIZjEqbrmvn/n+m1W0= +github.com/fluent/fluent-logger-golang v1.10.0/go.mod h1:UNyv8FAGmQcYJRtk+yfxhWqWUwsabTipgjXvBDR8kTs= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= @@ -260,8 +258,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/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.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= -github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -306,8 +304,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY= -github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= From 166ea55fd842b27355d50e45920623ecab769407 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Tue, 20 May 2025 20:43:41 +0800 Subject: [PATCH 035/868] [feature] enable --security-opt writable-cgroups=true|false as an option Signed-off-by: ningmingxiao --- .../container/container_run_cgroup_linux_test.go | 7 ++++++- docs/command-reference.md | 1 + pkg/cmd/container/run_security_linux.go | 13 ++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index 9f9e9812f13..dd6e8f93fdf 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -134,7 +134,9 @@ func TestRunCgroupV2(t *testing.T) { base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2", "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) - + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertOK() + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() + base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() } func TestRunCgroupV1(t *testing.T) { @@ -176,6 +178,9 @@ func TestRunCgroupV1(t *testing.T) { const expected = "42000\n100000\n0\n44040192\n6291456\n104857600\n0\n42\n2000\n0-1\n" base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertOK() + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() + base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() } // TestIssue3781 tests https://github.com/containerd/nerdctl/issues/3781 diff --git a/docs/command-reference.md b/docs/command-reference.md index 60db48e2b43..8d61e721c68 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -244,6 +244,7 @@ Security flags: - :whale: `--security-opt apparmor=`: specify custom AppArmor profile - :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities - :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container +- :whale: `--security-opt writable-cgroups`: making the cgroups writeable - :nerd_face: `--security-opt privileged-without-host-devices`: Don't pass host devices to privileged containers - :whale: `--cap-add=`: Add Linux capabilities - :whale: `--cap-drop=`: Drop Linux capabilities diff --git a/pkg/cmd/container/run_security_linux.go b/pkg/cmd/container/run_security_linux.go index 510310f265a..dbd76234c1b 100644 --- a/pkg/cmd/container/run_security_linux.go +++ b/pkg/cmd/container/run_security_linux.go @@ -18,6 +18,8 @@ package container import ( "errors" + "fmt" + "strconv" "strings" "sync" @@ -52,7 +54,7 @@ const ( func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) { for k := range securityOptsMap { switch k { - case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices": + case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups": default: log.L.Warnf("unknown security-opt: %q", k) } @@ -118,6 +120,15 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([ if privilegedWithoutHostDevices && !privileged { return nil, errors.New("flag `--security-opt privileged-without-host-devices` can't be used without `--privileged` enabled") } + if value, ok := securityOptsMap["writable-cgroups"]; ok { + writable, err := strconv.ParseBool(value) + if err != nil { + return nil, fmt.Errorf("invalid \"writable-cgroups\" value: %q", value) + } + if writable { + opts = append(opts, oci.WithWriteableCgroupfs) + } + } if privileged { if privilegedWithoutHostDevices { From f39631639738402107d05efa2c24fadd1e8b1d70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 18:03:56 +0000 Subject: [PATCH 036/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b09dff9c7f..777eede6974 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.0 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.1.1 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 98ebf01ef92..d9d8f080b4a 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.0 h1:lS6iJ/CwZrxYxKd6zWBz5LR7xOlMVQC78z68YtizUAM= -github.com/containerd/containerd/v2 v2.1.0/go.mod h1:t2VqM0zSiEdi33qgtsMwUKrYyVg4oq2FPe+cs3LBt7w= +github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM= +github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From 4d68bfc5fe895aab32de1517aa278a35efc69b6a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 21 May 2025 03:11:29 +0900 Subject: [PATCH 037/868] update containerd (2.1.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index ef6748b3649..526d19cabf7 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -141,9 +141,9 @@ jobs: go-version: 1.24 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.1.0 + containerd-version: 2.1.1 # Note: these as for amd64 - containerd-sha: 0e5359e957b66b679be807563a543c7416e305e3aafcf56bad90ef87a917014d + containerd-sha: 918e88fd393c28c89424e6535df0546ca36c1dfa7d8a5d685dee70b449380a9b containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 diff --git a/Dockerfile b/Dockerfile index e66d165188d..59c6e0501c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.1.0@061792f0ecf3684fb30a3a0eb006799b8c6638a7 +ARG CONTAINERD_VERSION=v2.1.1@cb1076646aa3740577fafbf3d914198b7fe8e3f7 ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY From 24eaa88bef9e1b5ebab97af2b8c41cfda32e0d2b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 21 May 2025 03:12:14 +0900 Subject: [PATCH 038/868] update containerd-fuse-overlayfs (2.1.6) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.5 | 6 ------ Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.5 create mode 100644 Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 diff --git a/Dockerfile b/Dockerfile index 59c6e0501c7..1553541603b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ ARG SLIRP4NETNS_VERSION=v1.3.2@BINARY ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS ARG FUSE_OVERLAYFS_VERSION=v1.15@BINARY -ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.5@BINARY +ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.6@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.5 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.5 deleted file mode 100644 index faf34421cfb..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.5 +++ /dev/null @@ -1,6 +0,0 @@ -acc149d60e2fad0cff480852c82f39bdaae2eb6faa265b2028c944ec572014f9 containerd-fuse-overlayfs-2.1.5-linux-amd64.tar.gz -2c1c12a99ac16e6ad137c474517d04cc7864d26d9045f50f99a6d6e887b9c425 containerd-fuse-overlayfs-2.1.5-linux-arm-v7.tar.gz -17759de9588cda1499877cc9587189eb24731ae41edda201087fd74658ddc127 containerd-fuse-overlayfs-2.1.5-linux-arm64.tar.gz -ce0310573fd667a2fa348588b12f1867a1bad5befc79d7d39e6419a7d4687ea8 containerd-fuse-overlayfs-2.1.5-linux-ppc64le.tar.gz -e9bbb9835346d8007a6429151eb7c7b23fa1f20b85aa6d20dd3702cb5a4c038a containerd-fuse-overlayfs-2.1.5-linux-riscv64.tar.gz -c088a7eee9b75f0a759e52d1ae2c8d69d21265594070f41021a94523d1c7bab1 containerd-fuse-overlayfs-2.1.5-linux-s390x.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 new file mode 100644 index 00000000000..b76b93d4d62 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 @@ -0,0 +1,6 @@ +8a768e4c953251d32b5e5d748d17593f7150834caaba403b483cf83f5856fea3 containerd-fuse-overlayfs-2.1.6-linux-amd64.tar.gz +a3af866a12e913cd1d4dda8e41c08345eca928a15ac1d466fdb2b00b013e14ee containerd-fuse-overlayfs-2.1.6-linux-arm-v7.tar.gz +417ca0c838e43e446f498b384d73f7caaeb00dc4c1c0fe4b0ecfdd36fd355daa containerd-fuse-overlayfs-2.1.6-linux-arm64.tar.gz +5fdebd9fb7b50473318f0410bc3ab46f3388ac8aa586b45c91a314af9ce6569c containerd-fuse-overlayfs-2.1.6-linux-ppc64le.tar.gz +7e1a9d2ba68ff31a8dfb53bf6e71b2879063b13c759922c8cff3013893829bca containerd-fuse-overlayfs-2.1.6-linux-riscv64.tar.gz +3c022651cdaff666e88996d5d9c7e776bf59419a03d7d718a28aa708036419f9 containerd-fuse-overlayfs-2.1.6-linux-s390x.tar.gz From 15cd2e0b97dac811f769afe010ed1cf221160776 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 21 May 2025 03:17:04 +0900 Subject: [PATCH 039/868] update buildg (0.5.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildg-v0.5.2 | 2 -- Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 | 4 ++++ 3 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildg-v0.5.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 diff --git a/Dockerfile b/Dockerfile index 1553541603b..ef3b8d55282 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.6@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug -ARG BUILDG_VERSION=v0.5.2@BINARY +ARG BUILDG_VERSION=v0.5.3@BINARY # Extra deps: gomodjail ARG GOMODJAIL_VERSION=v0.1.2@0a86b34442a491fa8f5e4565e9c846fce310239c diff --git a/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.2 b/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.2 deleted file mode 100644 index bff0ce012f6..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.2 +++ /dev/null @@ -1,2 +0,0 @@ -70371949ac56d118e55306091640e63537069a538a97c151eb7475c07cb5a8a4 buildg-v0.5.2-linux-amd64.tar.gz -9c44a5f8ecc3035998a07e1c564338205700cf5287c723e8ccba1da2815168cc buildg-v0.5.2-linux-arm64.tar.gz \ No newline at end of file diff --git a/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 b/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 new file mode 100644 index 00000000000..0e0aa45cbf4 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 @@ -0,0 +1,4 @@ +cf4c40c58ca795eeb6e75e2c6a0e5bb3a6a9c0623d51bc3b85163e5d483eeade buildg-full-v0.5.3-linux-amd64.tar.gz +47c479f2e5150c9c76294fa93a03ad20e5928f4315bf52ca8432bfb6707d4276 buildg-full-v0.5.3-linux-arm64.tar.gz +c289a454ae8673ff99acf56dec9ba97274c20d2015e80f7ac3b8eb8e4f77888f buildg-v0.5.3-linux-amd64.tar.gz +b2e244250ce7ea5c090388f2025a9c546557861d25bba7b0666aa512f01fa6cd buildg-v0.5.3-linux-arm64.tar.gz From 3b891956bc4bfcdcc761889553b3fdd34ce1c478 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 18 May 2025 10:28:03 -0700 Subject: [PATCH 040/868] Move away from raw github domain to API Signed-off-by: apostasie --- .../ghcr-image-build-and-publish.yml | 2 ++ .github/workflows/job-test-dependencies.yml | 3 +++ .github/workflows/job-test-in-container.yml | 3 +++ .github/workflows/job-test-in-lima.yml | 3 +++ Dockerfile | 15 +++++++++----- Makefile | 2 +- hack/scripts/lib.sh | 20 ++++++++++++++----- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 300fd4574f5..fb2a63206e0 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -68,3 +68,5 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index c4457bae1c7..625dca61fa8 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -39,6 +39,8 @@ jobs: uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 - name: "Run: build dependencies for the integration test environment image" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Cache is sharded per-architecture arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} @@ -49,6 +51,7 @@ jobs: args=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) fi docker buildx build \ + --secret id=github_token,env=GITHUB_TOKEN \ --cache-to type=gha,compression=zstd,mode=max,scope=test-integration-dependencies-"$arch" \ --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ --target build-dependencies "${args[@]}" . diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 6c1b9bae492..2257de5197d 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -86,6 +86,8 @@ jobs: canary::build::integration - if: ${{ ! inputs.canary }} name: "Init: prepare test image" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | buildargs=() # If the runner is old, use old ubuntu inside the container as well @@ -104,6 +106,7 @@ jobs: arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} docker buildx create --name with-gha --use docker buildx build \ + --secret id=github_token,env=GITHUB_TOKEN \ --output=type=docker \ --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ -t "$target" --target "$target" \ diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 22e2f3e9f8b..0867ac26a79 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -79,6 +79,8 @@ jobs: uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 - name: "Init: prepare integration tests" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eux @@ -88,6 +90,7 @@ jobs: [ "$TARGET" = "rootless" ] && TARGET=test-integration-rootless || TARGET=test-integration docker buildx create --name with-gha --use docker buildx build \ + --secret id=github_token,env=GITHUB_TOKEN \ --output=type=docker \ --cache-from type=gha,scope=test-integration-dependencies-amd64 \ -t test-integration --target "${TARGET}" \ diff --git a/Dockerfile b/Dockerfile index ef3b8d55282..e315706b0a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ make \ git \ + jq \ curl \ dpkg-dev ARG TARGETARCH @@ -75,6 +76,7 @@ RUN xx-apt-get update -qq && xx-apt-get install -qq --no-install-recommends \ pkg-config RUN git config --global advice.detachedHead false ADD hack/git-checkout-tag-with-hash.sh /usr/local/bin/ +ADD hack/scripts/lib.sh /usr/local/bin/http::helper FROM build-base AS build-containerd ARG TARGETARCH @@ -174,10 +176,11 @@ RUN cd /out/lib/systemd/system && \ echo "" >> buildkit.service && \ echo "# This file was converted from containerd.service, with \`sed -E '${sedcomm}'\`" >> buildkit.service ARG STARGZ_SNAPSHOTTER_VERSION -RUN STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ +RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ + STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \ - curl -o "stargz-snapshotter.service" -fsSL --proto '=https' --tlsv1.2 "https://raw.githubusercontent.com/containerd/stargz-snapshotter/${STARGZ_SNAPSHOTTER_VERSION}/script/config/etc/systemd/system/stargz-snapshotter.service" && \ + http::helper github::file containerd/stargz-snapshotter script/config/etc/systemd/system/stargz-snapshotter.service "${STARGZ_SNAPSHOTTER_VERSION}" > "stargz-snapshotter.service" && \ grep "${fname}" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ grep "stargz-snapshotter.service" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ tar xzf "${fname}" -C /out/bin && \ @@ -245,6 +248,10 @@ RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION%%@*}; \ ARG GOMODJAIL_VERSION COPY --from=build-gomodjail /out/${TARGETARCH:-amd64}/* /out/bin/ RUN echo "- gomodjail: ${GOMODJAIL_VERSION}" >> /out/share/doc/nerdctl-full/README.md +ARG CONTAINERIZED_SYSTEMD_VERSION +RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ + http::helper github::file AkihiroSuda/containerized-systemd docker-entrypoint.sh "${CONTAINERIZED_SYSTEMD_VERSION}" > /docker-entrypoint.sh && \ + chmod +x /docker-entrypoint.sh RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \ echo "## License" >> /out/share/doc/nerdctl-full/README.md && \ @@ -281,9 +288,7 @@ RUN apt-get update -qq && apt-get install -qq -y --no-install-recommends \ iproute2 iptables \ dbus dbus-user-session systemd systemd-sysv \ fuse3 -ARG CONTAINERIZED_SYSTEMD_VERSION -RUN curl -o /docker-entrypoint.sh -fsSL --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/AkihiroSuda/containerized-systemd/${CONTAINERIZED_SYSTEMD_VERSION}/docker-entrypoint.sh && \ - chmod +x /docker-entrypoint.sh +COPY --from=build-full /docker-entrypoint.sh /docker-entrypoint.sh COPY --from=out-full / /usr/local/ RUN perl -pi -e 's/multi-user.target/docker-entrypoint.target/g' /usr/local/lib/systemd/system/*.service && \ systemctl enable containerd buildkit stargz-snapshotter && \ diff --git a/Makefile b/Makefile index 65ec10c1c9a..1026a744eb4 100644 --- a/Makefile +++ b/Makefile @@ -253,7 +253,7 @@ TAR_OWNER0_FLAGS=--owner=0 --group=0 TAR_FLATTEN_FLAGS=--transform 's/.*\///g' define make_artifact_full_linux - $(DOCKER) build --output type=tar,dest=$(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar --target out-full --platform $(1) --build-arg GO_VERSION -f $(MAKEFILE_DIR)/Dockerfile $(MAKEFILE_DIR) + $(DOCKER) build --secret id=github_token,env=GITHUB_TOKEN --output type=tar,dest=$(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar --target out-full --platform $(1) --build-arg GO_VERSION -f $(MAKEFILE_DIR)/Dockerfile $(MAKEFILE_DIR) gzip -9 $(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar endef diff --git a/hack/scripts/lib.sh b/hack/scripts/lib.sh index 8eb93ca527a..7ce1da9a103 100755 --- a/hack/scripts/lib.sh +++ b/hack/scripts/lib.sh @@ -226,9 +226,10 @@ github::settoken(){ } github::request(){ - local endpoint="$1" + local accept="$1" + local endpoint="$2" local args=( - "Accept: application/vnd.github+json" + "Accept: $accept" "X-GitHub-Api-Version: 2022-11-28" ) @@ -237,21 +238,30 @@ github::request(){ http::get /dev/stdout https://api.github.com/"$endpoint" "${args[@]}" } +github::file(){ + local repo="$1" + local path="$2" + local ref="${3:-main}" + github::request "application/vnd.github.v3.raw" "repos/$repo/contents/$path?ref=$ref" +} + github::tags::latest(){ local repo="$1" - github::request "repos/$repo/tags" | jq -rc .[0].name + github::request "application/vnd.github+json" "repos/$repo/tags" | jq -rc .[0].name } github::releases(){ local repo="$1" - github::request "repos/$repo/releases" | + github::request "application/vnd.github+json" "repos/$repo/releases" | jq -rc .[] } github::releases::latest(){ local repo="$1" - github::request "repos/$repo/releases/latest" | jq -rc . + github::request "application/vnd.github+json" "repos/$repo/releases/latest" | jq -rc . } log::init host::require jq tar curl shasum + +[[ "${1:-}" != "github"* ]] || "$@" From b5fa00b0f2d610104e1b2c8507a96f42837c8c11 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 17 May 2025 21:25:39 -0700 Subject: [PATCH 041/868] Remove version: from test compose yaml files Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_cp_linux_test.go | 2 -- .../compose/compose_down_linux_test.go | 4 ---- .../compose/compose_exec_linux_test.go | 6 ----- .../compose/compose_kill_linux_test.go | 2 -- .../compose/compose_pause_linux_test.go | 2 -- .../compose/compose_port_linux_test.go | 4 ---- cmd/nerdctl/compose/compose_ps_linux_test.go | 4 ---- cmd/nerdctl/compose/compose_run_linux_test.go | 9 ------- cmd/nerdctl/compose/compose_up_linux_test.go | 24 ------------------- 9 files changed, 57 deletions(-) diff --git a/cmd/nerdctl/compose/compose_cp_linux_test.go b/cmd/nerdctl/compose/compose_cp_linux_test.go index 7d5dea8502c..c9cc1d1e040 100644 --- a/cmd/nerdctl/compose/compose_cp_linux_test.go +++ b/cmd/nerdctl/compose/compose_cp_linux_test.go @@ -31,8 +31,6 @@ import ( func TestComposeCopy(t *testing.T) { var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_down_linux_test.go b/cmd/nerdctl/compose/compose_down_linux_test.go index b995631d6b6..ad876052aa1 100644 --- a/cmd/nerdctl/compose/compose_down_linux_test.go +++ b/cmd/nerdctl/compose/compose_down_linux_test.go @@ -30,8 +30,6 @@ func TestComposeDownRemoveUsedNetwork(t *testing.T) { var ( dockerComposeYAMLOrphan = fmt.Sprintf(` -version: '3.1' - services: test: image: %s @@ -66,8 +64,6 @@ func TestComposeDownRemoveOrphans(t *testing.T) { var ( dockerComposeYAMLOrphan = fmt.Sprintf(` -version: '3.1' - services: test: image: %s diff --git a/cmd/nerdctl/compose/compose_exec_linux_test.go b/cmd/nerdctl/compose/compose_exec_linux_test.go index 0f86c447de4..8fca8d2376b 100644 --- a/cmd/nerdctl/compose/compose_exec_linux_test.go +++ b/cmd/nerdctl/compose/compose_exec_linux_test.go @@ -34,8 +34,6 @@ import ( func TestComposeExec(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -179,8 +177,6 @@ services: func TestComposeExecTTY(t *testing.T) { const expectedOutput = "speed 38400 baud" dockerComposeYAML := fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -267,8 +263,6 @@ services: func TestComposeExecWithIndex(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_kill_linux_test.go b/cmd/nerdctl/compose/compose_kill_linux_test.go index 6571950a62e..d66955ab33c 100644 --- a/cmd/nerdctl/compose/compose_kill_linux_test.go +++ b/cmd/nerdctl/compose/compose_kill_linux_test.go @@ -27,8 +27,6 @@ import ( func TestComposeKill(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: diff --git a/cmd/nerdctl/compose/compose_pause_linux_test.go b/cmd/nerdctl/compose/compose_pause_linux_test.go index 381e8686d6b..14624633f39 100644 --- a/cmd/nerdctl/compose/compose_pause_linux_test.go +++ b/cmd/nerdctl/compose/compose_pause_linux_test.go @@ -31,8 +31,6 @@ func TestComposePauseAndUnpause(t *testing.T) { } var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_port_linux_test.go b/cmd/nerdctl/compose/compose_port_linux_test.go index 15946557ad2..e066a873401 100644 --- a/cmd/nerdctl/compose/compose_port_linux_test.go +++ b/cmd/nerdctl/compose/compose_port_linux_test.go @@ -27,8 +27,6 @@ func TestComposePort(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -55,8 +53,6 @@ func TestComposePortFailure(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_ps_linux_test.go b/cmd/nerdctl/compose/compose_ps_linux_test.go index df6f1d3cfe5..5167e6c4796 100644 --- a/cmd/nerdctl/compose/compose_ps_linux_test.go +++ b/cmd/nerdctl/compose/compose_ps_linux_test.go @@ -32,8 +32,6 @@ import ( func TestComposePs(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s @@ -112,8 +110,6 @@ func TestComposePsJSON(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index bd606299bfe..ad08793ce23 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -40,7 +40,6 @@ func TestComposeRun(t *testing.T) { const expectedOutput = "speed 38400 baud" dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -120,7 +119,6 @@ func TestComposeRunWithServicePorts(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: web: image: %s @@ -182,7 +180,6 @@ func TestComposeRunWithPublish(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: web: image: %s @@ -242,7 +239,6 @@ func TestComposeRunWithEnv(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -274,7 +270,6 @@ func TestComposeRunWithUser(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -303,7 +298,6 @@ func TestComposeRunWithLabel(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -341,7 +335,6 @@ func TestComposeRunWithArgs(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -371,7 +364,6 @@ func TestComposeRunWithEntrypoint(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s @@ -399,7 +391,6 @@ func TestComposeRunWithVolume(t *testing.T) { containerName := testutil.Identifier(t) dockerComposeYAML := fmt.Sprintf(` -version: '3.1' services: alpine: image: %s diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 3d7597adf88..79bf19c7851 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -40,8 +40,6 @@ import ( func TestComposeUp(t *testing.T) { base := testutil.NewBase(t) helpers.ComposeUp(t, base, fmt.Sprintf(` -version: '3.1' - services: wordpress: @@ -117,8 +115,6 @@ func TestComposeUpNetWithStaticIP(t *testing.T) { base := testutil.NewBase(t) staticIP := "172.20.0.12" var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -155,8 +151,6 @@ func TestComposeUpMultiNet(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -204,8 +198,6 @@ func TestComposeUpOsEnvVar(t *testing.T) { base := testutil.NewBase(t) const containerName = "nginxAlpine" var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc1: image: %s @@ -237,8 +229,6 @@ func TestComposeUpDotEnvFile(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = ` -version: '3.1' - services: svc3: image: ghcr.io/stargz-containers/nginx:$TAG @@ -260,8 +250,6 @@ func TestComposeUpEnvFileNotFoundError(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = ` -version: '3.1' - services: svc4: image: ghcr.io/stargz-containers/nginx:$TAG @@ -284,8 +272,6 @@ func TestComposeUpWithScale(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: test: image: %s @@ -307,8 +293,6 @@ func TestComposeIPAMConfig(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: foo: image: %s @@ -337,8 +321,6 @@ func TestComposeUpRemoveOrphans(t *testing.T) { var ( dockerComposeYAMLOrphan = fmt.Sprintf(` -version: '3.1' - services: test: image: %s @@ -375,8 +357,6 @@ func TestComposeUpIdempotent(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: test: image: %s @@ -399,7 +379,6 @@ func TestComposeUpWithExternalNetwork(t *testing.T) { containerName2 := testutil.Identifier(t) + "-2" networkName := testutil.Identifier(t) + "-network" var dockerComposeYaml1 = fmt.Sprintf(` -version: "3" services: %s: image: %s @@ -413,7 +392,6 @@ networks: external: true `, containerName1, testutil.NginxAlpineImage, containerName1, networkName, networkName) var dockerComposeYaml2 = fmt.Sprintf(` -version: "3" services: %s: image: %s @@ -457,8 +435,6 @@ func TestComposeUpWithBypass4netns(t *testing.T) { testutil.RequireSystemService(t, "bypass4netnsd") base := testutil.NewBase(t) helpers.ComposeUp(t, base, fmt.Sprintf(` -version: '3.1' - services: wordpress: From 40bdc2bcd1a681de8e4dc3b4dd9e3875fd703d95 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 17 May 2025 21:28:22 -0700 Subject: [PATCH 042/868] Move from Alpine to Common for compose tests Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_build_linux_test.go | 2 +- cmd/nerdctl/compose/compose_config_test.go | 8 +++++--- cmd/nerdctl/compose/compose_create_linux_test.go | 12 ++++++------ cmd/nerdctl/compose/compose_down_linux_test.go | 8 ++++---- cmd/nerdctl/compose/compose_ps_linux_test.go | 8 ++++---- cmd/nerdctl/compose/compose_run_linux_test.go | 16 ++++++++-------- cmd/nerdctl/compose/compose_up_linux_test.go | 10 +++++----- 7 files changed, 33 insertions(+), 31 deletions(-) diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index d0092f0a9df..37a66babc03 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -29,7 +29,7 @@ import ( ) func TestComposeBuild(t *testing.T) { - dockerfile := "FROM " + testutil.AlpineImage + dockerfile := "FROM " + testutil.CommonImage testCase := nerdtest.Setup() diff --git a/cmd/nerdctl/compose/compose_config_test.go b/cmd/nerdctl/compose/compose_config_test.go index e9f16c6a92d..5fc09f814fe 100644 --- a/cmd/nerdctl/compose/compose_config_test.go +++ b/cmd/nerdctl/compose/compose_config_test.go @@ -25,15 +25,17 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeConfig(t *testing.T) { - const dockerComposeYAML = ` + dockerComposeYAML := fmt.Sprintf(` services: hello: - image: alpine:3.13 -` + image: %s +`, testutil.CommonImage) + testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 58e92514b82..41cc26685bd 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -35,7 +35,7 @@ func TestComposeCreate(t *testing.T) { services: svc0: image: %s -`, testutil.AlpineImage) +`, testutil.CommonImage) testCase := nerdtest.Setup() @@ -151,7 +151,7 @@ func TestComposeCreatePull(t *testing.T) { services: svc0: image: %s -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -161,12 +161,12 @@ services: defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() // `compose create --pull never` should fail: no such image - base.Cmd("rmi", "-f", testutil.AlpineImage).Run() + base.Cmd("rmi", "-f", testutil.CommonImage).Run() base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "never").AssertFail() // `compose create --pull missing(default)|always` should succeed: image is pulled and container is created - base.Cmd("rmi", "-f", testutil.AlpineImage).Run() + base.Cmd("rmi", "-f", testutil.CommonImage).Run() base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK() - base.Cmd("rmi", "-f", testutil.AlpineImage).Run() + base.Cmd("rmi", "-f", testutil.CommonImage).Run() base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "always").AssertOK() base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created") } @@ -181,7 +181,7 @@ services: image: %s `, imageSvc0) - dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage) + dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) diff --git a/cmd/nerdctl/compose/compose_down_linux_test.go b/cmd/nerdctl/compose/compose_down_linux_test.go index ad876052aa1..4a69c2ee9c4 100644 --- a/cmd/nerdctl/compose/compose_down_linux_test.go +++ b/cmd/nerdctl/compose/compose_down_linux_test.go @@ -34,14 +34,14 @@ services: test: image: %s command: "sleep infinity" -`, testutil.AlpineImage) +`, testutil.CommonImage) dockerComposeYAMLFull = fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" -`, dockerComposeYAMLOrphan, testutil.AlpineImage) +`, dockerComposeYAMLOrphan, testutil.CommonImage) ) compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) @@ -68,14 +68,14 @@ services: test: image: %s command: "sleep infinity" -`, testutil.AlpineImage) +`, testutil.CommonImage) dockerComposeYAMLFull = fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" -`, dockerComposeYAMLOrphan, testutil.AlpineImage) +`, dockerComposeYAMLOrphan, testutil.CommonImage) ) compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) diff --git a/cmd/nerdctl/compose/compose_ps_linux_test.go b/cmd/nerdctl/compose/compose_ps_linux_test.go index 5167e6c4796..6870f296968 100644 --- a/cmd/nerdctl/compose/compose_ps_linux_test.go +++ b/cmd/nerdctl/compose/compose_ps_linux_test.go @@ -62,7 +62,7 @@ services: volumes: wordpress: db: -`, testutil.WordpressImage, testutil.MariaDBImage, testutil.AlpineImage) +`, testutil.WordpressImage, testutil.MariaDBImage, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() projectName := comp.ProjectName() @@ -98,9 +98,9 @@ volumes: time.Sleep(3 * time.Second) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutWithFunc(assertHandler("wordpress_container", testutil.WordpressImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutWithFunc(assertHandler("db_container", testutil.MariaDBImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.AlpineImage) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage)) + base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.CommonImage) + base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) + base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--services", "-a").AssertOutContainsAll("wordpress\n", "db\n", "alpine\n") } diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index ad08793ce23..3739c25f045 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -45,7 +45,7 @@ services: image: %s entrypoint: - stty -`, testutil.AlpineImage) +`, testutil.CommonImage) testCase := nerdtest.Setup() @@ -246,7 +246,7 @@ services: - sh - -c - "echo $$FOO" -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -276,7 +276,7 @@ services: entrypoint: - id - -u -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -306,7 +306,7 @@ services: - "dummy log" labels: - "foo=bar" -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -340,7 +340,7 @@ services: image: %s entrypoint: - echo -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -369,7 +369,7 @@ services: image: %s entrypoint: - stty # should be changed -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -396,7 +396,7 @@ services: image: %s entrypoint: - stty # no meaning, just put any command -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -480,7 +480,7 @@ services: `, imageSvc0, keyPair.PublicKey, keyPair.PrivateKey, imageSvc1, keyPair.PrivateKey, imageSvc2) - dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage) + dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 79bf19c7851..38922510540 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -276,7 +276,7 @@ services: test: image: %s command: "sleep infinity" -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -303,7 +303,7 @@ networks: ipam: config: - subnet: 10.1.100.0/24 -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -325,14 +325,14 @@ services: test: image: %s command: "sleep infinity" -`, testutil.AlpineImage) +`, testutil.CommonImage) dockerComposeYAMLFull = fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" -`, dockerComposeYAMLOrphan, testutil.AlpineImage) +`, dockerComposeYAMLOrphan, testutil.CommonImage) ) compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) @@ -361,7 +361,7 @@ services: test: image: %s command: "sleep infinity" -`, testutil.AlpineImage) +`, testutil.CommonImage) comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() From 15914f1acb8215e0f6360f9e31c1fa96b9421392 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 17 May 2025 21:37:58 -0700 Subject: [PATCH 043/868] Remove useless port bindings Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_build_linux_test.go | 2 -- cmd/nerdctl/compose/compose_kill_linux_test.go | 2 -- cmd/nerdctl/compose/compose_ps_linux_test.go | 2 -- cmd/nerdctl/compose/compose_up_linux_test.go | 2 -- 4 files changed, 8 deletions(-) diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index 37a66babc03..79ef29a178b 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -51,8 +51,6 @@ services: svc1: build: . image: %s - ports: - - 8081:80 `, imageSvc0, imageSvc1) data.Temp().Save(dockerComposeYAML, "compose.yaml") diff --git a/cmd/nerdctl/compose/compose_kill_linux_test.go b/cmd/nerdctl/compose/compose_kill_linux_test.go index d66955ab33c..8c4687045b5 100644 --- a/cmd/nerdctl/compose/compose_kill_linux_test.go +++ b/cmd/nerdctl/compose/compose_kill_linux_test.go @@ -31,8 +31,6 @@ services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_ps_linux_test.go b/cmd/nerdctl/compose/compose_ps_linux_test.go index 6870f296968..c6ebb1de8aa 100644 --- a/cmd/nerdctl/compose/compose_ps_linux_test.go +++ b/cmd/nerdctl/compose/compose_ps_linux_test.go @@ -36,8 +36,6 @@ services: wordpress: image: %s container_name: wordpress_container - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 38922510540..610e0952105 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -533,8 +533,6 @@ func TestComposeUpAbortOnContainerExit(t *testing.T) { services: %s: image: %s - ports: - - 8080:80 %s: image: %s entrypoint: /bin/sh -c "exit 1" From 01c83c7302664e8e06494c3d1cf4440749cbab5d Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 18 May 2025 13:18:14 -0700 Subject: [PATCH 044/868] Use data.Temp() for assets Signed-off-by: apostasie --- .../container/container_inspect_linux_test.go | 4 +--- cmd/nerdctl/image/image_list_test.go | 8 ++------ cmd/nerdctl/image/image_prune_test.go | 14 ++++---------- cmd/nerdctl/ipfs/ipfs_registry_linux_test.go | 4 +--- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 7ccf35eeea9..855abe82aaa 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "slices" "strings" "testing" @@ -535,8 +534,7 @@ RUN groupadd -r test && useradd -r -g test test USER test `, testutil.UbuntuImage) - err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), data.Temp().Path()) helpers.Ensure("create", "--name", data.Identifier(), "--user", "test", data.Identifier()) diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 96b04c3faa7..3510b4708bc 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -19,8 +19,6 @@ package image import ( "errors" "fmt" - "os" - "path/filepath" "runtime" "slices" "strings" @@ -149,8 +147,7 @@ LABEL version=0.1 RUN echo "actually creating a layer so that docker sets the createdAt time" `, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", buildCtx) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -296,8 +293,7 @@ func TestImagesFilterDangling(t *testing.T) { CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", buildCtx) }, Cleanup: func(data test.Data, helpers test.Helpers) { diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index 402ea7bb94a..b7c4f61bfe0 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -18,8 +18,6 @@ package image import ( "fmt" - "os" - "path/filepath" "strings" "testing" "time" @@ -72,8 +70,7 @@ func TestImagePrune(t *testing.T) { `, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", buildCtx) // After we rebuild with tag, docker will no longer show the version from above // Swapping order does not change anything. @@ -120,8 +117,7 @@ func TestImagePrune(t *testing.T) { `, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", buildCtx) helpers.Ensure("build", "-t", identifier, buildCtx) imgList := helpers.Capture("images") @@ -164,8 +160,7 @@ CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) @@ -204,8 +199,7 @@ LABEL version=0.1`, testutil.CommonImage) RUN echo "Anything, so that we create actual content for docker to set the current time for CreatedAt" CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier(), buildCtx) imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index 5c044bf36af..99450b7c7d7 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -19,7 +19,6 @@ package ipfs import ( "fmt" "os" - "path/filepath" "regexp" "strings" "testing" @@ -138,8 +137,7 @@ CMD ["echo", "nerdctl-build-test-string"] `, data.Labels().Get(ipfsImageURLKey)) buildCtx := data.Temp().Path() - err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx) }, From b0307c08985843a17e71d9b2feee1ab7d7e3358a Mon Sep 17 00:00:00 2001 From: Kay Yan Date: Wed, 21 May 2025 11:55:32 +0000 Subject: [PATCH 045/868] cleanup for golang linter QF1012 Signed-off-by: Kay Yan --- .golangci.yml | 3 --- pkg/logging/cri_logger_test.go | 11 +++++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7fda716e51a..7338764f2b6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,9 +53,6 @@ linters: - "-ST1022" ##### TODO: fix and enable these - # 4 occurrences. - # Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...)) https://staticcheck.dev/docs/checks#QF1012 - - "-QF1012" # 6 occurrences. # Apply De Morgan’s law https://staticcheck.dev/docs/checks#QF1001 - "-QF1001" diff --git a/pkg/logging/cri_logger_test.go b/pkg/logging/cri_logger_test.go index 6d45e4999bc..01f16e43840 100644 --- a/pkg/logging/cri_logger_test.go +++ b/pkg/logging/cri_logger_test.go @@ -184,12 +184,12 @@ func TestReadLogsLimitsWithTimestamps(t *testing.T) { count := 10000 for i := 0; i < count; i++ { - tmpfile.WriteString(fmt.Sprintf(logLineFmt, i)) + fmt.Fprintf(tmpfile, logLineFmt, i) } tmpfile.WriteString(logLineNewLine) for i := 0; i < count; i++ { - tmpfile.WriteString(fmt.Sprintf(logLineFmt, i)) + fmt.Fprintf(tmpfile, logLineFmt, i) } tmpfile.WriteString(logLineNewLine) @@ -271,11 +271,10 @@ func TestReadRotatedLog(t *testing.T) { // Write the first three lines to log file now := time.Now().Format(time.RFC3339Nano) if line%2 == 0 { - file.WriteString(fmt.Sprintf( - "%s stdout P line%d\n", now, line)) + fmt.Fprintf(file, "%s stdout P line%d\n", now, line) + } else { - file.WriteString(fmt.Sprintf( - "%s stderr P line%d\n", now, line)) + fmt.Fprintf(file, "%s stderr P line%d\n", now, line) } time.Sleep(1 * time.Millisecond) From 41e1a56453c1349641375bd6e5a8f2739529c5e1 Mon Sep 17 00:00:00 2001 From: "rongfu.leng" Date: Mon, 31 Mar 2025 22:35:57 +0800 Subject: [PATCH 046/868] add commit compression type support Signed-off-by: rongfu.leng --- cmd/nerdctl/container/container_commit.go | 25 ++++++--- .../container/container_commit_test.go | 53 +++++++++++++++++++ docs/command-reference.md | 1 + pkg/api/types/container_types.go | 9 ++++ pkg/cmd/container/commit.go | 11 ++-- pkg/imgutil/commit/commit.go | 25 +++++---- pkg/testutil/nerdtest/requirements.go | 23 ++++++++ 7 files changed, 125 insertions(+), 22 deletions(-) diff --git a/cmd/nerdctl/container/container_commit.go b/cmd/nerdctl/container/container_commit.go index 7db58bca88e..62dbcced157 100644 --- a/cmd/nerdctl/container/container_commit.go +++ b/cmd/nerdctl/container/container_commit.go @@ -17,6 +17,8 @@ package container import ( + "errors" + "github.com/spf13/cobra" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" @@ -40,6 +42,7 @@ func CommitCommand() *cobra.Command { cmd.Flags().StringP("message", "m", "", "Commit message") cmd.Flags().StringArrayP("change", "c", nil, "Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT])") cmd.Flags().BoolP("pause", "p", true, "Pause container during commit") + cmd.Flags().StringP("compression", "", "gzip", "commit compression algorithm (zstd or gzip)") return cmd } @@ -66,15 +69,22 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { return types.ContainerCommitOptions{}, err } + com, err := cmd.Flags().GetString("compression") + if err != nil { + return types.ContainerCommitOptions{}, err + } + if com != string(types.Zstd) && com != string(types.Gzip) { + return types.ContainerCommitOptions{}, errors.New("--compression param only supports zstd or gzip") + } return types.ContainerCommitOptions{ - Stdout: cmd.OutOrStdout(), - GOptions: globalOptions, - Author: author, - Message: message, - Pause: pause, - Change: change, + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Author: author, + Message: message, + Pause: pause, + Change: change, + Compression: types.CompressionType(com), }, nil - } func commitAction(cmd *cobra.Command, args []string) error { @@ -82,7 +92,6 @@ func commitAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err diff --git a/cmd/nerdctl/container/container_commit_test.go b/cmd/nerdctl/container/container_commit_test.go index b0744f014a8..ee16dfbb822 100644 --- a/cmd/nerdctl/container/container_commit_test.go +++ b/cmd/nerdctl/container/container_commit_test.go @@ -19,10 +19,14 @@ package container import ( "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/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -86,3 +90,52 @@ func TestCommit(t *testing.T) { testCase.Run(t) } + +func TestZstdCommit(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All( + // FIXME: Docker does not support compression + require.Not(nerdtest.Docker), + nerdtest.ContainerdVersion("2.0.0"), + nerdtest.CGroup, + ) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", data.Identifier("image")) + } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier() + helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, identifier) + helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`) + helpers.Ensure("commit", identifier, data.Identifier("image"), "--compression=zstd") + data.Labels().Set("image", data.Identifier("image")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "verify zstd has been used", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "inspect", "--mode=native", data.Labels().Get("image")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.JSON([]native.Image{}, func(images []native.Image, s string, t tig.T) { + assert.Equal(t, len(images), 1) + assert.Equal(helpers.T(), images[0].Manifest.Layers[len(images[0].Manifest.Layers)-1].MediaType, "application/vnd.docker.image.rootfs.diff.tar.zstd") + }), + } + }, + }, + { + Description: "verify the image is working", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Labels().Get("image"), "sh", "-c", "--", "cat /foo") + }, + Expected: test.Expects(0, nil, expect.Equals("hello-test-commit\n")), + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 60db48e2b43..55c3dd4ce54 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -765,6 +765,7 @@ Flags: - :whale: `-m, --message`: Commit message - :whale: `-c, --change`: Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT]) - :whale: `-p, --pause`: Pause container during commit (default: true) +- :nerd_face: `--compression`: Commit compression algorithm (supported values: zstd or gzip) (default: gzip) (zstd is generally better for compression ratio but might not be as widely supported) ## Image management diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 3a7f89b0d5f..8f0d0137b02 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -385,8 +385,17 @@ type ContainerCommitOptions struct { Change []string // Pause container during commit Pause bool + // Compression is set commit compression algorithm + Compression CompressionType } +type CompressionType string + +const ( + Zstd CompressionType = "zstd" + Gzip CompressionType = "gzip" +) + // ContainerDiffOptions specifies options for `nerdctl (container) diff`. type ContainerDiffOptions struct { Stdout io.Writer diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go index 1e089c7e92c..1e219575cab 100644 --- a/pkg/cmd/container/commit.go +++ b/pkg/cmd/container/commit.go @@ -44,11 +44,12 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s } opts := &commit.Opts{ - Author: options.Author, - Message: options.Message, - Ref: parsedReference.String(), - Pause: options.Pause, - Changes: changes, + Author: options.Author, + Message: options.Message, + Ref: parsedReference.String(), + Pause: options.Pause, + Changes: changes, + Compression: options.Compression, } walker := &containerwalker.ContainerWalker{ diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index fd5886bfb7f..7eaafd18e4e 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -57,11 +57,12 @@ type Changes struct { } type Opts struct { - Author string - Message string - Ref string - Pause bool - Changes Changes + Author string + Message string + Ref string + Pause bool + Changes Changes + Compression types.CompressionType } var ( @@ -176,7 +177,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd // Sync filesystem to make sure that all the data writes in container could be persisted to disk. Sync() - diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ) + diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ, opts.Compression) if err != nil { return emptyDigest, fmt.Errorf("failed to export layer: %w", err) } @@ -356,8 +357,14 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container } // createDiff creates a layer diff into containerd's content store. -func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (ocispec.Descriptor, digest.Digest, error) { - newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer) +func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer, compression types.CompressionType) (ocispec.Descriptor, digest.Digest, error) { + opts := make([]diff.Opt, 0) + mediaType := images.MediaTypeDockerSchema2LayerGzip + if compression == types.Zstd { + opts = append(opts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd)) + mediaType = images.MediaTypeDockerSchema2LayerZstd + } + newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer, opts...) if err != nil { return ocispec.Descriptor{}, digest.Digest(""), err } @@ -378,7 +385,7 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c } return ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2LayerGzip, + MediaType: mediaType, Digest: newDesc.Digest, Size: info.Size, }, diffID, nil diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 237b349988b..e0e621501ee 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -24,6 +24,7 @@ import ( "os/exec" "strings" + "github.com/Masterminds/semver/v3" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/defaults" @@ -32,6 +33,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -416,3 +418,24 @@ var RemapIDs = &test.Requirement{ return false, "snapshotter does not support ID remapping" }, } + +func ContainerdVersion(v string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + ctx := context.Background() + namespace := defaultNamespace + address := defaults.DefaultAddress + client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address) + if err != nil { + return false, fmt.Sprintf("failed to create client: %v", err) + } + defer cancel() + if sv, err := infoutil.ServerSemVer(ctx, client); err != nil { + return false, err.Error() + } else if sv.LessThan(semver.MustParse(v)) { + return false, fmt.Sprintf("`nerdctl commit --compression expects containerd %s or later, got containerd %v", v, sv) + } + return true, "" + }, + } +} From 87a4206dcc5ee53522e2eb416ffd4ed96cc4f005 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 23:00:45 +0000 Subject: [PATCH 047/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.6.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 777eede6974..b5ea3011f2d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.6.3 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.6.4 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index d9d8f080b4a..0554452d966 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.6.3 h1:zfW1Qp605ESySyth/zR+6yLr55XE0AiOAUlZLHKMoW0= -github.com/compose-spec/compose-go/v2 v2.6.3/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= +github.com/compose-spec/compose-go/v2 v2.6.4 h1:Gjv6x8eAhqwwWvoXIo0oZ4bDQBh0OMwdU7LUL9PDLiM= +github.com/compose-spec/compose-go/v2 v2.6.4/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From dcbf14bc4e9979ebea5b7d7a86a3b0a8fef482b8 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 23 May 2025 22:28:56 -0700 Subject: [PATCH 048/868] Prevent empty container names Signed-off-by: apostasie --- cmd/nerdctl/container/container_create.go | 1 - pkg/api/types/container_types.go | 2 -- pkg/cmd/container/create.go | 39 ++++++++++------------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index e8d7e6a4d33..62ceb96a299 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -371,7 +371,6 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { // #endregion // #region for metadata flags - opt.NameChanged = cmd.Flags().Changed("name") opt.Name, err = cmd.Flags().GetString("name") if err != nil { return opt, err diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 3a7f89b0d5f..f60f456ef55 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -237,8 +237,6 @@ type ContainerCreateOptions struct { // #endregion // #region for metadata flags - // NameChanged specifies whether the name has been changed - NameChanged bool // Name assign a name to the container Name string // Label set meta data on a container diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 6699f97c00d..deee011f343 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -339,8 +339,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa cOpts = append(cOpts, lCOpts...) var containerNameStore namestore.NameStore - if options.Name == "" && !options.NameChanged { - // Automatically set the container name, unless `--name=""` was explicitly specified. + if options.Name == "" { var imageRef string if ensuredImage != nil { imageRef = ensuredImage.Ref @@ -352,15 +351,15 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } options.Name = parsedReference.SuggestContainerName(id) } - if options.Name != "" { - containerNameStore, err = namestore.New(dataStore, options.GOptions.Namespace) - if err != nil { - return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err - } - if err := containerNameStore.Acquire(options.Name, id); err != nil { - return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err - } + + containerNameStore, err = namestore.New(dataStore, options.GOptions.Namespace) + if err != nil { + return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err } + if err := containerNameStore.Acquire(options.Name, id); err != nil { + return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err + } + internalLabels.name = options.Name internalLabels.pidFile = options.PidFile @@ -714,9 +713,7 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO var hostConfigLabel dockercompat.HostConfigLabel var dnsSettings dockercompat.DNSSettings m[labels.Namespace] = internalLabels.namespace - if internalLabels.name != "" { - m[labels.Name] = internalLabels.name - } + m[labels.Name] = internalLabels.name m[labels.Hostname] = internalLabels.hostname m[labels.Domainname] = internalLabels.domainname extraHostsJSON, err := json.Marshal(internalLabels.extraHosts) @@ -1024,15 +1021,13 @@ func generateGcFunc(ctx context.Context, container containerd.Container, ns, id, log.G(ctx).WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, internalLabels.stateDir) } - if name != "" { - var errE error - if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil { - log.G(ctx).WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id) - } - // Double-releasing may happen with containers started with --rm, so, ignore NotFound errors - if errE := containerNameStore.Release(name, id); errE != nil && !errors.Is(errE, store.ErrNotFound) { - log.G(ctx).WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id) - } + var errE error + if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil { + log.G(ctx).WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id) + } + // Double-releasing may happen with containers started with --rm, so, ignore NotFound errors + if errE := containerNameStore.Release(name, id); errE != nil && !errors.Is(errE, store.ErrNotFound) { + log.G(ctx).WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id) } } } From 02b16a30c64176961552abfbb3219626d3ca9206 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Tue, 20 May 2025 17:47:10 +0000 Subject: [PATCH 049/868] test: update test functions in portutil_test.go - update the logic of TestParseFlagP - rename TestTestParseFlagPWithPlatformSpec to the appropriate function name Signed-off-by: Hayato Kiwata --- pkg/portutil/portutil_test.go | 96 +++++++++++++++++------------------ 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/pkg/portutil/portutil_test.go b/pkg/portutil/portutil_test.go index 55930e07f3b..46b9eff7544 100644 --- a/pkg/portutil/portutil_test.go +++ b/pkg/portutil/portutil_test.go @@ -30,7 +30,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) -func TestTestParseFlagPWithPlatformSpec(t *testing.T) { +func TestParseFlagPWithPlatformSpec(t *testing.T) { if runtime.GOOS != "linux" || rootlessutil.IsRootless() { t.Skip("no non-Linux platform or rootless mode in Linux are not supported yet") } @@ -232,10 +232,10 @@ func TestParseFlagP(t *testing.T) { s string } tests := []struct { - name string - args args - want []cni.PortMapping - wantErr bool + name string + args args + want []cni.PortMapping + wantErrMsg string }{ { name: "normal", @@ -250,7 +250,7 @@ func TestParseFlagP(t *testing.T) { HostIP: "127.0.0.1", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "with port range", @@ -271,15 +271,15 @@ func TestParseFlagP(t *testing.T) { HostIP: "127.0.0.1", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "with wrong port range", args: args{ s: "127.0.0.1:3000-3001:8080-8082/tcp", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: "invalid ranges specified for container and host Ports: 8080-8082 and 3000-3001", }, { name: "without host ip", @@ -294,7 +294,7 @@ func TestParseFlagP(t *testing.T) { HostIP: "0.0.0.0", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "without protocol", @@ -309,7 +309,7 @@ func TestParseFlagP(t *testing.T) { HostIP: "0.0.0.0", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "with protocol udp", @@ -324,10 +324,10 @@ func TestParseFlagP(t *testing.T) { HostIP: "0.0.0.0", }, }, - wantErr: false, + wantErrMsg: "", }, { - name: "with protocol udp", + name: "with protocol sctp", args: args{ s: "3000:8080/sctp", }, @@ -339,7 +339,7 @@ func TestParseFlagP(t *testing.T) { HostIP: "0.0.0.0", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "with ipv6 host ip", @@ -354,86 +354,82 @@ func TestParseFlagP(t *testing.T) { HostIP: "::0", }, }, - wantErr: false, + wantErrMsg: "", }, { name: "with invalid protocol", args: args{ s: "3000:8080/invalid", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: `invalid protocol "invalid"`, }, { name: "multiple colon", args: args{ s: "127.0.0.1:3000:0.0.0.0:8080", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: "invalid hostPort: 127.0.0.1:3000:0.0.0.0", }, { name: "multiple slash", args: args{ s: "127.0.0.1:3000:8080/tcp/", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: `failed to parse "127.0.0.1:3000:8080/tcp/", unexpected slashes`, }, { name: "invalid ip", args: args{ s: "127.0.0.256:3000:8080/tcp", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: "invalid ip address: 127.0.0.256", }, { name: "large port", args: args{ s: "3000:65536", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: "invalid containerPort: 65536", }, { name: "blank", args: args{ s: "", }, - want: nil, - wantErr: true, + want: nil, + wantErrMsg: "no port specified: ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseFlagP(tt.args.s) - t.Log(err) - if (err != nil) != tt.wantErr { - t.Errorf("ParseFlagP() error = %v, wantErr %v", err, tt.wantErr) - return + if tt.wantErrMsg == "" { + assert.NilError(t, err) + } else { + assert.Error(t, err, tt.wantErrMsg) } if !reflect.DeepEqual(got, tt.want) { - if len(got) == len(tt.want) { - if len(got) > 1 { - var hostPorts []int32 - var containerPorts []int32 - for _, value := range got { - hostPorts = append(hostPorts, value.HostPort) - containerPorts = append(containerPorts, value.ContainerPort) - } - sort.Slice(hostPorts, func(i, j int) bool { - return i < j - }) - sort.Slice(containerPorts, func(i, j int) bool { - return i < j - }) - if (hostPorts[len(hostPorts)-1] - hostPorts[0]) != (containerPorts[len(hostPorts)-1] - containerPorts[0]) { - t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) - } + assert.Equal(t, len(got), len(tt.want)) + if len(got) > 0 { + sort.Slice(got, func(i, j int) bool { + return got[i].HostPort < got[j].HostPort + }) + assert.Equal( + t, + got[len(got)-1].HostPort-got[0].HostPort, + got[len(got)-1].ContainerPort-got[0].ContainerPort, + ) + for i := range len(got) { + assert.Equal(t, got[i].HostPort, tt.want[i].HostPort) + assert.Equal(t, got[i].ContainerPort, tt.want[i].ContainerPort) + assert.Equal(t, got[i].Protocol, tt.want[i].Protocol) + assert.Equal(t, got[i].HostIP, tt.want[i].HostIP) } - } else { - t.Errorf("ParseFlagP() = %v, want %v", got, tt.want) } } }) From 1e2802082369d25d10213c67604ce71627d49b96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 22:22:34 +0000 Subject: [PATCH 050/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.1 to 0.15.2. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.1...v0.15.2) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b5ea3011f2d..5850e9b8044 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.1 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.2 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.16.3 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.16.3 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 0554452d966..e89a030b9bf 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlK github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.1 h1:huPj2d8J1BEx6mjm6h72BCo1kY5lTrfatnnujzpu6BA= -github.com/containerd/nydus-snapshotter v0.15.1/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= +github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= +github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 5fcbccc4c116785d599dace0a9442e5404154798 Mon Sep 17 00:00:00 2001 From: zzzzzzzzzy9 Date: Fri, 23 May 2025 17:31:50 +0800 Subject: [PATCH 051/868] stats: CPU perusage use the wrong systemUsage Signed-off-by: zzzzzzzzzy9 --- pkg/cmd/container/stats.go | 13 +++++- pkg/cmd/container/stats_linux.go | 75 ++++++++++++++++++++++++++++-- pkg/cmd/container/stats_nolinux.go | 8 +++- pkg/statsutil/stats.go | 5 ++ pkg/statsutil/stats_linux.go | 15 +++--- 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/container/stats.go b/pkg/cmd/container/stats.go index 8382fb4b241..e69074a6c8f 100644 --- a/pkg/cmd/container/stats.go +++ b/pkg/cmd/container/stats.go @@ -379,6 +379,17 @@ func collect(ctx context.Context, globalOptions types.GlobalCommandOptions, s *s continue } + // Sample system CPU usage close to container usage to avoid + // noise in metric calculations. + systemUsage, onlineCPUs, err := getSystemCPUUsage() + if err != nil { + u <- err + continue + } + systemInfo := statsutil.SystemInfo{ + OnlineCPUs: onlineCPUs, + SystemUsage: systemUsage, + } metric, err := task.Metrics(ctx) if err != nil { u <- err @@ -397,7 +408,7 @@ func collect(ctx context.Context, globalOptions types.GlobalCommandOptions, s *s } // when (firstSet == true), we only set container stats without rendering stat entry - statsEntry, err := setContainerStatsAndRenderStatsEntry(previousStats, firstSet, anydata, int(task.Pid()), netNS.Interfaces) + statsEntry, err := setContainerStatsAndRenderStatsEntry(previousStats, firstSet, anydata, int(task.Pid()), netNS.Interfaces, systemInfo) if err != nil { u <- err continue diff --git a/pkg/cmd/container/stats_linux.go b/pkg/cmd/container/stats_linux.go index 76aa1c96ab3..ee117888e53 100644 --- a/pkg/cmd/container/stats_linux.go +++ b/pkg/cmd/container/stats_linux.go @@ -17,9 +17,13 @@ package container import ( + "bufio" "errors" "fmt" + "io" "net" + "os" + "strconv" "strings" "time" @@ -33,8 +37,17 @@ import ( "github.com/containerd/nerdctl/v2/pkg/statsutil" ) +const ( + // The value comes from `C.sysconf(C._SC_CLK_TCK)`, and + // on Linux it's a constant which is safe to be hard coded, + // so we can avoid using cgo here. For details, see: + // https://github.com/containerd/cgroups/pull/12 + clockTicksPerSecond = 100 + nanoSecondsPerSecond = 1e9 +) + //nolint:nakedret -func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface) (statsEntry statsutil.StatsEntry, err error) { +func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface, systemInfo statsutil.SystemInfo) (statsEntry statsutil.StatsEntry, err error) { var ( data *v1.Metrics @@ -96,10 +109,10 @@ func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStat if data != nil { if !firstSet { - statsEntry, err = statsutil.SetCgroupStatsFields(previousStats, data, nlinks) + statsEntry, err = statsutil.SetCgroupStatsFields(previousStats, data, nlinks, systemInfo) } previousStats.CgroupCPU = data.CPU.Usage.Total - previousStats.CgroupSystem = data.CPU.Usage.Kernel + previousStats.CgroupSystem = systemInfo.SystemUsage if err != nil { return } @@ -117,3 +130,59 @@ func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStat return } + +// getSystemCPUUsage reads the system's CPU usage from /proc/stat and returns +// the total CPU usage in nanoseconds and the number of CPUs. +func getSystemCPUUsage() (cpuUsage uint64, cpuNum uint32, _ error) { + f, err := os.Open("/proc/stat") + if err != nil { + return 0, 0, err + } + defer f.Close() + + return readSystemCPUUsage(f) +} + +// readSystemCPUUsage parses CPU usage information from a reader providing +// /proc/stat format data. It returns the total CPU usage in nanoseconds +// and the number of CPUs. More: +// https://github.com/moby/moby/blob/26db31fdab628a2345ed8f179e575099384166a9/daemon/stats_unix.go#L327-L368 +func readSystemCPUUsage(r io.Reader) (cpuUsage uint64, cpuNum uint32, _ error) { + rdr := bufio.NewReaderSize(r, 1024) + + for { + data, isPartial, err := rdr.ReadLine() + + if err != nil { + return 0, 0, fmt.Errorf("error scanning /proc/stat file: %w", err) + } + // Assume all cpu* records are at the start of the file, like glibc: + // https://github.com/bminor/glibc/blob/5d00c201b9a2da768a79ea8d5311f257871c0b43/sysdeps/unix/sysv/linux/getsysstats.c#L108-L135 + if isPartial || len(data) < 4 { + break + } + line := string(data) + if line[:3] != "cpu" { + break + } + if line[3] == ' ' { + parts := strings.Fields(line) + if len(parts) < 8 { + return 0, 0, fmt.Errorf("invalid number of cpu fields") + } + var totalClockTicks uint64 + for _, i := range parts[1:8] { + v, err := strconv.ParseUint(i, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("unable to convert value %s to int: %w", i, err) + } + totalClockTicks += v + } + cpuUsage = (totalClockTicks * nanoSecondsPerSecond) / clockTicksPerSecond + } + if '0' <= line[3] && line[3] <= '9' { + cpuNum++ + } + } + return cpuUsage, cpuNum, nil +} diff --git a/pkg/cmd/container/stats_nolinux.go b/pkg/cmd/container/stats_nolinux.go index fbef460eaab..9f644ee1702 100644 --- a/pkg/cmd/container/stats_nolinux.go +++ b/pkg/cmd/container/stats_nolinux.go @@ -23,6 +23,12 @@ import ( "github.com/containerd/nerdctl/v2/pkg/statsutil" ) -func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface) (statsutil.StatsEntry, error) { +func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface, systemInfo statsutil.SystemInfo) (statsutil.StatsEntry, error) { return statsutil.StatsEntry{}, nil } + +// getSystemCPUUsage reads the system's CPU usage from /proc/stat and returns +// the total CPU usage in nanoseconds and the number of CPUs. +func getSystemCPUUsage() (uint64, uint32, error) { + return 0, 0, nil +} diff --git a/pkg/statsutil/stats.go b/pkg/statsutil/stats.go index c5bf3c0f68d..c61e5648a2f 100644 --- a/pkg/statsutil/stats.go +++ b/pkg/statsutil/stats.go @@ -26,6 +26,11 @@ import ( units "github.com/docker/go-units" ) +type SystemInfo struct { + OnlineCPUs uint32 + SystemUsage uint64 +} + // StatsEntry represents the statistics data collected from a container type StatsEntry struct { Name string diff --git a/pkg/statsutil/stats_linux.go b/pkg/statsutil/stats_linux.go index 4f1f53bc828..8e7f08f056b 100644 --- a/pkg/statsutil/stats_linux.go +++ b/pkg/statsutil/stats_linux.go @@ -38,8 +38,8 @@ func calculateMemPercent(limit float64, usedNo float64) float64 { return 0 } -func SetCgroupStatsFields(previousStats *ContainerStats, data *v1.Metrics, links []netlink.Link) (StatsEntry, error) { - cpuPercent := calculateCgroupCPUPercent(previousStats, data) +func SetCgroupStatsFields(previousStats *ContainerStats, data *v1.Metrics, links []netlink.Link, systemInfo SystemInfo) (StatsEntry, error) { + cpuPercent := calculateCgroupCPUPercent(previousStats, data, systemInfo) blkRead, blkWrite := calculateCgroupBlockIO(data) mem := calculateCgroupMemUsage(data) memLimit := getCgroupMemLimit(float64(data.Memory.Usage.Limit)) @@ -114,18 +114,21 @@ func getHostMemLimit() float64 { return float64(^uint64(0)) } -func calculateCgroupCPUPercent(previousStats *ContainerStats, metrics *v1.Metrics) float64 { +func calculateCgroupCPUPercent(previousStats *ContainerStats, metrics *v1.Metrics, systemInfo SystemInfo) float64 { var ( cpuPercent = 0.0 // calculate the change for the cpu usage of the container in between readings cpuDelta = float64(metrics.CPU.Usage.Total) - float64(previousStats.CgroupCPU) // calculate the change for the entire system between readings - systemDelta = float64(metrics.CPU.Usage.Kernel) - float64(previousStats.CgroupSystem) - onlineCPUs = float64(len(metrics.CPU.Usage.PerCPU)) + systemDelta = float64(systemInfo.SystemUsage) - float64(previousStats.CgroupSystem) + onlineCPUs = systemInfo.OnlineCPUs ) + if onlineCPUs == 0 { + onlineCPUs = uint32(len(metrics.CPU.Usage.PerCPU)) + } if systemDelta > 0.0 && cpuDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 + cpuPercent = (cpuDelta / systemDelta) * float64(onlineCPUs) * 100.0 } return cpuPercent } From 1c9e0d33c86fd397a6bebf1dd99a41e855e2f63e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 22:13:31 +0000 Subject: [PATCH 052/868] build(deps): bump docker/build-push-action from 6.17.0 to 6.18.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.17.0 to 6.18.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/1dc73863535b631f98b2378be8619f83b136f4a0...263435318d21b8e681c14492fe198d362a7d2c83) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 6.18.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index fb2a63206e0..63c971ca12b 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 From c71cfb65c3b578031683eafb8e2f3aec76458276 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 22:13:33 +0000 Subject: [PATCH 053/868] build(deps): bump lima-vm/lima-actions from 1.0.0 to 1.0.1 Bumps [lima-vm/lima-actions](https://github.com/lima-vm/lima-actions) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/lima-vm/lima-actions/releases) - [Commits](https://github.com/lima-vm/lima-actions/compare/be564a1408f84557d067b099a475652288074b2e...03b96d61959e83b2c737e44162c3088e81de0886) --- updated-dependencies: - dependency-name: lima-vm/lima-actions dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 0867ac26a79..8a7567cb120 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 1 - name: "Init: lima" - uses: lima-vm/lima-actions/setup@be564a1408f84557d067b099a475652288074b2e # v1.0.0 + uses: lima-vm/lima-actions/setup@03b96d61959e83b2c737e44162c3088e81de0886 # v1.0.1 id: lima-actions-setup - name: "Init: Cache" From 5bfcf9f24373b66c103fdd9fbb17a4b3a1f88500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 22:42:39 +0000 Subject: [PATCH 054/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.1.1+incompatible to 28.2.0+incompatible - [Commits](https://github.com/docker/cli/compare/v28.1.1...v28.2.0) Updates `github.com/docker/docker` from 28.1.1+incompatible to 28.2.1+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.1.1...v28.2.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.2.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.2.1+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 5850e9b8044..d29b86c3502 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.1.1+incompatible //gomodjail:unconfined - github.com/docker/docker v28.1.1+incompatible //gomodjail:unconfined + github.com/docker/cli v28.2.2+incompatible //gomodjail:unconfined + github.com/docker/docker v28.2.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index e89a030b9bf..e979b5d7415 100644 --- a/go.sum +++ b/go.sum @@ -86,10 +86,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= -github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= From e9ac44674211146d0c6cbbcd0336f5633b316906 Mon Sep 17 00:00:00 2001 From: fahed dorgaa Date: Tue, 20 May 2025 16:17:39 +0200 Subject: [PATCH 055/868] fix: improve network settings application and enhance iptables rule deletion Signed-off-by: fahed dorgaa --- .../container/container_remove_linux_test.go | 121 ++++++++++++++++++ pkg/ocihook/ocihook.go | 58 ++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 cmd/nerdctl/container/container_remove_linux_test.go diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go new file mode 100644 index 00000000000..997c3e99c7f --- /dev/null +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -0,0 +1,121 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "fmt" + "strconv" + "testing" + "time" + + "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/portlock" +) + +// iptablesCheckCommand is the shell command to check iptables rules +const iptablesCheckCommand = "iptables -t nat -S && iptables -t filter -S && iptables -t mangle -S" + +// testContainerRmIptablesExecutor is a common executor function for testing iptables rules cleanup +func testContainerRmIptablesExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { + t := helpers.T() + + // Get the container ID from the label + containerID := data.Labels().Get("containerID") + + // Remove the container + helpers.Ensure("rm", "-f", containerID) + + time.Sleep(1 * time.Second) + + // Create a TestableCommand using helpers.Custom + if rootlessutil.IsRootless() { + // In rootless mode, we need to enter the rootlesskit network namespace + if netns, err := rootlessutil.DetachedNetNS(); err != nil { + t.Fatalf("Failed to get detached network namespace: %v", err) + } else { + if netns != "" { + // Use containerd-rootless-setuptool.sh to enter the RootlessKit namespace + return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "nsenter", "--net="+netns, "sh", "-ec", iptablesCheckCommand) + } + // Enter into :RootlessKit namespace using containerd-rootless-setuptool.sh + return helpers.Custom("containerd-rootless-setuptool.sh", "nsenter", "--", "sh", "-ec", iptablesCheckCommand) + } + } + + // In non-rootless mode, check iptables rules directly on the host + return helpers.Custom("sh", "-ec", iptablesCheckCommand) +} + +// TestContainerRmIptables tests that iptables rules are cleared after container deletion +func TestContainerRmIptables(t *testing.T) { + testCase := nerdtest.Setup() + + // Require iptables and containerd-rootless-setuptool.sh commands to be available + testCase.Require = require.All( + require.Binary("iptables"), + require.Binary("containerd-rootless-setuptool.sh"), + require.Not(require.Windows), + require.Not(nerdtest.Docker), + ) + + testCase.SubTests = []*test.Case{ + { + Description: "Test iptables rules are cleared after container deletion", + Setup: func(data test.Data, helpers test.Helpers) { + // Get a free port using portlock + port, err := portlock.Acquire(0) + if err != nil { + helpers.T().Fatalf("Failed to acquire port: %v", err) + } + data.Labels().Set("port", strconv.Itoa(port)) + + // Create a container with port mapping to ensure iptables rules are created + containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "-p", fmt.Sprintf("%d:80", port), testutil.NginxAlpineImage) + data.Labels().Set("containerID", containerID) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // Make sure container is removed even if test fails + helpers.Anyhow("rm", "-f", data.Identifier()) + + // Release the acquired port + if portStr := data.Labels().Get("port"); portStr != "" { + port, _ := strconv.Atoi(portStr) + _ = portlock.Release(port) + } + }, + Command: testContainerRmIptablesExecutor, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // Get the container ID from the label + containerID := data.Labels().Get("containerID") + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + // Verify that the iptables output does not contain the container ID + Output: expect.DoesNotContain(containerID), + } + }, + }, + } + + testCase.Run(t) +} diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 79385396f59..a83275e907c 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -24,6 +24,7 @@ import ( "io" "net" "os" + "os/exec" "path/filepath" "strings" "time" @@ -418,7 +419,7 @@ func getIP6AddressOpts(opts *handlerOpts) ([]cni.NamespaceOpts, error) { return nil, nil } -func applyNetworkSettings(opts *handlerOpts) error { +func applyNetworkSettings(opts *handlerOpts) (err error) { portMapOpts, err := getPortMapOpts(opts) if err != nil { return err @@ -475,10 +476,19 @@ func applyNetworkSettings(opts *handlerOpts) error { // See https://github.com/containerd/nerdctl/issues/3355 _ = opts.cni.Remove(ctx, opts.fullID, "", namespaceOpts...) + // Defer CNI configuration removal to ensure idempotency of oci-hook. + defer func() { + if err != nil { + log.L.Warn("Container failed starting. Removing allocated network configuration.") + _ = opts.cni.Remove(ctx, opts.fullID, nsPath, namespaceOpts...) + } + }() + cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, namespaceOpts...) if err != nil { return fmt.Errorf("failed to call cni.Setup: %w", err) } + cniResRaw := cniRes.Raw() for i, cniName := range opts.cniNames { hsMeta.Networks[cniName] = cniResRaw[i] @@ -622,6 +632,15 @@ func onPostStop(opts *handlerOpts) error { log.L.WithError(err).Errorf("failed to call cni.Remove") return err } + + // opts.cni.Remove has trouble removing network configurations when netns is empty. + // Therefore, we force the deletion of iptables rules here to prevent netns exhaustion. + // This is a workaround until https://github.com/containernetworking/plugins/pull/1078 is merged. + if err := cleanupIptablesRules(opts.fullID); err != nil { + log.L.WithError(err).Warnf("failed to clean up iptables rules for container %s", opts.fullID) + // Don't return error here, continue with the rest of the cleanup + } + hs, err := hostsstore.New(opts.dataStore, ns) if err != nil { return err @@ -642,6 +661,43 @@ func onPostStop(opts *handlerOpts) error { return nil } +// cleanupIptablesRules cleans up iptables rules related to the container +func cleanupIptablesRules(containerID string) error { + // Check if iptables command exists + if _, err := exec.LookPath("iptables"); err != nil { + return fmt.Errorf("iptables command not found: %w", err) + } + + // Tables to check for rules + tables := []string{"nat", "filter", "mangle"} + + for _, table := range tables { + // Get all iptables rules for this table + cmd := exec.Command("iptables", "-t", table, "-S") + output, err := cmd.CombinedOutput() + if err != nil { + log.L.WithError(err).Warnf("failed to list iptables rules for table %s", table) + continue + } + + // Find and delete rules related to the container + rules := strings.Split(string(output), "\n") + for _, rule := range rules { + if strings.Contains(rule, containerID) { + // Execute delete command + deleteCmd := exec.Command("sh", "-c", "--", fmt.Sprintf(`iptables -t %s -D %s`, table, rule[3:])) + if deleteOutput, err := deleteCmd.CombinedOutput(); err != nil { + log.L.WithError(err).Warnf("failed to delete iptables rule: %s, output: %s", rule, string(deleteOutput)) + } else { + log.L.Debugf("deleted iptables rule: %s", rule) + } + } + } + } + + return nil +} + // writePidFile writes the pid atomically to a file. // From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/commands.go#L265-L282 func writePidFile(path string, pid int) error { From a27427fd29c1f5e0a6153907baf62b439362a9e3 Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Tue, 3 Jun 2025 17:18:42 +0000 Subject: [PATCH 056/868] mark go-iptables as gomodjail unconfined Signed-off-by: Swagat Bora --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d29b86c3502..23a9bb55526 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.7.1 //gomodjail:unconfined - github.com/coreos/go-iptables v0.8.0 + github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 From 3fa39407f1dae76a195543306ffaf775065d4f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:05:24 +0000 Subject: [PATCH 057/868] build(deps): bump the golang-x group with 4 updates Bumps the golang-x group with 4 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net), [golang.org/x/sync](https://github.com/golang/sync) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/crypto` from 0.38.0 to 0.39.0 - [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.39.0) Updates `golang.org/x/net` from 0.40.0 to 0.41.0 - [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0) Updates `golang.org/x/sync` from 0.14.0 to 0.15.0 - [Commits](https://github.com/golang/sync/compare/v0.14.0...v0.15.0) Updates `golang.org/x/text` from 0.25.0 to 0.26.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 23a9bb55526..fd650f2f7af 100644 --- a/go.mod +++ b/go.mod @@ -62,12 +62,12 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.5.2 - golang.org/x/crypto v0.38.0 - golang.org/x/net v0.40.0 - golang.org/x/sync v0.14.0 //gomodjail:unconfined + golang.org/x/crypto v0.39.0 + golang.org/x/net v0.41.0 + golang.org/x/sync v0.15.0 //gomodjail:unconfined golang.org/x/sys v0.33.0 //gomodjail:unconfined golang.org/x/term v0.32.0 //gomodjail:unconfined - golang.org/x/text v0.25.0 + golang.org/x/text v0.26.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined @@ -138,7 +138,7 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.24.0 // indirect + golang.org/x/mod v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect //gomodjail:unconfined google.golang.org/grpc v1.72.0 // indirect diff --git a/go.sum b/go.sum index e979b5d7415..b5a0be9b0e7 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -372,8 +372,8 @@ 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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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= @@ -390,8 +390,8 @@ 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 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= @@ -404,8 +404,8 @@ 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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -452,8 +452,8 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 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= @@ -466,8 +466,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 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= From 04f836d7f90f935feb2880f9c66cececb5a33a2e Mon Sep 17 00:00:00 2001 From: Vishwas Siravara Date: Mon, 29 May 2023 14:10:20 -0700 Subject: [PATCH 058/868] Check if port is used in publish flag Signed-off-by: Swagat Bora Co-authored-by: Vishwas Siravara --- .../container_run_network_linux_test.go | 55 +++++++++++++ pkg/portutil/port_allocate_linux.go | 77 ++++++++++++------- pkg/portutil/port_allocate_other.go | 4 + pkg/portutil/portutil.go | 10 +++ pkg/portutil/procnet/procnet.go | 11 +++ 5 files changed, 131 insertions(+), 26 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index f8a93aaa6a2..cd7e6905a38 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -349,6 +349,61 @@ func TestUniqueHostPortAssignement(t *testing.T) { } } +func TestHostPortAlreadyInUse(t *testing.T) { + testCases := []struct { + hostPort string + containerPort string + }{ + { + hostPort: "5000", + containerPort: "80/tcp", + }, + { + hostPort: "5000", + containerPort: "80/tcp", + }, + { + hostPort: "5000", + containerPort: "80/udp", + }, + { + hostPort: "5000", + containerPort: "80/sctp", + }, + } + + tID := testutil.Identifier(t) + + for i, tc := range testCases { + tc := tc + tcName := fmt.Sprintf("%+v", tc) + t.Run(tcName, func(t *testing.T) { + if strings.Contains(tc.containerPort, "sctp") && rootlessutil.IsRootless() { + t.Skip("sctp is not supported in rootless mode") + } + testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) + testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) + base := testutil.NewBase(t) + t.Cleanup(func() { + base.Cmd("rm", "-f", testContainerName1, testContainerName2).AssertOK() + }) + pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort) + cmd1 := base.Cmd("run", "-d", + "--name", testContainerName1, "-p", + pFlag, + testutil.NginxAlpineImage) + + cmd2 := base.Cmd("run", "-d", + "--name", testContainerName2, "-p", + pFlag, + testutil.NginxAlpineImage) + + cmd1.AssertOK() + cmd2.AssertFail() + }) + } +} + func TestRunPort(t *testing.T) { baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true) } diff --git a/pkg/portutil/port_allocate_linux.go b/pkg/portutil/port_allocate_linux.go index bd396a52555..5e2a5956b90 100644 --- a/pkg/portutil/port_allocate_linux.go +++ b/pkg/portutil/port_allocate_linux.go @@ -25,11 +25,14 @@ import ( const ( // This port range is compatible with Docker, FYI https://github.com/moby/moby/blob/eb9e42a09ee123af1d95bf7d46dd738258fa2109/libnetwork/portallocator/portallocator_unix.go#L7-L12 - allocateEnd = 60999 + allocateEnd = uint64(60999) + + tcpTimeWait = 6 //TIME_WAIT state is represented by the value 6 in /proc/net/tcp + tcpCloseWait = 8 //CLOSE_WAIT state is represented by the value 8 in /proc/net/tcp ) var ( - allocateStart = 49153 + allocateStart = uint64(49153) ) func filter(ss []procnet.NetworkDetail, filterFunc func(detail procnet.NetworkDetail) bool) (ret []procnet.NetworkDetail) { @@ -42,24 +45,56 @@ func filter(ss []procnet.NetworkDetail, filterFunc func(detail procnet.NetworkDe } func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, error) { - netprocData, err := procnet.ReadStatsFileData(protocol) + usedPorts, err := getUsedPorts(ip, protocol) if err != nil { return 0, 0, err } - netprocItems := procnet.Parse(netprocData) + + start := allocateStart + if count > allocateEnd-allocateStart+1 { + return 0, 0, fmt.Errorf("can not allocate %d ports", count) + } + for start < allocateEnd { + needReturn := true + for i := start; i < start+count; i++ { + if _, ok := usedPorts[i]; ok { + needReturn = false + break + } + } + if needReturn { + allocateStart = start + count + return start, start + count - 1, nil + } + start += count + } + return 0, 0, fmt.Errorf("there is not enough %d free ports", count) +} + +func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) { + netprocItems := []procnet.NetworkDetail{} + + if protocol == "tcp" || protocol == "udp" { + netprocData, err := procnet.ReadStatsFileData(protocol) + if err != nil { + return nil, err + } + netprocItems = append(netprocItems, procnet.Parse(netprocData)...) + } + // In some circumstances, when we bind address like "0.0.0.0:80", we will get the formation of ":::80" in /proc/net/tcp6. // So we need some trick to process this situation. if protocol == "tcp" { tempTCPV6Data, err := procnet.ReadStatsFileData("tcp6") if err != nil { - return 0, 0, err + return nil, err } netprocItems = append(netprocItems, procnet.Parse(tempTCPV6Data)...) } if protocol == "udp" { tempUDPV6Data, err := procnet.ReadStatsFileData("udp6") if err != nil { - return 0, 0, err + return nil, err } netprocItems = append(netprocItems, procnet.Parse(tempUDPV6Data)...) } @@ -73,12 +108,20 @@ func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, err usedPort := make(map[uint64]bool) for _, value := range netprocItems { + // Skip ports in TIME_WAIT or CLOSE_WAIT state + if protocol == "tcp" && (value.State == tcpTimeWait || value.State == tcpCloseWait) { + // In rootless mode, Rootlesskit creates extra socket connections to proxy traffic from the host network namespace + // to the container namespace. Proxy TCP connections can remain in TIME_WAIT state for 10-20 seconds even when the + // container is stopped/removed, which is standard TCP behavior. These ports are actually available for allocation + // despite appearing in /proc/net/tcp. + continue + } usedPort[value.LocalPort] = true } ipTableItems, err := iptable.ReadIPTables("nat") if err != nil { - return 0, 0, err + return nil, err } destinationPorts := iptable.ParseIPTableRules(ipTableItems) @@ -86,23 +129,5 @@ func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, err usedPort[port] = true } - start := uint64(allocateStart) - if count > uint64(allocateEnd-allocateStart+1) { - return 0, 0, fmt.Errorf("can not allocate %d ports", count) - } - for start < allocateEnd { - needReturn := true - for i := start; i < start+count; i++ { - if _, ok := usedPort[i]; ok { - needReturn = false - break - } - } - if needReturn { - allocateStart = int(start + count) - return start, start + count - 1, nil - } - start += count - } - return 0, 0, fmt.Errorf("there is not enough %d free ports", count) + return usedPort, nil } diff --git a/pkg/portutil/port_allocate_other.go b/pkg/portutil/port_allocate_other.go index 9749574c97c..957c9538f38 100644 --- a/pkg/portutil/port_allocate_other.go +++ b/pkg/portutil/port_allocate_other.go @@ -23,3 +23,7 @@ import "fmt" func portAllocate(protocol string, ip string, count uint64) (uint64, uint64, error) { return 0, 0, fmt.Errorf("auto port allocate are not support Non-Linux platform yet") } + +func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) { + return nil, nil +} diff --git a/pkg/portutil/portutil.go b/pkg/portutil/portutil.go index 28a1836bb2f..a832470abce 100644 --- a/pkg/portutil/portutil.go +++ b/pkg/portutil/portutil.go @@ -101,6 +101,16 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) { if err != nil { return nil, fmt.Errorf("invalid hostPort: %s", hostPort) } + var usedPorts map[uint64]bool + usedPorts, err = getUsedPorts(ip, proto) + if err != nil { + return nil, err + } + for i := startHostPort; i <= endHostPort; i++ { + if usedPorts[i] { + return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", ip, i) + } + } } if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { if endPort != startPort { diff --git a/pkg/portutil/procnet/procnet.go b/pkg/portutil/procnet/procnet.go index d5a382bff85..c68b5bed2b9 100644 --- a/pkg/portutil/procnet/procnet.go +++ b/pkg/portutil/procnet/procnet.go @@ -27,6 +27,7 @@ import ( type NetworkDetail struct { LocalIP net.IP LocalPort uint64 + State int } func Parse(data []string) (results []NetworkDetail) { @@ -37,9 +38,19 @@ func Parse(data []string) (results []NetworkDetail) { if err != nil { continue } + + state := 0 + if len(lineData) > 2 { + stateHex, err := strconv.ParseInt(lineData[3], 16, 32) + if err == nil { + state = int(stateHex) + } + } + results = append(results, NetworkDetail{ LocalIP: ip, LocalPort: uint64(port), + State: state, }) } return results From a4957197c3b536ea6882d93faf8726a60eeadd4c Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 7 Jun 2025 02:15:19 +0000 Subject: [PATCH 059/868] fix: display containers belonging to multiple networks in nerdctl network inspect When a container belongs to multiple networks, running the nerdctl network inspect command on the network to which the container belongs does not display the container in the current implementation. Specifically, it is displayed as follows. ``` $ sudo nerdctl run -d --name net --net=foo --net=bar nginx d88e878f0c60823bd0c361bad250f27b19ad117fb3336fcf18fa26ab1910c367 $ sudo nerdctl network inspect foo | jq .[0].Containers {} $ sudo nerdctl network inspect bar | jq .[0].Containers {} ``` Ideally, running the nerdctl network inspect command on the networks to which the contaienr belongs should display the container name as follows. ``` $ sudo nerdctl network inspect foo | jq .[0].Containers { "d88e878f0c60823bd0c361bad250f27b19ad117fb3336fcf18fa26ab1910c367": { "Name": "net" } } $ sudo nerdctl network inspect bar | jq .[0].Containers { "d88e878f0c60823bd0c361bad250f27b19ad117fb3336fcf18fa26ab1910c367": { "Name": "net" } } ``` Therefore, this behaviour is fixed in this PR. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/network/network_inspect_test.go | 33 +++++++++++++++++++++ pkg/cmd/network/inspect.go | 31 ++++++++++++++++--- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index ed4bb00d1e5..10b9b5a8459 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -320,6 +320,39 @@ func TestNetworkInspect(t *testing.T) { } }, }, + { + Description: "Display containers belonging to multiple networks in the output of nerdctl network inspect", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("nginx-network-1")) + helpers.Ensure("network", "create", data.Identifier("nginx-network-2")) + + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("nginx-network-1"), "--network", data.Identifier("nginx-network-2"), testutil.NginxAlpineImage) + + data.Labels().Set("containerID", strings.Trim(helpers.Capture("inspect", data.Identifier(), "--format", "{{.Id}}"), "\n")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "remove", data.Identifier("nginx-network-1")) + helpers.Anyhow("network", "remove", data.Identifier("nginx-network-2")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("nginx-network-1")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var dc []dockercompat.Network + err := json.Unmarshal([]byte(stdout), &dc) + + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.Equal(t, dc[0].Name, data.Identifier("nginx-network-1")) + assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") + assert.Equal(t, data.Identifier(), dc[0].Containers[data.Labels().Get("containerID")].Name) + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/cmd/network/inspect.go b/pkg/cmd/network/inspect.go index 0a9090b95aa..a958cc9fa9e 100644 --- a/pkg/cmd/network/inspect.go +++ b/pkg/cmd/network/inspect.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" @@ -58,16 +59,14 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo } network := netList[0] - var filters = []string{fmt.Sprintf("labels.%q==%q", labels.Networks, []string{network.Name})} + var filters = []string{fmt.Sprintf("labels.%q~=%q", labels.Networks, network.Name)} filteredContainers, err := client.Containers(ctx, filters...) - if err != nil { return err } var containers []*native.Container - for _, container := range filteredContainers { nativeContainer, err := containerinspector.Inspect(ctx, container) if err != nil { @@ -76,7 +75,14 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo if nativeContainer.Process == nil || nativeContainer.Process.Status.Status != containerd.Running { continue } - containers = append(containers, nativeContainer) + + isNetworkMember, err := isContainerInNetwork(ctx, container, network.Name) + if err != nil { + return err + } + if isNetworkMember { + containers = append(containers, nativeContainer) + } } r := &native.Network{ @@ -113,3 +119,20 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo return err } + +func isContainerInNetwork(ctx context.Context, container containerd.Container, networkName string) (bool, error) { + info, err := container.Info(ctx) + if err != nil { + return false, err + } + networkLabels, ok := info.Labels[labels.Networks] + if !ok { + return false, nil + } + + var containerNetworks []string + if err := json.Unmarshal([]byte(networkLabels), &containerNetworks); err != nil { + return false, err + } + return slices.Contains(containerNetworks, networkName), nil +} From 3086d712380e8144df2bb624cd7fa927ecaa5cc0 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 8 Jun 2025 16:04:46 +0000 Subject: [PATCH 060/868] test: refactor TestNetworkInspect in network_inspect_test.go Based on the following three pieces of advice received in the merged PR, this commit refactors the tests. - https://github.com/containerd/nerdctl/pull/4309#discussion_r2134362878 - https://github.com/containerd/nerdctl/pull/4309#discussion_r2134362909 - https://github.com/containerd/nerdctl/pull/4309#discussion_r2134363162 Signed-off-by: Hayato Kiwata --- cmd/nerdctl/network/network_inspect_test.go | 25 +++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 10b9b5a8459..197836df663 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -323,33 +324,29 @@ func TestNetworkInspect(t *testing.T) { { Description: "Display containers belonging to multiple networks in the output of nerdctl network inspect", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier("nginx-network-1")) - helpers.Ensure("network", "create", data.Identifier("nginx-network-2")) + helpers.Ensure("network", "create", data.Identifier("network-1")) + helpers.Ensure("network", "create", data.Identifier("network-2")) - helpers.Ensure("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("nginx-network-1"), "--network", data.Identifier("nginx-network-2"), testutil.NginxAlpineImage) + containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("network-1"), "--network", data.Identifier("network-2"), testutil.CommonImage, "sleep", nerdtest.Infinity) - data.Labels().Set("containerID", strings.Trim(helpers.Capture("inspect", data.Identifier(), "--format", "{{.Id}}"), "\n")) + data.Labels().Set("containerID", strings.Trim(containerID, "\n")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("network", "remove", data.Identifier("nginx-network-1")) - helpers.Anyhow("network", "remove", data.Identifier("nginx-network-2")) + helpers.Anyhow("network", "remove", data.Identifier("network-1")) + helpers.Anyhow("network", "remove", data.Identifier("network-2")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "inspect", data.Identifier("nginx-network-1")) + return helpers.Command("network", "inspect", data.Identifier("network-1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var dc []dockercompat.Network - err := json.Unmarshal([]byte(stdout), &dc) - - assert.NilError(t, err, "Unable to unmarshal output\n"+info) + Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, info string, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - assert.Equal(t, dc[0].Name, data.Identifier("nginx-network-1")) + assert.Equal(t, dc[0].Name, data.Identifier("network-1")) assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") assert.Equal(t, data.Identifier(), dc[0].Containers[data.Labels().Get("containerID")].Name) - }, + }), } }, }, From fee517ad2f93c581cd4ee88cb1490f685c46ce4b Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 9 Jun 2025 15:44:47 +0000 Subject: [PATCH 061/868] test: add one test case to TestNetworkInspect in network_inspect_test.go Suppose that a `container A` is running in the network `some-network-as-well`. In this case, this commit adds a test to ensure that `container A` isn't displayed in the `Containers` section of the output of `nerdctl network inspect some-network`. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/network/network_inspect_test.go | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 197836df663..e714d7cdc1f 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -350,6 +350,32 @@ func TestNetworkInspect(t *testing.T) { } }, }, + { + Description: "Display only containers attached to the specific network", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("some-network")) + helpers.Ensure("network", "create", data.Identifier("some-network-as-well")) + + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("some-network-as-well"), testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "remove", data.Identifier("some-network")) + helpers.Anyhow("network", "remove", data.Identifier("some-network-as-well")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("some-network")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, info string, t tig.T) { + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.Equal(t, dc[0].Name, data.Identifier("some-network")) + assert.Equal(t, 0, len(dc[0].Containers), "Expected no containers as per configuration, but got multiple.") + }), + } + }, + }, } testCase.Run(t) From 9df1527422ca89067ad8124cfdd37d91e008cdfe Mon Sep 17 00:00:00 2001 From: shubhranshu153 Date: Tue, 17 Jun 2025 18:40:47 -0700 Subject: [PATCH 062/868] fix:go-license dependency Signed-off-by: shubhranshu153 --- Dockerfile | 2 +- Makefile | 6 ++++-- hack/build-integration-canary.sh | 4 +++- mod/tigron/Makefile | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e315706b0a2..4acda707b80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GOMODJAIL_VERSION=v0.1.2@0a86b34442a491fa8f5e4565e9c846fce310239c ARG GO_VERSION=1.24 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 -ARG GOTESTSUM_VERSION=v1.12.2 +ARG GOTESTSUM_VERSION=0d9599e513d70e5792bb9334869f82f6e8b53d4d ARG NYDUS_VERSION=v2.3.1 ARG SOCI_SNAPSHOTTER_VERSION=0.9.0 ARG KUBO_VERSION=v0.34.1 diff --git a/Makefile b/Makefile index 1026a744eb4..9d9602e1711 100644 --- a/Makefile +++ b/Makefile @@ -218,12 +218,14 @@ install-dev-tools: # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) + # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 + # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ + && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ - && go install github.com/google/go-licenses/v2@d01822334fba5896920a060f762ea7ecdbd086e8 \ - && go install gotest.tools/gotestsum@ac6dad9c7d87b969004f7749d1942938526c9716 + && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) diff --git a/hack/build-integration-canary.sh b/hack/build-integration-canary.sh index ae205c90bed..725628b962a 100755 --- a/hack/build-integration-canary.sh +++ b/hack/build-integration-canary.sh @@ -28,7 +28,9 @@ readonly root # "Blacklisting" here means that any dependency which name is blacklisted will be left untouched, at the version # currently pinned in the Dockerfile. # This is convenient so that currently broken alpha/beta/RC can be held back temporarily to keep the build green -blacklist=() +# TODO: Blacklisting gotestsum until a new version compatible with golang v1.25rc1 is released +# Issue: https://github.com/google/go-licenses/issues/312 +blacklist=(gotestsum) # List all the repositories we depend on to build and run integration tests dependencies=( diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index ba2bbf0d754..de48ce35c39 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -168,11 +168,13 @@ install-dev-tools: # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) + # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 + # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ - && go install github.com/google/go-licenses/v2@d01822334fba5896920a060f762ea7ecdbd086e8 + && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) From 0eee948d0caf530cef2bec78a5a379529bcf8168 Mon Sep 17 00:00:00 2001 From: shubhranshu153 Date: Tue, 17 Jun 2025 18:41:10 -0700 Subject: [PATCH 063/868] fix: sbom and provenance tests Signed-off-by: shubhranshu153 --- cmd/nerdctl/builder/builder_build_test.go | 55 ++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 839fd0d6e01..a6aff0ea60d 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -19,7 +19,9 @@ package builder import ( "errors" "fmt" + "os" "path/filepath" + "regexp" "runtime" "strings" "testing" @@ -851,8 +853,9 @@ RUN curl -I http://google.com func TestBuildAttestation(t *testing.T) { nerdtest.Setup() - const testSBOMFileName = "sbom.spdx.json" - const testProvenanceFileName = "provenance.json" + // Using regex patterns to match SBOM and provenance files with optional platform suffix + const testSBOMFilePattern = `sbom\.spdx(?:\.[a-z0-9_]+)?\.json` + const testProvenanceFilePattern = `provenance(?:\.[a-z0-9_]+)?\.json` dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) @@ -892,7 +895,17 @@ func TestBuildAttestation(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout, info string, t *testing.T) { - data.Temp().Exists("dir-for-bom", testSBOMFileName) + files, err := os.ReadDir(data.Temp().Path("dir-for-bom")) + assert.NilError(t, err, "failed to read directory") + + found := false + for _, file := range files { + if !file.IsDir() && regexp.MustCompile(testSBOMFilePattern).MatchString(file.Name()) { + found = true + break + } + } + assert.Assert(t, found, "no SBOM file matching pattern %s found", testSBOMFilePattern) }, } }, @@ -914,7 +927,17 @@ func TestBuildAttestation(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout, info string, t *testing.T) { - data.Temp().Exists("dir-for-prov", testProvenanceFileName) + files, err := os.ReadDir(data.Temp().Path("dir-for-prov")) + assert.NilError(t, err, "failed to read directory") + + found := false + for _, file := range files { + if !file.IsDir() && regexp.MustCompile(testProvenanceFilePattern).MatchString(file.Name()) { + found = true + break + } + } + assert.Assert(t, found, "no provenance file matching pattern %s found", testProvenanceFilePattern) }, } }, @@ -937,8 +960,28 @@ func TestBuildAttestation(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout, info string, t *testing.T) { - data.Temp().Exists("dir-for-attest", testSBOMFileName) - data.Temp().Exists("dir-for-attest", testProvenanceFileName) + // Check if any file in the directory matches the SBOM file pattern + files, err := os.ReadDir(data.Temp().Path("dir-for-attest")) + assert.NilError(t, err, "failed to read directory") + + sbomFound := false + for _, file := range files { + if !file.IsDir() && regexp.MustCompile(testSBOMFilePattern).MatchString(file.Name()) { + sbomFound = true + break + } + } + assert.Assert(t, sbomFound, "no SBOM file matching pattern %s found", testSBOMFilePattern) + + // Check if any file in the directory matches the provenance file pattern + provenanceFound := false + for _, file := range files { + if !file.IsDir() && regexp.MustCompile(testProvenanceFilePattern).MatchString(file.Name()) { + provenanceFound = true + break + } + } + assert.Assert(t, provenanceFound, "no provenance file matching pattern %s found", testProvenanceFilePattern) }, } }, From b074e5b04fba0f90152014ce550d957865deded2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 06:20:50 +0000 Subject: [PATCH 064/868] build(deps): bump github.com/go-viper/mapstructure/v2 Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.3.0. - [Release notes](https://github.com/go-viper/mapstructure/releases) - [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.3.0) --- updated-dependencies: - dependency-name: github.com/go-viper/mapstructure/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fd650f2f7af..99f9540b184 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/fatih/color v1.18.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.0 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined - github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/go-viper/mapstructure/v2 v2.3.0 github.com/ipfs/go-cid v0.5.0 github.com/klauspost/compress v1.18.0 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined diff --git a/go.sum b/go.sum index b5a0be9b0e7..e550cf29f3f 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= From f38310bb9e6afb4e4de861e02fc1877c0c69a772 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 6 Jun 2025 13:34:48 -0700 Subject: [PATCH 065/868] Fix typo in `across`. Signed-off-by: apostasie --- docs/dev/auditing_dockerfile.md | 2 +- docs/dev/store.md | 2 +- docs/dir.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/auditing_dockerfile.md b/docs/dev/auditing_dockerfile.md index 39fd518a1b0..37034fd981d 100644 --- a/docs/dev/auditing_dockerfile.md +++ b/docs/dev/auditing_dockerfile.md @@ -255,7 +255,7 @@ On a warm cache, it is still over 150MB and 30+ seconds. In and of itself, this is hard to reduce, as we need these... Actions: -- [ ] we could cache the module download location to reduce round-trips on modules that are shared accross +- [ ] we could cache the module download location to reduce round-trips on modules that are shared across different projects - [ ] we are likely installing nerdctl modules six times - (once per architecture during the build phase, then once per ubuntu version and architecture during the tests runs (this is not even accounted for in the audit above)) - it should diff --git a/docs/dev/store.md b/docs/dev/store.md index c0954fb0063..a4bd3a9b20b 100644 --- a/docs/dev/store.md +++ b/docs/dev/store.md @@ -23,7 +23,7 @@ containers can be named the same), etc. However, storing data on the filesystem in a reliable way comes with challenges: - incomplete writes may happen (because of a system restart, or an application crash), leaving important structured files in a broken state -- concurrent writes, or reading while writing would obviously be a problem as well, be it accross goroutines, or between +- concurrent writes, or reading while writing would obviously be a problem as well, be it across goroutines, or between concurrent executions of the nerdctl binary, or embedded in a third-party application that does concurrently access resources The `pkg/store` package does provide a "storage" abstraction that takes care of these issues, generally providing diff --git a/docs/dir.md b/docs/dir.md index 4843eadb6bc..b3350cddabc 100644 --- a/docs/dir.md +++ b/docs/dir.md @@ -65,7 +65,7 @@ Data volume Can be overridden with `nerdctl --cni-netconfpath=` flag and environment variable `$NETCONFPATH`. -At the top-level of , network (files) are shared accross all namespaces. +At the top-level of , network (files) are shared across all namespaces. Sub-folders inside are only available to the namespace bearing the same name, and its networks definitions are private. From 21e112424d92cd4cd88ec9c3316e882aaac218db Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 6 Jun 2025 13:38:33 -0700 Subject: [PATCH 066/868] Move from os filesystem operations to internal/filesystem Signed-off-by: apostasie --- pkg/buildkitutil/buildkitutil_test.go | 22 ++-- pkg/cmd/builder/build.go | 3 +- pkg/cmd/compose/compose.go | 3 +- pkg/cmd/container/create.go | 3 +- pkg/cmd/image/pull.go | 3 +- pkg/cmd/image/push.go | 3 +- pkg/cmd/ipfs/registry_serve.go | 3 +- .../serviceparser/serviceparser_test.go | 4 +- .../container_network_manager.go | 3 +- pkg/dnsutil/hostsstore/hostsstore.go | 16 ++- .../credentialsstore_test.go | 9 +- .../dockercompat/dockercompat_test.go | 5 +- pkg/internal/filesystem/consts.go | 9 +- pkg/internal/filesystem/errors.go | 9 +- pkg/internal/filesystem/lock.go | 2 +- pkg/internal/filesystem/os.go | 23 ++++ pkg/internal/filesystem/umask.go | 49 ++++++++ pkg/internal/filesystem/umask_test.go | 95 +++++++++++++++ .../filesystem/{atomic.go => umask_unix.go} | 24 +--- pkg/internal/filesystem/umask_windows.go | 21 ++++ pkg/internal/filesystem/writefile_rename.go | 112 ++++++++++++++++++ pkg/logging/json_logger.go | 3 +- pkg/netutil/netutil_test.go | 3 +- pkg/netutil/store.go | 2 +- pkg/resolvconf/resolvconf.go | 6 +- pkg/store/filestore.go | 2 +- pkg/store/filestore_test.go | 10 +- pkg/testutil/compose.go | 4 +- pkg/testutil/nerdtest/command.go | 5 +- .../testregistry/testregistry_linux.go | 3 +- pkg/testutil/testutil.go | 2 +- 31 files changed, 392 insertions(+), 69 deletions(-) create mode 100644 pkg/internal/filesystem/os.go create mode 100644 pkg/internal/filesystem/umask.go create mode 100644 pkg/internal/filesystem/umask_test.go rename pkg/internal/filesystem/{atomic.go => umask_unix.go} (63%) create mode 100644 pkg/internal/filesystem/umask_windows.go create mode 100644 pkg/internal/filesystem/writefile_rename.go diff --git a/pkg/buildkitutil/buildkitutil_test.go b/pkg/buildkitutil/buildkitutil_test.go index a123bc5f3cd..f55f5dd88c1 100644 --- a/pkg/buildkitutil/buildkitutil_test.go +++ b/pkg/buildkitutil/buildkitutil_test.go @@ -29,6 +29,8 @@ import ( "testing" "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) func TestBuildKitFile(t *testing.T) { @@ -55,7 +57,7 @@ func TestBuildKitFile(t *testing.T) { { name: "only Dockerfile is present", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, DefaultDockerfileName), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, DefaultDockerfileName), []byte{}, 0644) }, args: args{".", ""}, wantAbsDir: tmp, @@ -65,7 +67,7 @@ func TestBuildKitFile(t *testing.T) { { name: "only Containerfile is present", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{}, 0644) }, args: args{".", ""}, wantAbsDir: tmp, @@ -75,11 +77,11 @@ func TestBuildKitFile(t *testing.T) { { name: "both Dockerfile and Containerfile are present", prepare: func(t *testing.T) error { - var err = os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{}, 0644) + var err = filesystem.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{}, 0644) if err != nil { return err } - return os.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{}, 0644) }, args: args{".", ""}, wantAbsDir: tmp, @@ -89,11 +91,11 @@ func TestBuildKitFile(t *testing.T) { { name: "Dockerfile and Containerfile have different contents", prepare: func(t *testing.T) error { - var err = os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{'d'}, 0644) + var err = filesystem.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{'d'}, 0644) if err != nil { return err } - return os.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{'c'}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "Containerfile"), []byte{'c'}, 0644) }, args: args{".", ""}, wantAbsDir: tmp, @@ -103,7 +105,7 @@ func TestBuildKitFile(t *testing.T) { { name: "Custom file is specfied", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, "CustomFile"), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "CustomFile"), []byte{}, 0644) }, args: args{".", "CustomFile"}, wantAbsDir: tmp, @@ -113,7 +115,7 @@ func TestBuildKitFile(t *testing.T) { { name: "Absolute path is specified along with custom file", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, "CustomFile"), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "CustomFile"), []byte{}, 0644) }, args: args{tmp, "CustomFile"}, wantAbsDir: tmp, @@ -123,7 +125,7 @@ func TestBuildKitFile(t *testing.T) { { name: "Absolute path is specified along with Docker file", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, "Dockerfile"), []byte{}, 0644) }, args: args{tmp, "."}, wantAbsDir: tmp, @@ -133,7 +135,7 @@ func TestBuildKitFile(t *testing.T) { { name: "Absolute path is specified with Container file in the path", prepare: func(t *testing.T) error { - return os.WriteFile(filepath.Join(tmp, ContainerfileName), []byte{}, 0644) + return filesystem.WriteFile(filepath.Join(tmp, ContainerfileName), []byte{}, 0644) }, args: args{tmp, "."}, wantAbsDir: tmp, diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index c25287bb441..e9aa654a425 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -41,6 +41,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/strutil" @@ -110,7 +111,7 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder if err != nil { return err } - if err := os.WriteFile(options.IidFile, []byte(id), 0644); err != nil { + if err := filesystem.WriteFile(options.IidFile, []byte(id), 0644); err != nil { return err } } diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index ba6e0868af1..21bed075580 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -34,6 +34,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/composer" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipfs" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" @@ -136,7 +137,7 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op return err } defer os.RemoveAll(dir) - if err := os.WriteFile(filepath.Join(dir, "api"), []byte(ipfsAddress), 0600); err != nil { + if err := filesystem.WriteFile(filepath.Join(dir, "api"), []byte(ipfsAddress), 0600); err != nil { return err } ipfsPath = dir diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index deee011f343..59270de8aea 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -51,6 +51,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/imgutil/load" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/logging" @@ -951,7 +952,7 @@ func generateLogConfig(dataStore string, id string, logDriver string, logOpt []s } logConfigFilePath := logging.LogConfigFilePath(dataStore, ns, id) - if err = os.WriteFile(logConfigFilePath, logConfigB, 0600); err != nil { + if err = filesystem.WriteFile(logConfigFilePath, logConfigB, 0600); err != nil { return logConfig, err } diff --git a/pkg/cmd/image/pull.go b/pkg/cmd/image/pull.go index 1d943c9b62d..848f7179300 100644 --- a/pkg/cmd/image/pull.go +++ b/pkg/cmd/image/pull.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipfs" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/signutil" @@ -62,7 +63,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, return nil, err } defer os.RemoveAll(dir) - if err := os.WriteFile(filepath.Join(dir, "api"), []byte(options.IPFSAddress), 0600); err != nil { + if err := filesystem.WriteFile(filepath.Join(dir, "api"), []byte(options.IPFSAddress), 0600); err != nil { return nil, err } ipfsPath = dir diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go index 0c463e76f02..5c4b9d1272e 100644 --- a/pkg/cmd/image/push.go +++ b/pkg/cmd/image/push.go @@ -46,6 +46,7 @@ import ( nerdconverter "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/imgutil/push" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipfs" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" @@ -85,7 +86,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options return err } defer os.RemoveAll(dir) - if err := os.WriteFile(filepath.Join(dir, "api"), []byte(options.IpfsAddress), 0600); err != nil { + if err := filesystem.WriteFile(filepath.Join(dir, "api"), []byte(options.IpfsAddress), 0600); err != nil { return err } ipfsPath = dir diff --git a/pkg/cmd/ipfs/registry_serve.go b/pkg/cmd/ipfs/registry_serve.go index 09294032c1d..47cd3fc985f 100644 --- a/pkg/cmd/ipfs/registry_serve.go +++ b/pkg/cmd/ipfs/registry_serve.go @@ -24,6 +24,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipfs" ) @@ -35,7 +36,7 @@ func RegistryServe(options types.IPFSRegistryServeOptions) error { return err } defer os.RemoveAll(dir) - if err := os.WriteFile(filepath.Join(dir, "api"), []byte(options.IPFSAddress), 0600); err != nil { + if err := filesystem.WriteFile(filepath.Join(dir, "api"), []byte(options.IPFSAddress), 0600); err != nil { return err } ipfsPath = dir diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index 7d30ad6a875..ee7a704897d 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -18,7 +18,6 @@ package serviceparser import ( "fmt" - "os" "path/filepath" "runtime" "strconv" @@ -27,6 +26,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/testutil" ) @@ -521,7 +521,7 @@ configs: assert.NilError(t, err) for _, f := range []string{"secret1", "secret2", "secret3", "config1", "config2"} { - err = os.WriteFile(filepath.Join(project.WorkingDir, f), []byte("content-"+f), 0444) + err = filesystem.WriteFile(filepath.Join(project.WorkingDir, f), []byte("content-"+f), 0444) assert.NilError(t, err) } diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index 9f082f9ce12..b1e192019fd 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -39,6 +39,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/mountutil" "github.com/containerd/nerdctl/v2/pkg/netutil" @@ -829,7 +830,7 @@ func writeEtcHostnameForContainer(globalOptions types.GlobalCommandOptions, host } hostnamePath := filepath.Join(stateDir, "hostname") - if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { + if err := filesystem.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { return nil, err } diff --git a/pkg/dnsutil/hostsstore/hostsstore.go b/pkg/dnsutil/hostsstore/hostsstore.go index de50043f366..d7fc28b53db 100644 --- a/pkg/dnsutil/hostsstore/hostsstore.go +++ b/pkg/dnsutil/hostsstore/hostsstore.go @@ -40,6 +40,7 @@ import ( "github.com/containerd/errdefs" "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/store" ) @@ -116,11 +117,11 @@ func (x *hostsStore) Acquire(meta Meta) (err error) { // Because of the way we call network manager ContainerNetworkingOpts then SetupNetworking in sequence // we need to make sure we do not overwrite an already allocated hosts file. if _, err = os.Stat(loc); os.IsNotExist(err) { - if err = os.WriteFile(loc, []byte{}, 0o644); err != nil { + if err = filesystem.WriteFile(loc, []byte{}, 0o644); err != nil { return errors.Join(store.ErrSystemFailure, err) } - // os.WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched + // WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched // against the current process umask. // See https://www.man7.org/linux/man-pages/man2/open.2.html for details. // Since we must make sure that these files are world readable, explicitly chmod them here. @@ -185,12 +186,12 @@ func (x *hostsStore) AllocHostsFile(id string, content []byte) (location string, return err } - err = os.WriteFile(loc, content, 0o644) + err = filesystem.WriteFile(loc, content, 0o644) if err != nil { err = errors.Join(store.ErrSystemFailure, err) } - // os.WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched + // WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched // against the current process umask. // See https://www.man7.org/linux/man-pages/man2/open.2.html for details. // Since we must make sure that these files are world readable, explicitly chmod them here. @@ -351,6 +352,13 @@ func (x *hostsStore) updateAllHosts() (err error) { return err } + // Because the file is mounted, we cannot do atomic writes here as that would change inode. + // The practical implications of this are that a partial / interrupted write would leave the hosts file with + // an invalid entry and/or missing entries. At worse, this would lead to a container losing localhost network + // capabilities. + // Proper consistency requires that we would have a rollback mechanism in case of recoverable failure, + // and a disaster management / cleanup mechanism, presumably at the top-level of the operation. + // nolint:forbidigo err = os.WriteFile(loc, buf.Bytes(), 0o644) if err != nil { log.L.WithError(err).Errorf("failed to write hosts file for %q", entry) diff --git a/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go b/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go index 61b99d87d8d..5dedacc82c2 100644 --- a/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go +++ b/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go @@ -26,6 +26,7 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -92,7 +93,7 @@ func TestBrokenCredentialsStore(t *testing.T) { description: "Pointing DOCKER_CONFIG at a directory containing am unparsable `config.json` will prevent instantiation", setup: func() string { tmpDir := createTempDir(t, 0700) - err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("porked"), 0600) + err := filesystem.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("porked"), 0600) if err != nil { t.Fatal(err) } @@ -143,7 +144,7 @@ func TestBrokenCredentialsStore(t *testing.T) { description: "Pointing DOCKER_CONFIG at a directory containing an unreadable, valid `config.json` file will prevent instantiation", setup: func() string { tmpDir := createTempDir(t, 0700) - err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600) + err := filesystem.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600) if err != nil { t.Fatal(err) } @@ -159,7 +160,7 @@ func TestBrokenCredentialsStore(t *testing.T) { description: "Pointing DOCKER_CONFIG at a directory containing a read-only, valid `config.json` file will NOT prevent saving credentials", setup: func() string { tmpDir := createTempDir(t, 0700) - err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600) + err := filesystem.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600) if err != nil { t.Fatal(err) } @@ -215,7 +216,7 @@ func TestBrokenCredentialsStore(t *testing.T) { func writeContent(t *testing.T, content string) string { t.Helper() tmpDir := createTempDir(t, 0700) - err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte(content), 0600) + err := filesystem.WriteFile(filepath.Join(tmpDir, "config.json"), []byte(content), 0600) if err != nil { t.Fatal(err) } diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index a31286bff36..e271217610c 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) func TestContainerFromNative(t *testing.T) { @@ -38,7 +39,7 @@ func TestContainerFromNative(t *testing.T) { if err != nil { t.Fatal(err) } - os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) + filesystem.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) defer os.RemoveAll(tempStateDir) testcase := []struct { @@ -313,7 +314,7 @@ func TestNetworkSettingsFromNative(t *testing.T) { if err != nil { t.Fatal(err) } - os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) + filesystem.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) defer os.RemoveAll(tempStateDir) testcase := []struct { diff --git a/pkg/internal/filesystem/consts.go b/pkg/internal/filesystem/consts.go index b17236822d2..c23112c3fd1 100644 --- a/pkg/internal/filesystem/consts.go +++ b/pkg/internal/filesystem/consts.go @@ -16,7 +16,14 @@ package filesystem +import "io" + const ( - lockPermission = 0o600 pathComponentMaxLength = 255 + privateFilePermission = 0o600 +) + +var ( + // Lightweight indirection to ease testing + ioCopy = io.Copy ) diff --git a/pkg/internal/filesystem/errors.go b/pkg/internal/filesystem/errors.go index 311f0c719bf..88c38119927 100644 --- a/pkg/internal/filesystem/errors.go +++ b/pkg/internal/filesystem/errors.go @@ -19,8 +19,9 @@ package filesystem import "errors" var ( - ErrLockFail = errors.New("failed to acquire lock") - ErrUnlockFail = errors.New("failed to release lock") - ErrLockIsNil = errors.New("nil lock") - ErrInvalidPath = errors.New("invalid path") + ErrLockFail = errors.New("failed to acquire lock") + ErrUnlockFail = errors.New("failed to release lock") + ErrLockIsNil = errors.New("nil lock") + ErrInvalidPath = errors.New("invalid path") + ErrFilesystemFailure = errors.New("filesystem error") ) diff --git a/pkg/internal/filesystem/lock.go b/pkg/internal/filesystem/lock.go index 82339c20a58..847e403883d 100644 --- a/pkg/internal/filesystem/lock.go +++ b/pkg/internal/filesystem/lock.go @@ -67,7 +67,7 @@ func commonlock(path string, mode lockType) (file *os.File, err error) { file, err = os.Open(path) if errors.Is(err, os.ErrNotExist) { - file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, lockPermission) + file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, privateFilePermission) } if err != nil { diff --git a/pkg/internal/filesystem/os.go b/pkg/internal/filesystem/os.go new file mode 100644 index 00000000000..12764795b62 --- /dev/null +++ b/pkg/internal/filesystem/os.go @@ -0,0 +1,23 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filesystem + +import "os" + +func WriteFile(filename string, data []byte, perm os.FileMode) error { + return WriteFileWithRename(filename, data, perm) +} diff --git a/pkg/internal/filesystem/umask.go b/pkg/internal/filesystem/umask.go new file mode 100644 index 00000000000..d3a69c7fdb1 --- /dev/null +++ b/pkg/internal/filesystem/umask.go @@ -0,0 +1,49 @@ +/* + 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 filesystem + +import ( + "math" + "sync" +) + +var ( + mu sync.Mutex + cMask = -1 +) + +// GetUmask retrieves the current umask. +func GetUmask() uint32 { + if cMask != -1 { + return uint32(cMask) + } + + mu.Lock() + defer mu.Unlock() + + cMask = umask(0) + + // FIXME: one day... we will get rid of 32 bits arm... + cMask64 := int64(cMask) + if cMask64 > math.MaxUint32 || cMask < 0 { + panic("currently set user umask is out of range") + } + + _ = umask(cMask) + + return uint32(cMask) +} diff --git a/pkg/internal/filesystem/umask_test.go b/pkg/internal/filesystem/umask_test.go new file mode 100644 index 00000000000..7b07ff68841 --- /dev/null +++ b/pkg/internal/filesystem/umask_test.go @@ -0,0 +1,95 @@ +/* + 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 filesystem_test + +import ( + "fmt" + "os/exec" + "runtime" + "strconv" + "strings" + "sync/atomic" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" +) + +func TestUmask(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("windows does not have a unix-style umask") + } + + userHostReportedUmask, err := exec.Command("sh", "-c", "umask").CombinedOutput() + assert.NilError(t, err, fmt.Sprintf( + "umask command should succeed (output: %s)", + userHostReportedUmask, + )) + expectedUmask, err := strconv.ParseInt(strings.TrimSpace(string(userHostReportedUmask)), 8, 0) + assert.NilError( + t, + err, + fmt.Sprintf("umask command should have returned parsable output (was: %s)", userHostReportedUmask), + ) + + userMask := filesystem.GetUmask() + assert.Equal(t, expectedUmask, int64(userMask), "system reported umask and implementation umask are the same") + + userHostReportedUmask, err = exec.Command("sh", "-c", "umask").CombinedOutput() + assert.NilError(t, err) + expectedUmask, err = strconv.ParseInt(strings.TrimSpace(string(userHostReportedUmask)), 8, 0) + assert.NilError(t, err) + + assert.Equal(t, expectedUmask, int64(userMask), "system reported umask has not changed") +} + +func TestUmaskConcurrent(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("windows does not have a unix-style umask") + } + + userHostReportedUmask, err := exec.Command("sh", "-c", "umask").Output() + assert.NilError(t, err) + expectedUmask, err := strconv.ParseInt(strings.TrimSpace(string(userHostReportedUmask)), 8, 0) + assert.NilError(t, err) + + var counter int32 = 100 + + ch := make(chan uint32) + + for range counter { + go func(ch chan uint32) { + u := filesystem.GetUmask() + if atomic.AddInt32(&counter, -1) == 0 { + ch <- u + } + }(ch) + } + + ret := <-ch + assert.Equal(t, expectedUmask, int64(ret)) + userHostReportedUmask, err = exec.Command("sh", "-c", "umask").Output() + assert.NilError(t, err) + newUmask, err := strconv.ParseInt(strings.TrimSpace(string(userHostReportedUmask)), 8, 0) + assert.NilError(t, err) + assert.Equal(t, newUmask, expectedUmask, "system reported umask has not changed") +} diff --git a/pkg/internal/filesystem/atomic.go b/pkg/internal/filesystem/umask_unix.go similarity index 63% rename from pkg/internal/filesystem/atomic.go rename to pkg/internal/filesystem/umask_unix.go index fc45def648c..89dc6d87076 100644 --- a/pkg/internal/filesystem/atomic.go +++ b/pkg/internal/filesystem/umask_unix.go @@ -1,3 +1,5 @@ +//go:build unix + /* Copyright The containerd Authors. @@ -16,24 +18,8 @@ package filesystem -import ( - "os" - "path/filepath" -) - -func AtomicWrite(parent string, fileName string, perm os.FileMode, data []byte) error { - dest := filepath.Join(parent, fileName) - temp := filepath.Join(parent, ".temp."+fileName) - - err := os.WriteFile(temp, data, perm) - if err != nil { - return err - } - - err = os.Rename(temp, dest) - if err != nil { - return err - } +import "syscall" - return nil +func umask(mask int) int { + return syscall.Umask(mask) } diff --git a/pkg/internal/filesystem/umask_windows.go b/pkg/internal/filesystem/umask_windows.go new file mode 100644 index 00000000000..dadaec0abb3 --- /dev/null +++ b/pkg/internal/filesystem/umask_windows.go @@ -0,0 +1,21 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filesystem + +func umask(_ int) int { + return 0 +} diff --git a/pkg/internal/filesystem/writefile_rename.go b/pkg/internal/filesystem/writefile_rename.go new file mode 100644 index 00000000000..8a317139bd0 --- /dev/null +++ b/pkg/internal/filesystem/writefile_rename.go @@ -0,0 +1,112 @@ +/* + 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 filesystem + +import ( + "bytes" + "errors" + "io" + "os" + "path/filepath" + "time" +) + +// WriteFileWithRename is a drop-in replacement for os.WriteFile, with the same signature and almost identical behavior +// (see note below on inodes). +// Unlike os.WriteFile, it does provide extra guarantees: +// - Atomicity (provided by rename - *mostly* atomic, except on OS crash, where rename behavior is undefined) +// - Durability (sync-ed) +// Note that: +// - this does not provide Isolation (a locking mechanism needs to be used independently to enforce that) +// - Consistency is orthogonal here, and high-level operations that expect it across a set of unrelated ops need to +// implement locking, rollback, and disaster recovery +// - this will change inode in case the file already exist - therefore, there are cases where this cannot be used +// (specifically if a file is mounted inside a container) - these are the exception though, and in almost all cases, +// this method should be preferred over os.WriteFile +// Finally note that we do not do anything smart wrt symlinks. +// User is expected to resolve symlink for the destination before calling this if needed. +func WriteFileWithRename(filename string, data []byte, perm os.FileMode) error { + return CopyToFileWithRename(filename, bytes.NewBuffer(data), int64(len(data)), perm, time.Time{}) +} + +// CopyToFileWithRename is an atomic wrapper around io.Copy(file, reader). See notes above in WriteFile for details. +func CopyToFileWithRename(filename string, reader io.Reader, dataSize int64, perm os.FileMode, mTime time.Time) (err error) { + var tmpFile *os.File + mustClose := true + + defer func() { + // Close if we have not already + if mustClose { + err = errors.Join(err, tmpFile.Close()) + } + + // On error, wrap it into ErrFilesystemFailure (and ensure we don't leak temp files) + if err != nil { + if tmpFile != nil { + err = errors.Join(err, os.Remove(tmpFile.Name())) + } + err = errors.Join(ErrFilesystemFailure, err) + } + }() + + // Ensure we set permission honoring umask to be compatible with os.WriteFile + perm = (^os.FileMode(GetUmask())) & perm + + // Create a new temp file. + tmpFile, err = os.CreateTemp(filepath.Dir(filename), ".tmp-"+filepath.Base(filename)) + if err != nil { + return err + } + + // Set permissions + if err = os.Chmod(tmpFile.Name(), perm); err != nil { + return err + } + + // Write data + n, err := ioCopy(tmpFile, reader) + if err == nil && n < dataSize { + return io.ErrShortWrite + } + + if err != nil { + return err + } + + // Sync it, ensuring the data cannot be lost + if err = tmpFile.Sync(); err != nil { + return err + } + + // Close + if err = tmpFile.Close(); err != nil { + return err + } + + mustClose = false + + // Set mtime if requested + if !mTime.IsZero() { + if err = os.Chtimes(tmpFile.Name(), mTime, mTime); err != nil { + return err + } + } + + // Rename to final destination (hopefully on the same volume) + // NOTE: this is atomic in *most* cases - it might not be if the OS crashes. + return os.Rename(tmpFile.Name(), filename) +} diff --git a/pkg/logging/json_logger.go b/pkg/logging/json_logger.go index d715d0158ee..7e2dca196fd 100644 --- a/pkg/logging/json_logger.go +++ b/pkg/logging/json_logger.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/containerd/v2/core/runtime/v2/logging" "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/logging/jsonfile" "github.com/containerd/nerdctl/v2/pkg/logging/tail" "github.com/containerd/nerdctl/v2/pkg/strutil" @@ -72,7 +73,7 @@ func (jsonLogger *JSONLogger) Init(dataStore, ns, id string) error { return err } if _, err := os.Stat(jsonFilePath); errors.Is(err, os.ErrNotExist) { - if writeErr := os.WriteFile(jsonFilePath, []byte{}, 0600); writeErr != nil { + if writeErr := filesystem.WriteFile(jsonFilePath, []byte{}, 0600); writeErr != nil { return writeErr } } diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index c3dadc74360..4c5459e706d 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -30,6 +30,7 @@ import ( "gotest.tools/v3/assert" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/testutil" ) @@ -329,7 +330,7 @@ func TestNetworkWithDefaultNameAlreadyExists(t *testing.T) { // Filename is irrelevant as long as it's not nerdctl's. testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", testutil.Identifier(t))) - err = os.WriteFile(testConfFile, buf.Bytes(), 0600) + err = filesystem.WriteFile(testConfFile, buf.Bytes(), 0600) assert.NilError(t, err) // Check network is detected. diff --git a/pkg/netutil/store.go b/pkg/netutil/store.go index c1fbe3744bd..7376b3510c5 100644 --- a/pkg/netutil/store.go +++ b/pkg/netutil/store.go @@ -66,7 +66,7 @@ func fsWrite(e *CNIEnv, net *NetworkConfig) error { if _, err := os.Stat(filename); err == nil { return errdefs.ErrAlreadyExists } - return os.WriteFile(filename, net.Bytes, 0644) + return filesystem.WriteFile(filename, net.Bytes, 0644) }) } diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index 79bec3ecd9e..82968afca54 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -36,6 +36,8 @@ import ( "sync" "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) const ( @@ -317,12 +319,12 @@ func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) { return nil, err } - err = os.WriteFile(path, content.Bytes(), 0o644) + err = filesystem.WriteFile(path, content.Bytes(), 0o644) if err != nil { return nil, err } - // os.WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched + // WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched // against the current process umask. // See https://www.man7.org/linux/man-pages/man2/open.2.html for details. // Since we must make sure that these files are world readable, explicitly chmod them here. diff --git a/pkg/store/filestore.go b/pkg/store/filestore.go index 2ad3982eb4b..0de4e06df5b 100644 --- a/pkg/store/filestore.go +++ b/pkg/store/filestore.go @@ -193,7 +193,7 @@ func (vs *fileStore) Set(data []byte, key ...string) error { } } - if err := filesystem.AtomicWrite(parent, fileName, vs.filePerm, data); err != nil { + if err := filesystem.WriteFileWithRename(filepath.Join(parent, fileName), data, vs.filePerm); err != nil { return errors.Join(ErrSystemFailure, err) } diff --git a/pkg/store/filestore_test.go b/pkg/store/filestore_test.go index 2496b96cbb1..ac7c7c8b5cd 100644 --- a/pkg/store/filestore_test.go +++ b/pkg/store/filestore_test.go @@ -21,6 +21,8 @@ import ( "time" "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) func TestFileStoreBasics(t *testing.T) { @@ -60,16 +62,16 @@ func TestFileStoreBasics(t *testing.T) { // Invalid keys _, err = tempStore.Get("..") - assert.ErrorIs(t, err, ErrInvalidArgument, "unsupported characters or patterns should return ErrInvalidArgument") + assert.ErrorIs(t, err, filesystem.ErrInvalidPath, "unsupported characters or patterns should return filesystem.ErrInvalidPath") err = tempStore.Set([]byte("foo"), "..") - assert.ErrorIs(t, err, ErrInvalidArgument, "unsupported characters or patterns should return ErrInvalidArgument") + assert.ErrorIs(t, err, filesystem.ErrInvalidPath, "unsupported characters or patterns should return filesystem.ErrInvalidPath") err = tempStore.Delete("..") - assert.ErrorIs(t, err, ErrInvalidArgument, "unsupported characters or patterns should return ErrInvalidArgument") + assert.ErrorIs(t, err, filesystem.ErrInvalidPath, "unsupported characters or patterns should return filesystem.ErrInvalidPath") _, err = tempStore.List("..") - assert.ErrorIs(t, err, ErrInvalidArgument, "unsupported characters or patterns should return ErrInvalidArgument") + assert.ErrorIs(t, err, filesystem.ErrInvalidPath, "unsupported characters or patterns should return filesystem.ErrInvalidPath") // Writing, reading, listing, deleting err = tempStore.Set([]byte("foo"), "something") diff --git a/pkg/testutil/compose.go b/pkg/testutil/compose.go index 2e5b55b056d..a432486ebd9 100644 --- a/pkg/testutil/compose.go +++ b/pkg/testutil/compose.go @@ -24,6 +24,8 @@ import ( "github.com/compose-spec/compose-go/v2/loader" compose "github.com/compose-spec/compose-go/v2/types" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) type ComposeDir struct { @@ -33,7 +35,7 @@ type ComposeDir struct { } func (cd *ComposeDir) WriteFile(name, content string) { - if err := os.WriteFile(filepath.Join(cd.dir, name), []byte(content), 0644); err != nil { + if err := filesystem.WriteFile(filepath.Join(cd.dir, name), []byte(content), 0644); err != nil { cd.t.Fatal(err) } } diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go index e886514933f..4f0eca550d1 100644 --- a/pkg/testutil/nerdtest/command.go +++ b/pkg/testutil/nerdtest/command.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" @@ -126,7 +127,7 @@ func (nc *nerdCommand) prep() { if customDCConfig := nc.GenericCommand.Config.Read(DockerConfig); customDCConfig != "" { if !nc.hasWrittenDockerConfig { dest := filepath.Join(nc.Env["DOCKER_CONFIG"], "config.json") - err := os.WriteFile(dest, []byte(customDCConfig), test.FilePermissionsDefault) + err := filesystem.WriteFile(dest, []byte(customDCConfig), test.FilePermissionsDefault) assert.NilError(nc.T(), err, "failed to write custom docker config json file for test") nc.hasWrittenDockerConfig = true } @@ -175,7 +176,7 @@ func (nc *nerdCommand) prep() { if nc.Config.Read(NerdctlToml) != "" { if !nc.hasWrittenToml { dest := nc.Env["NERDCTL_TOML"] - err := os.WriteFile(dest, []byte(nc.Config.Read(NerdctlToml)), test.FilePermissionsDefault) + err := filesystem.WriteFile(dest, []byte(nc.Config.Read(NerdctlToml)), test.FilePermissionsDefault) assert.NilError(nc.T(), err, "failed to write NerdctlToml") nc.hasWrittenToml = true } diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go index d6610f9046a..c1c3f3f8643 100644 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ b/pkg/testutil/testregistry/testregistry_linux.go @@ -26,6 +26,7 @@ import ( "golang.org/x/crypto/bcrypt" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" @@ -234,7 +235,7 @@ func (ba *BasicAuth) Params(base *testutil.Base) []string { encryptedPass, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) tmpDir, _ := os.MkdirTemp(base.T.TempDir(), "htpasswd") ba.HtFile = filepath.Join(tmpDir, "htpasswd") - _ = os.WriteFile(ba.HtFile, []byte(fmt.Sprintf(`%s:%s`, ba.Username, string(encryptedPass[:]))), 0600) + _ = filesystem.WriteFile(ba.HtFile, []byte(fmt.Sprintf(`%s:%s`, ba.Username, string(encryptedPass[:]))), 0600) } ret := []string{ "--env", "REGISTRY_AUTH=htpasswd", diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 14d322de07b..0e3f3740fd0 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -534,7 +534,7 @@ func M(m *testing.M) { defer filesystem.Unlock(lock) // Create marker file - err = os.WriteFile(testLockFile, []byte("prevent testing from running in parallel for subpackages integration tests"), 0o666) + err = filesystem.WriteFile(testLockFile, []byte("prevent testing from running in parallel for subpackages integration tests"), 0o666) if err != nil { log.L.WithError(err).Errorf("failed writing lock file %q", testLockFile) return 1 From f3dc0eee52087d3c11c4a6ae91e2e9eb2f1303c1 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 6 Jun 2025 13:39:17 -0700 Subject: [PATCH 067/868] Forbidigo: prevent os fs operations Signed-off-by: apostasie --- .golangci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 7338764f2b6..483315a995c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,6 +31,7 @@ linters: - revive # Gocritic - gocritic + - forbidigo # 3. We used to use these, but have now removed them @@ -41,6 +42,12 @@ linters: # - nakedret settings: + forbidigo: + forbid: + # FIXME: there are still calls to os.WriteFile in tests under `cmd` + - pattern: ^os\.WriteFile.*$ + pkg: github.com/containerd/nerdctl/v2/pkg + msg: os.WriteFile is neither atomic nor durable - use nerdctl filesystem.WriteFile instead staticcheck: checks: # Below is the default set From e2db1e1e01b03c303c55e283ba1a1b084290e3c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:12:33 +0000 Subject: [PATCH 068/868] build(deps): bump docker/setup-buildx-action from 3.10.0 to 3.11.1 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.10.0 to 3.11.1. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2...e468171a9de216ec08956ac3ada2f0791b6bd435) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 3.11.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 63c971ca12b..9ede0bdfd05 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -38,7 +38,7 @@ jobs: uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 # Login against a Docker registry except on PR # https://github.com/docker/login-action From 98aa61891b3612301438ea4e9e8a78cdad6a60d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 03:30:53 +0000 Subject: [PATCH 069/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 99f9540b184..557abcd86e8 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.1 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.1.2 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -141,7 +141,7 @@ require ( golang.org/x/mod v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect //gomodjail:unconfined - google.golang.org/grpc v1.72.0 // indirect + google.golang.org/grpc v1.72.2 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.6 // indirect lukechampine.com/blake3 v1.3.0 // indirect diff --git a/go.sum b/go.sum index e550cf29f3f..35f0f85a807 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM= -github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU= +github.com/containerd/containerd/v2 v2.1.2 h1:4ZQxB+FVYmwXZgpBcKfar6ieppm3KC5C6FRKvtJ6DRU= +github.com/containerd/containerd/v2 v2.1.2/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -484,8 +484,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac 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.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 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= From f07305205826c2dc0941a9c4b394a87d5c9dd25a Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Thu, 19 Jun 2025 16:16:35 +0100 Subject: [PATCH 070/868] feat: allow regexp name filters to match docker Signed-off-by: Justin Chadwell --- .../container/container_list_linux_test.go | 36 +++++++++++++++++++ .../network/network_list_linux_test.go | 22 ++++++++++++ cmd/nerdctl/volume/volume_list_test.go | 21 +++++++++++ pkg/cmd/container/list_util.go | 7 +++- pkg/cmd/network/list.go | 19 +++++----- pkg/cmd/volume/list.go | 19 +++++----- 6 files changed, 105 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index e7ce1c92e11..29687730d6a 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -304,6 +304,42 @@ func TestContainerListWithFilter(t *testing.T) { return nil }) + // should support regexp + base.Cmd("ps", "--filter", "name=.*"+testContainerA.name+".*").AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) < 2 { + return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) + } + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + if err != nil { + return fmt.Errorf("failed to parse header: %v", err) + } + + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, testContainerA.name) + return nil + }) + + // fully anchored regexp + base.Cmd("ps", "--filter", "name=^"+testContainerA.name+"$").AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) < 2 { + return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) + } + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + if err != nil { + return fmt.Errorf("failed to parse header: %v", err) + } + + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, testContainerA.name) + return nil + }) + base.Cmd("ps", "-q", "--filter", "name="+testContainerA.name+testContainerA.name).AssertOutWithFunc(func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") if len(lines) > 0 { diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index 3bf6f9e912f..cb6583139b5 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -81,6 +81,28 @@ func TestNetworkLsFilter(t *testing.T) { data.Labels().Get("netID2")[:12]: {}, } + for _, name := range lines { + _, ok := netNames[name] + assert.Assert(t, ok, info) + } + }, + } + }, + }, + { + Description: "filter name regexp", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "ls", "--quiet", "--filter", "name=.*"+data.Labels().Get("net2")+".*") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, info) + netNames := map[string]struct{}{ + data.Labels().Get("netID2")[:12]: {}, + } + for _, name := range lines { _, ok := netNames[name] assert.Assert(t, ok, info) diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index 8dca19fa584..8535f55b562 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -280,6 +280,27 @@ func TestVolumeLsFilter(t *testing.T) { } }, }, + { + Description: "Retrieving name=.*volume1.*", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "name=.*"+data.Labels().Get("vol1")+".*") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) + volNames := map[string]struct{}{ + data.Labels().Get("vol1"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, + }, { Description: "Retrieving name=volume1 and name=volume2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { diff --git a/pkg/cmd/container/list_util.go b/pkg/cmd/container/list_util.go index da63106efd5..a2fdb0a2892 100644 --- a/pkg/cmd/container/list_util.go +++ b/pkg/cmd/container/list_util.go @@ -19,6 +19,7 @@ package container import ( "context" "fmt" + "regexp" "strconv" "strings" "time" @@ -164,11 +165,15 @@ func (cl *containerFilterContext) foldIDFilter(_ context.Context, filter, value } func (cl *containerFilterContext) foldNameFilter(_ context.Context, filter, value string) error { + re, err := regexp.Compile(value) + if err != nil { + return err + } cl.nameFilterFuncs = append(cl.nameFilterFuncs, func(name string) bool { if value == "" { return true } - return strings.Contains(name, value) + return re.MatchString(name) }) return nil } diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go index 731c51b1b99..d27333a3321 100644 --- a/pkg/cmd/network/list.go +++ b/pkg/cmd/network/list.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" "text/tabwriter" "text/template" @@ -147,21 +148,21 @@ func getNetworkFilterFuncs(filters []string) ([]func(*map[string]string) bool, [ for _, filter := range filters { if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") { - subs := strings.SplitN(filter, "=", 2) - if len(subs) < 2 { + filter, value, ok := strings.Cut(filter, "=") + if !ok { continue } - switch subs[0] { + switch filter { case "name": + re, err := regexp.Compile(value) + if err != nil { + return nil, nil, err + } nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { - return strings.Contains(name, subs[1]) + return re.MatchString(name) }) case "label": - v, k, hasValue := "", subs[1], false - if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 { - hasValue = true - k, v = subs[0], subs[1] - } + k, v, hasValue := strings.Cut(value, "=") labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { if labels == nil { return false diff --git a/pkg/cmd/volume/list.go b/pkg/cmd/volume/list.go index bb0654ba5b2..126b65b9045 100644 --- a/pkg/cmd/volume/list.go +++ b/pkg/cmd/volume/list.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "regexp" "strconv" "strings" "text/tabwriter" @@ -216,21 +217,21 @@ func getVolumeFilterFuncs(filters []string) ([]func(*map[string]string) bool, [] } for _, filter := range filters { if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") { - subs := strings.SplitN(filter, "=", 2) - if len(subs) < 2 { + filter, value, ok := strings.Cut(filter, "=") + if !ok { continue } - switch subs[0] { + switch filter { case "name": + re, err := regexp.Compile(value) + if err != nil { + return nil, nil, nil, false, err + } nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { - return strings.Contains(name, subs[1]) + return re.MatchString(name) }) case "label": - v, k, hasValue := "", subs[1], false - if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 { - hasValue = true - k, v = subs[0], subs[1] - } + k, v, hasValue := strings.Cut(value, "=") labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { if labels == nil { return false From c459113113d4f991e458394f166a74e643710d9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:15:41 +0000 Subject: [PATCH 071/868] build(deps): bump actions/attest-build-provenance from 2.3.0 to 2.4.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/db473fddc028af60658334401dc6fa3ffd8669fd...e8998f949152b193b063cb0ec769d69d929409be) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b68c6395047..1881d185058 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From c5805c5bedfe5c399e20512946e0db83bfc340db Mon Sep 17 00:00:00 2001 From: Subash Kotha Date: Thu, 19 Jun 2025 10:29:16 -0700 Subject: [PATCH 072/868] feat: Add healthcheck command in nerdctl Signed-off-by: Subash Kotha --- cmd/nerdctl/container/container.go | 1 + cmd/nerdctl/container/container_create.go | 34 ++ .../container/container_health_check.go | 85 +++ .../container/container_health_check_test.go | 556 ++++++++++++++++++ cmd/nerdctl/container/container_run.go | 9 + cmd/nerdctl/container/container_run_test.go | 220 +++++++ cmd/nerdctl/helpers/flagutil.go | 33 ++ cmd/nerdctl/main.go | 1 + docs/command-reference.md | 12 +- docs/healthchecks.md | 51 ++ pkg/api/types/container_types.go | 9 + pkg/cmd/container/create.go | 73 +++ pkg/cmd/container/health_check.go | 96 +++ pkg/healthcheck/executor.go | 200 +++++++ pkg/healthcheck/health.go | 139 +++++ pkg/healthcheck/log.go | 255 ++++++++ pkg/imgutil/imgutil.go | 34 ++ pkg/inspecttypes/dockercompat/dockercompat.go | 38 +- .../dockercompat/dockercompat_test.go | 153 +++++ pkg/internal/filesystem/lock.go | 42 +- pkg/labels/labels.go | 6 + 21 files changed, 2032 insertions(+), 15 deletions(-) create mode 100644 cmd/nerdctl/container/container_health_check.go create mode 100644 cmd/nerdctl/container/container_health_check_test.go create mode 100644 docs/healthchecks.md create mode 100644 pkg/cmd/container/health_check.go create mode 100644 pkg/healthcheck/executor.go create mode 100644 pkg/healthcheck/health.go create mode 100644 pkg/healthcheck/log.go diff --git a/cmd/nerdctl/container/container.go b/cmd/nerdctl/container/container.go index 2d92f8d5922..6188e7013a0 100644 --- a/cmd/nerdctl/container/container.go +++ b/cmd/nerdctl/container/container.go @@ -54,6 +54,7 @@ func Command() *cobra.Command { pruneCommand(), StatsCommand(), AttachCommand(), + HealthCheckCommand(), ) AddCpCommand(cmd) return cmd diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index 62ceb96a299..83f377eb245 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -258,6 +258,40 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { } // #endregion + // #region for healthcheck flags + opt.HealthCmd, err = cmd.Flags().GetString("health-cmd") + if err != nil { + return opt, err + } + opt.HealthInterval, err = cmd.Flags().GetDuration("health-interval") + if err != nil { + return opt, err + } + opt.HealthTimeout, err = cmd.Flags().GetDuration("health-timeout") + if err != nil { + return opt, err + } + opt.HealthRetries, err = cmd.Flags().GetInt("health-retries") + if err != nil { + return opt, err + } + opt.HealthStartPeriod, err = cmd.Flags().GetDuration("health-start-period") + if err != nil { + return opt, err + } + opt.HealthStartInterval, err = cmd.Flags().GetDuration("health-start-interval") + if err != nil { + return opt, err + } + opt.NoHealthcheck, err = cmd.Flags().GetBool("no-healthcheck") + if err != nil { + return opt, err + } + if err := helpers.ValidateHealthcheckFlags(opt); err != nil { + return opt, err + } + // #endregion + // #region for intel RDT flags opt.RDTClass, err = cmd.Flags().GetString("rdt-class") if err != nil { diff --git a/cmd/nerdctl/container/container_health_check.go b/cmd/nerdctl/container/container_health_check.go new file mode 100644 index 00000000000..abc0337168d --- /dev/null +++ b/cmd/nerdctl/container/container_health_check.go @@ -0,0 +1,85 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/container" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +// HealthCheckCommand returns a cobra command for `nerdctl container healthcheck` +func HealthCheckCommand() *cobra.Command { + var healthCheckCommand = &cobra.Command{ + Use: "healthcheck [flags] CONTAINER", + Short: "Execute the health check command in a container", + Args: cobra.ExactArgs(1), + RunE: healthCheckAction, + ValidArgsFunction: healthCheckShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + + return healthCheckCommand +} + +func healthCheckAction(cmd *cobra.Command, args []string) error { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return err + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) + if err != nil { + return err + } + defer cancel() + + containerID := args[0] + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + return container.HealthCheck(ctx, client, found.Container) + }, + } + + n, err := walker.Walk(ctx, containerID) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("no such container %s", containerID) + } + return nil +} + +func healthCheckShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ContainerNames(cmd, func(status containerd.ProcessStatus) bool { + return status == containerd.Running + }) +} diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go new file mode 100644 index 00000000000..66d60e30705 --- /dev/null +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -0,0 +1,556 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "errors" + "strings" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/healthcheck" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestContainerHealthCheckBasic(t *testing.T) { + testCase := nerdtest.Setup() + + // Docker CLI does not provide a standalone healthcheck command. + testCase.Require = require.Not(nerdtest.Docker) + + testCase.SubTests = []*test.Case{ + { + Description: "Container does not exist", + Command: test.Command("container", "healthcheck", "non-existent"), + Expected: test.Expects(1, []error{errors.New("no such container non-existent")}, nil), + }, + { + Description: "Missing health check config", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: test.Expects(1, []error{errors.New("container has no health check configured")}, nil), + }, + { + Description: "Basic health check success", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "45s", + "--health-timeout", "30s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state to be present") + assert.Equal(t, healthcheck.Healthy, h.Status) + assert.Equal(t, 0, h.FailingStreak) + assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") + }), + } + }, + }, + { + Description: "Health check on stopped container", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "3s", + testutil.CommonImage, "sleep", "2") + helpers.Ensure("stop", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "healthcheck", data.Identifier()) + }, + Expected: test.Expects(1, []error{errors.New("container is not running (status: stopped)")}, nil), + }, + { + Description: "Health check without task", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: test.Expects(1, []error{errors.New("failed to get container task: no running task found")}, nil), + }, + } + + testCase.Run(t) +} + +func TestContainerHealthCheckAdvance(t *testing.T) { + testCase := nerdtest.Setup() + + // Docker CLI does not provide a standalone healthcheck command. + testCase.Require = require.Not(nerdtest.Docker) + + testCase.SubTests = []*test.Case{ + { + Description: "Health check timeout scenario", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "sleep 10", + "--health-timeout", "2s", + "--health-interval", "1s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.FailingStreak, 1) + assert.Assert(t, len(inspect.State.Health.Log) > 0, "expected health log to have entries") + last := inspect.State.Health.Log[0] + assert.Equal(t, -1, last.ExitCode) + }), + } + }, + }, + { + Description: "Health check failing streak behavior", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "exit 1", + "--health-interval", "1s", + "--health-retries", "2", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Run healthcheck twice to ensure failing streak + for i := 0; i < 2; i++ { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(2 * time.Second) + } + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Unhealthy) + assert.Equal(t, h.FailingStreak, 2) + }), + } + }, + }, + { + Description: "Health check with start period", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "exit 1", + "--health-interval", "1s", + "--health-start-period", "5s", + "--health-retries", "2", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Starting) + assert.Equal(t, h.FailingStreak, 0) + }), + } + }, + }, + { + Description: "Health check with invalid command", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "not-a-real-cmd", + "--health-interval", "1s", + "--health-retries", "1", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Unhealthy) + assert.Equal(t, h.FailingStreak, 1) + }), + } + }, + }, + { + Description: "No healthcheck flag disables health status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--no-healthcheck", testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Assert(t, inspect.State.Health == nil, "expected health to be nil with --no-healthcheck") + }), + } + }, + }, + { + Description: "Healthcheck using CMD-SHELL format", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo shell-format", "--health-interval", "1s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(_, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy) + assert.Assert(t, len(h.Log) > 0) + assert.Assert(t, strings.Contains(h.Log[0].Output, "shell-format")) + }), + } + }, + }, + { + Description: "Health check uses container environment variables", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--env", "MYVAR=test-value", + "--health-cmd", "echo $MYVAR", + "--health-interval", "1s", + "--health-timeout", "1s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy) + assert.Assert(t, h.FailingStreak == 0) + assert.Assert(t, strings.Contains(h.Log[0].Output, "test"), "expected health log output to contain 'test'") + }), + } + }, + }, + { + Description: "Health check respects container WorkingDir", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--workdir", "/tmp", + "--health-cmd", "pwd", + "--health-interval", "1s", + "--health-timeout", "1s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy) + assert.Equal(t, h.FailingStreak, 0) + assert.Assert(t, strings.Contains(h.Log[0].Output, "/tmp"), "expected health log output to contain '/tmp'") + }), + } + }, + }, + { + Description: "Healthcheck emits large output repeatedly", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "yes X | head -c 60000", + "--health-interval", "1s", "--health-timeout", "2s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + for i := 0; i < 3; i++ { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(2 * time.Second) + } + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(_, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy) + assert.Assert(t, len(h.Log) >= 3, "expected at least 3 health log entries") + for _, log := range h.Log { + assert.Assert(t, len(log.Output) >= 1024, "each output should be >= 1024 bytes") + } + }), + } + }, + }, + { + Description: "Health log in inspect keeps only the latest 5 entries", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "exit 1", + "--health-interval", "1s", + "--health-retries", "1", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + for i := 0; i < 7; i++ { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(1 * time.Second) + } + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(_, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Unhealthy) + assert.Assert(t, len(h.Log) <= 5, "expected health log to contain at most 5 entries") + }), + } + }, + }, + { + Description: "Healthcheck with large output gets truncated in health log", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "yes X | head -c 1048576", // 1MB output + "--health-interval", "1s", "--health-timeout", "2s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("container", "healthcheck", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(_, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy) + assert.Equal(t, h.FailingStreak, 0) + assert.Assert(t, len(h.Log) == 1, "expected one log entry") + output := h.Log[0].Output + assert.Assert(t, strings.HasSuffix(output, "[truncated]"), "expected output to be truncated with '[truncated]'") + }), + } + }, + }, + { + Description: "Health status transitions from healthy to unhealthy after retries", + Setup: func(data test.Data, helpers test.Helpers) { + containerName := data.Identifier() + helpers.Ensure("run", "-d", "--name", containerName, + "--health-cmd", "exit 1", + "--health-timeout", "10s", + "--health-retries", "3", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + for i := 0; i < 4; i++ { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(2 * time.Second) + } + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Unhealthy) + assert.Assert(t, h.FailingStreak >= 3) + }), + } + }, + }, + { + Description: "Failed healthchecks in start-period do not change status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "ls /foo || exit 1", "--health-retries", "2", + "--health-start-period", "30s", // long enough to stay in "starting" + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Run healthcheck 3 times (should still be in start period) + for i := 0; i < 3; i++ { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(1 * time.Second) + } + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Starting) + assert.Equal(t, h.FailingStreak, 0, "failing streak should not increase during start period") + }), + } + }, + }, + { + Description: "Successful healthcheck in start-period sets status to healthy", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "ls || exit 1", "--health-retries", "2", + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("container", "healthcheck", data.Identifier()) + time.Sleep(1 * time.Second) + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout, _ string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state") + assert.Equal(t, h.Status, healthcheck.Healthy, "expected healthy status even during start-period") + assert.Equal(t, h.FailingStreak, 0) + }), + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index be629b7eb2f..39f1004aeda 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -234,6 +234,15 @@ func setCreateFlags(cmd *cobra.Command) { // rootfs flags (from Podman) cmd.Flags().Bool("rootfs", false, "The first argument is not an image but the rootfs to the exploded container") + // 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-start-period", 0, "Start period for the container to initialize before starting health-retries countdown") + cmd.Flags().Duration("health-start-interval", 0, "Time between running the checks during the start period") + cmd.Flags().Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK") + // #region env flags // entrypoint needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} // entrypoint StringArray is an internal implementation to support `nerdctl compose` entrypoint yaml filed with multiple strings diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 345523f1382..b621c8522ef 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -837,3 +837,223 @@ func TestRunDomainname(t *testing.T) { }) } } + +func TestRunHealthcheckFlags(t *testing.T) { + testCase := nerdtest.Setup() + + testCases := []struct { + name string + args []string + shouldFail bool + expectTest []string + expectRetries int + expectInterval time.Duration + expectTimeout time.Duration + expectStartPeriod time.Duration + }{ + { + name: "Valid_full_config", + args: []string{ + "--health-cmd", "curl -f http://localhost || exit 1", + "--health-interval", "30s", + "--health-timeout", "5s", + "--health-retries", "3", + "--health-start-period", "2s", + }, + expectTest: []string{"CMD-SHELL", "curl -f http://localhost || exit 1"}, + expectInterval: 30 * time.Second, + expectTimeout: 5 * time.Second, + expectRetries: 3, + expectStartPeriod: 2 * time.Second, + }, + { + name: "No_healthcheck", + args: []string{ + "--no-healthcheck", + }, + expectTest: []string{"NONE"}, + }, + { + name: "No_healthcheck_flag", + args: []string{}, + expectTest: nil, + }, + { + name: "Conflicting_flags", + args: []string{ + "--no-healthcheck", "--health-cmd", "true", + }, + shouldFail: true, + }, + { + name: "Negative_retries", + args: []string{ + "--health-cmd", "true", + "--health-retries", "-2", + }, + shouldFail: true, + }, + { + name: "Negative_timeout", + args: []string{ + "--health-cmd", "true", + "--health-timeout", "-5s", + }, + shouldFail: true, + }, + { + name: "Invalid_timeout_format", + args: []string{ + "--health-cmd", "true", + "--health-timeout", "5blah", + }, + shouldFail: true, + }, + { + name: "Health_cmd_cmd_shell", + args: []string{ + "--health-cmd", "curl -f http://localhost || exit 1", + }, + expectTest: []string{"CMD-SHELL", "curl -f http://localhost || exit 1"}, + }, + { + name: "Health_cmd_array_like", + args: []string{ + "--health-cmd", "echo hello", + }, + expectTest: []string{"CMD-SHELL", "echo hello"}, + }, + { + name: "Health_cmd_empty", + args: []string{ + "--health-cmd", "", + "--health-retries", "2", + }, + expectTest: nil, + expectRetries: 2, + }, + } + + 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 { + args := append([]string{"run", "-d", "--name", tc.name}, tc.args...) + args = append(args, testutil.CommonImage, "sleep", "infinity") + return helpers.Command(args...) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if tc.shouldFail { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + } + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + func(stdout, info string, t *testing.T) { + inspect := nerdtest.InspectContainer(helpers, tc.name) + hc := inspect.Config.Healthcheck + if tc.expectTest == nil { + assert.Assert(t, hc == nil || len(hc.Test) == 0) + } else { + assert.Assert(t, hc != nil) + assert.DeepEqual(t, hc.Test, tc.expectTest) + } + if tc.expectRetries > 0 { + assert.Equal(t, hc.Retries, tc.expectRetries) + } + if tc.expectTimeout > 0 { + assert.Equal(t, hc.Timeout, tc.expectTimeout) + } + if tc.expectInterval > 0 { + assert.Equal(t, hc.Interval, tc.expectInterval) + } + if tc.expectStartPeriod > 0 { + assert.Equal(t, hc.StartPeriod, tc.expectStartPeriod) + } + }, + ), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", tc.name) + }, + }) + } + + testCase.Run(t) +} + +func TestRunHealthcheckFromImage(t *testing.T) { + 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()) + }, + 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 *testing.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"), + ) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All(func(stdout, _ string, t *testing.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()) + }, + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 4f9261c0cf3..9c4e8c018c4 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -46,6 +46,39 @@ func VerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions, err error) return } +func ValidateHealthcheckFlags(options types.ContainerCreateOptions) error { + healthFlagsSet := + options.HealthInterval != 0 || + options.HealthTimeout != 0 || + options.HealthRetries != 0 || + options.HealthStartPeriod != 0 || + options.HealthStartInterval != 0 + + if options.NoHealthcheck { + if options.HealthCmd != "" || healthFlagsSet { + return fmt.Errorf("--no-healthcheck conflicts with --health-* options") + } + } + + // Note: HealthCmd can be empty with other healthcheck flags set cause healthCmd could be coming from image. + if options.HealthInterval < 0 { + return fmt.Errorf("--health-interval cannot be negative") + } + if options.HealthTimeout < 0 { + return fmt.Errorf("--health-timeout cannot be negative") + } + if options.HealthRetries < 0 { + return fmt.Errorf("--health-retries cannot be negative") + } + if options.HealthStartPeriod < 0 { + return fmt.Errorf("--health-start-period cannot be negative") + } + if options.HealthStartInterval < 0 { + return fmt.Errorf("--health-start-interval cannot be negative") + } + return nil +} + func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) { debug, err := cmd.Flags().GetBool("debug") if err != nil { diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 88d753a170c..d04f54bd08a 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -287,6 +287,7 @@ Config file ($NERDCTL_TOML): %s container.WaitCommand(), container.RenameCommand(), container.AttachCommand(), + container.HealthCheckCommand(), // #endregion // Build diff --git a/docs/command-reference.md b/docs/command-reference.md index a4173d8f93a..8b10bd4bf57 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -321,6 +321,16 @@ Metadata flags: - :whale: :blue_square: `--cidfile`: Write the container ID to the file - :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention. +Health check flags: + +- :whale: :blue_square: `--health-cmd`: Command to run to check container health +- :whale: :blue_square: `--health-interval`: Time between running the check (e.g., 30s, 1m) +- :whale: :blue_square: `--health-timeout`: Time to wait before considering the check failed (e.g., 5s) +- :whale: :blue_square: `--health-retries`: Number of failures before container is considered unhealthy +- :whale: :blue_square: `--health-start-period`: Start period for the container to initialize before starting health-retries countdown +- :whale: :blue_square: `--health-start-interval`: Interval between checks during the start period +- :whale: :blue_square: `--no-healthcheck`: Disable any health checks defined by image or CLI + Logging flags: - :whale: `--log-driver=(json-file|journald|fluentd|syslog|none)`: Logging driver for the container (default `json-file`). @@ -428,7 +438,7 @@ IPFS flags: - :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`) Unimplemented `docker run` flags: - `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`, + `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, `--isolation`, `--link*`, `--publish-all`, `--storage-opt`, `--volume-driver` ### :whale: :blue_square: nerdctl exec diff --git a/docs/healthchecks.md b/docs/healthchecks.md new file mode 100644 index 00000000000..b47c92748b5 --- /dev/null +++ b/docs/healthchecks.md @@ -0,0 +1,51 @@ +# Health Check Support in nerdctl + +`nerdctl` supports Docker-compatible health checks for containers, allowing users to monitor container health via a user-defined command. + +Currently, health checks can be triggered manually using the nerdctl container healthcheck command. Automatic orchestration (e.g., periodic checks) will be added in a future update. + +Health checks can be configured in multiple ways: + +1. At container creation time using nerdctl run or nerdctl create with `--health-*` flags +2. At image build time using HEALTHCHECK in a Dockerfile +3. In docker-compose.yaml files, if using nerdctl compose + +When a container is created, nerdctl determines the health check configuration based on the following priority: + +1. **CLI flags** take highest precedence (e.g., `--health-cmd`, etc.) +2. If no CLI flags are set, nerdctl will use any health check defined in the image. +3. If neither is present, no health check will be configured + +Example: + +```bash +nerdctl run --name web --health-cmd="curl -f http://localhost || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 nginx +``` + +### Disabling Health Checks + +You can disable health checks using the following flag during container create/run: + +```bash +--no-healthcheck +``` + +### Running Health Checks Manually + +nerdctl provides a container healthcheck command that can be manually triggered by the user. This command runs the +configured health check inside the container and reports the result. It serves as the entry point for executing +health checks, especially in scenarios where external scheduling is used. + +Example: + +``` +nerdctl container healthcheck +``` + +### Future Work (WIP) + +Since nerdctl is daemonless and does not have a persistent background process, we rely on systemd(or external schedulers) +to invoke nerdctl container healthcheck at configured intervals. This allows periodic health checks for containers in a +systemd-based environment. We are actively working on automating health checks, where we will listen to container lifecycle +events and generate appropriate systemd service and timer units. This will enable nerdctl to support automated, +Docker-compatible health checks by leveraging systemd for scheduling and lifecycle integration. \ No newline at end of file diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 7489d449672..7765c2393c2 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -284,6 +284,15 @@ type ContainerCreateOptions struct { // ImagePullOpt specifies image pull options which holds the ImageVerifyOptions for verifying the image. ImagePullOpt ImagePullOptions + // Healthcheck related fields + HealthCmd string + HealthInterval time.Duration + HealthTimeout time.Duration + HealthRetries int + HealthStartPeriod time.Duration + HealthStartInterval time.Duration + NoHealthcheck bool + // UserNS name for user namespace mapping of container UserNS string } diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 59270de8aea..76b0ee24137 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -25,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "strconv" "strings" @@ -47,6 +48,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/v2/pkg/flagutil" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idgen" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/imgutil/load" @@ -333,6 +335,15 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } cOpts = append(cOpts, rtCOpts...) + // Generate health check config based on CLI flags and image. + healthcheckConfig, err := withHealthcheck(options, ensuredImage) + if err != nil { + return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err + } + if healthcheckConfig != "" { + internalLabels.healthcheck = healthcheckConfig + } + lCOpts, err := withContainerLabels(options.Label, options.LabelFile, ensuredImage) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err @@ -706,6 +717,8 @@ type internalLabels struct { deviceMapping []dockercompat.DeviceMapping user string + + healthcheck string } // WithInternalLabels sets the internal labels for a container. @@ -829,9 +842,69 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.User] = internalLabels.user } + if len(internalLabels.healthcheck) > 0 { + m[labels.HealthCheck] = internalLabels.healthcheck + } + return containerd.WithAdditionalContainerLabels(m), nil } +func withHealthcheck(options types.ContainerCreateOptions, ensuredImage *imgutil.EnsuredImage) (string, error) { + // If explicitly disabled + if options.NoHealthcheck { + hc := &healthcheck.Healthcheck{ + Test: []string{"NONE"}, + } + hcJSON, err := hc.ToJSONString() + if err != nil { + return "", fmt.Errorf("failed to serialize disabled healthcheck config: %w", err) + } + return hcJSON, nil + } + + // Start with health checks in image if present + hc := &healthcheck.Healthcheck{} + if ensuredImage != nil && ensuredImage.ImageConfig.Labels != nil { + if label := ensuredImage.ImageConfig.Labels[labels.HealthCheck]; label != "" { + parsed, err := healthcheck.HealthCheckFromJSON(label) + if err != nil { + return "", fmt.Errorf("failed to parse healthcheck label in image: %w", err) + } + hc = parsed + } + } + + // Apply CLI overrides + if options.HealthCmd != "" { + hc.Test = []string{"CMD-SHELL", options.HealthCmd} + } + if options.HealthInterval != 0 { + hc.Interval = options.HealthInterval + } + if options.HealthTimeout != 0 { + hc.Timeout = options.HealthTimeout + } + if options.HealthRetries != 0 { + hc.Retries = options.HealthRetries + } + if options.HealthStartPeriod != 0 { + hc.StartPeriod = options.HealthStartPeriod + } + if options.HealthStartInterval != 0 { + hc.StartInterval = options.HealthStartInterval + } + + // If no healthcheck config is set (via CLI or image), return empty string so we skip adding to container config. + if reflect.DeepEqual(hc, &healthcheck.Healthcheck{}) { + return "", nil + } + hcJSON, err := hc.ToJSONString() + if err != nil { + return "", fmt.Errorf("failed to serialize healthcheck config: %w", err) + } + return hcJSON, nil +} + // loadNetOpts loads network options into InternalLabels. func (il *internalLabels) loadNetOpts(opts types.NetworkOptions) { il.hostname = opts.Hostname diff --git a/pkg/cmd/container/health_check.go b/pkg/cmd/container/health_check.go new file mode 100644 index 00000000000..6fe31c8ebc3 --- /dev/null +++ b/pkg/cmd/container/health_check.go @@ -0,0 +1,96 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "fmt" + "time" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/healthcheck" + "github.com/containerd/nerdctl/v2/pkg/labels" +) + +// HealthCheck executes the health check command for a container +func HealthCheck(ctx context.Context, client *containerd.Client, container containerd.Container) error { + // verify container status and get task + task, err := isContainerRunning(ctx, container) + if err != nil { + return err + } + + // Check if container has health check configured + info, err := container.Info(ctx) + if err != nil { + return fmt.Errorf("failed to get container info: %w", err) + } + hcConfigJSON, ok := info.Labels[labels.HealthCheck] + if !ok { + return fmt.Errorf("container has no health check configured") + } + + // Parse health check configuration from labels + var hcConfig *healthcheck.Healthcheck + hcConfig, err = healthcheck.HealthCheckFromJSON(hcConfigJSON) + if err != nil { + return fmt.Errorf("invalid health check configuration: %w", err) + } + if hcConfig.Test == nil { + return fmt.Errorf("health check configuration has no test") + } + + // Populate defaults + hcConfig.Interval = timeoutWithDefault(hcConfig.Interval, healthcheck.DefaultProbeInterval) + hcConfig.Timeout = timeoutWithDefault(hcConfig.Timeout, healthcheck.DefaultProbeTimeout) + hcConfig.StartPeriod = timeoutWithDefault(hcConfig.StartPeriod, healthcheck.DefaultStartPeriod) + hcConfig.StartInterval = timeoutWithDefault(hcConfig.StartInterval, healthcheck.DefaultStartInterval) + if hcConfig.Retries == 0 { + hcConfig.Retries = healthcheck.DefaultProbeRetries + } + + // Execute the health check + return healthcheck.ExecuteHealthCheck(ctx, task, container, hcConfig) +} + +func isContainerRunning(ctx context.Context, container containerd.Container) (containerd.Task, error) { + // Get container task to check status + task, err := container.Task(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get container task: %w", err) + } + + // Check if container is running + status, err := task.Status(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get container status: %w", err) + } + if status.Status != containerd.Running { + return nil, fmt.Errorf("container is not running (status: %s)", status.Status) + } + + return task, nil +} + +// If configuredValue is zero, use defaultValue instead. +func timeoutWithDefault(configuredValue time.Duration, defaultValue time.Duration) time.Duration { + if configuredValue == 0 { + return defaultValue + } + return configuredValue +} diff --git a/pkg/healthcheck/executor.go b/pkg/healthcheck/executor.go new file mode 100644 index 00000000000..1b3f16a7460 --- /dev/null +++ b/pkg/healthcheck/executor.go @@ -0,0 +1,200 @@ +/* + 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 healthcheck + +import ( + "context" + "fmt" + "strings" + "syscall" + "time" + + "github.com/opencontainers/runtime-spec/specs-go" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/pkg/cio" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/idgen" +) + +// ExecuteHealthCheck executes the health check command for a container +func ExecuteHealthCheck(ctx context.Context, task containerd.Task, container containerd.Container, hc *Healthcheck) error { + // Prepare process spec for health check command + processSpec, err := prepareProcessSpec(ctx, container, hc) + if err != nil { + return err + } + if processSpec == nil { + return nil + } + + startTime := time.Now() + result, err := probeHealthCheck(ctx, task, hc, processSpec) + if err != nil { + _ = updateHealthStatus(ctx, container, hc, &HealthcheckResult{ + Start: startTime, + End: time.Now(), + ExitCode: -1, + Output: err.Error(), + }) + return fmt.Errorf("health check probe failed: %w", err) + } + + // Success case, update health status + result.Start = startTime + if err := updateHealthStatus(ctx, container, hc, result); err != nil { + return fmt.Errorf("failed to update health status: %w", err) + } + return nil +} + +// probeHealthCheck executes the health check command inside the container context +func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck, processSpec *specs.Process) (*HealthcheckResult, error) { + execID := "health-check-" + idgen.TruncateID(idgen.GenerateID()) + outputBuf := NewResizableBuffer(MaxOutputLen) + + process, err := task.Exec(ctx, execID, processSpec, cio.NewCreator( + cio.WithStreams(nil, outputBuf, outputBuf), + )) + if err != nil { + log.G(ctx).Debugf("failed to exec health check: %v", err) + return nil, fmt.Errorf("exec error: %w", err) + } + + if err := process.Start(ctx); err != nil { + log.G(ctx).Debugf("failed to start health check: %v", err) + return nil, fmt.Errorf("start error: %w", err) + } + + exitStatusC, err := process.Wait(ctx) + if err != nil { + return nil, fmt.Errorf("failed to wait for health check: %w", err) + } + + select { + case <-time.After(hc.Timeout): + _ = process.Kill(ctx, syscall.SIGKILL) + go func() { <-exitStatusC }() + + msg := fmt.Sprintf("Health check exceeded timeout (%v)", hc.Timeout) + if out := outputBuf.String(); len(out) > 0 { + msg = fmt.Sprintf("Health check exceeded timeout (%v): %s", hc.Timeout, out) + } + + log.G(ctx).Debugf("health check timed out: %s", msg) + + return &HealthcheckResult{ + ExitCode: -1, + Output: msg, + End: time.Now(), + }, nil + + case exitStatus := <-exitStatusC: + code, _, _ := exitStatus.Result() + return &HealthcheckResult{ + ExitCode: int(code), + Output: outputBuf.String(), + End: time.Now(), + }, nil + } +} + +// updateHealthStatus updates the health status based on the health check result +func updateHealthStatus(ctx context.Context, container containerd.Container, hcConfig *Healthcheck, hcResult *HealthcheckResult) error { + // Get current health state from labels + currentHealth, err := readHealthStateFromLabels(ctx, container) + if err != nil { + return fmt.Errorf("failed to read health state from labels: %w", err) + } + if currentHealth == nil { + currentHealth = &HealthState{ + Status: Starting, + FailingStreak: 0, + } + } + + // Check if still within start period + startPeriod := hcConfig.StartPeriod + info, err := container.Info(ctx) + if err != nil { + return fmt.Errorf("failed to get container info: %w", err) + } + containerCreated := info.CreatedAt + stillInStartPeriod := hcResult.Start.Sub(containerCreated) < startPeriod + + // Update health status based on exit code + if hcResult.ExitCode == 0 { + currentHealth.Status = Healthy + currentHealth.FailingStreak = 0 + } else if !stillInStartPeriod { + currentHealth.FailingStreak++ + if currentHealth.FailingStreak >= hcConfig.Retries { + currentHealth.Status = Unhealthy + } + } + + // Write updated health state back to labels + if err := writeHealthStateToLabels(ctx, container, currentHealth); err != nil { + return fmt.Errorf("failed to write health state to labels: %w", err) + } + + // Store the latest health check result in the log file + if err := writeHealthLog(ctx, container, hcResult); err != nil { + return fmt.Errorf("failed to write health log: %w", err) + } + return nil +} + +// prepareProcessSpec prepares the process spec for health check execution +func prepareProcessSpec(ctx context.Context, container containerd.Container, hcConfig *Healthcheck) (*specs.Process, error) { + hcCommand := hcConfig.Test + + var args []string + switch hcCommand[0] { + case TestNone, CmdNone: + log.G(ctx).Debug("health check is set to NONE, skipping execution") + return nil, nil + case Cmd: + args = hcCommand[1:] + case CmdShell: + if len(hcCommand) < 2 || strings.TrimSpace(hcCommand[1]) == "" { + return nil, fmt.Errorf("no health check command specified") + } + args = []string{"/bin/sh", "-c", strings.Join(hcCommand[1:], " ")} + default: + args = hcCommand + } + + if len(args) < 1 || args[0] == "" { + return nil, fmt.Errorf("no health check command specified") + } + + // Get container spec for environment and working directory + spec, err := container.Spec(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get container spec: %w", err) + } + processSpec := &specs.Process{ + Args: args, + Env: spec.Process.Env, + User: spec.Process.User, + Cwd: spec.Process.Cwd, + } + + return processSpec, nil +} diff --git a/pkg/healthcheck/health.go b/pkg/healthcheck/health.go new file mode 100644 index 00000000000..8e0301b492a --- /dev/null +++ b/pkg/healthcheck/health.go @@ -0,0 +1,139 @@ +/* + 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 healthcheck + +import ( + "encoding/json" + "time" +) + +type HealthStatus = string + +// Health states +const ( + NoHealthcheck HealthStatus = "none" // Indicates there is no healthcheck + Starting HealthStatus = "starting" + Healthy HealthStatus = "healthy" + Unhealthy HealthStatus = "unhealthy" +) + +// Healthcheck cmd types +const ( + CmdNone = "NONE" + Cmd = "CMD" + CmdShell = "CMD-SHELL" + TestNone = "" +) + +const ( + DefaultProbeInterval = 30 * time.Second // Default interval between probe runs. Also applies before the first probe. + DefaultProbeTimeout = 30 * time.Second // Max duration a single probe run may take before it's considered failed. + DefaultStartPeriod = 0 * time.Second // Grace period for container startup before health checks count as failures. + DefaultStartInterval = 5 * time.Second // Interval between checks during the start period. + DefaultProbeRetries = 3 // Number of consecutive failures before marking container as unhealthy. + MaxLogEntries = 5 // Maximum number of health check log entries to keep. + MaxOutputLenForInspect = 4096 // Max output length (in bytes) stored in health check logs during inspect. Longer outputs are truncated. + MaxOutputLen = 1 * 1024 * 1024 // Max output size for health check logs: 1MB limit (prevents excessive memory usage) + HealthLogFilename = "health.json" // HealthLogFilename is the name of the file used to persist health check status for a container. +) + +// NOTE: Health, HealthcheckResult and Healthcheck types are kept Docker-compatible. +// See: https://github.com/moby/moby/blob/9d1b069a4bfdcee368e67767978eff596b696d4c/api/types/container/health.go +// Health stores information about the container's healthcheck results +type Health struct { + Status HealthStatus // Status is one of [Starting], [Healthy] or [Unhealthy]. + FailingStreak int // FailingStreak is the number of consecutive failures + Log []*HealthcheckResult // Log contains the last few results (oldest first) +} + +// HealthcheckResult stores information about a single run of a healthcheck probe +type HealthcheckResult struct { + Start time.Time // Start is the time this check started + End time.Time // End is the time this check ended + ExitCode int // ExitCode meanings: 0=healthy, 1=unhealthy, 2=reserved (considered unhealthy), else=error running probe + Output string // Output from last check +} + +// Healthcheck represents the health check configuration +type Healthcheck struct { + Test []string `json:"Test,omitempty"` // Test is the check to perform that the container is healthy + Interval time.Duration `json:"Interval,omitempty"` // Interval is the time to wait between checks + Timeout time.Duration `json:"Timeout,omitempty"` // Timeout is the time to wait before considering the check to have hung + Retries int `json:"Retries,omitempty"` // Retries is the number of consecutive failures needed to consider a container as unhealthy + StartPeriod time.Duration `json:"StartPeriod,omitempty"` // StartPeriod is the period for the container to initialize before the health check starts + StartInterval time.Duration `json:"StartInterval,omitempty"` // StartInterval is the time between health checks during the start period +} + +// HealthState stores the current health state of a container +type HealthState struct { + Status HealthStatus // Status is one of [Starting], [Healthy] or [Unhealthy] + FailingStreak int // FailingStreak is the number of consecutive failures +} + +// ToJSONString serializes HealthState to a JSON string for label storage +func (hs *HealthState) ToJSONString() (string, error) { + b, err := json.Marshal(hs) + if err != nil { + return "", err + } + return string(b), nil +} + +// HealthStateFromJSON deserializes a JSON string into a HealthState +func HealthStateFromJSON(s string) (*HealthState, error) { + var hs HealthState + if err := json.Unmarshal([]byte(s), &hs); err != nil { + return nil, err + } + return &hs, nil +} + +// ToJSONString serializes a Healthcheck struct to a JSON string +func (hc *Healthcheck) ToJSONString() (string, error) { + b, err := json.Marshal(hc) + if err != nil { + return "", err + } + return string(b), nil +} + +// HealthCheckFromJSON deserializes a JSON string into a Healthcheck struct +func HealthCheckFromJSON(s string) (*Healthcheck, error) { + var hc Healthcheck + if err := json.Unmarshal([]byte(s), &hc); err != nil { + return nil, err + } + return &hc, nil +} + +// ToJSONString serializes a HealthcheckResult struct to a JSON string +func (r *HealthcheckResult) ToJSONString() (string, error) { + b, err := json.Marshal(r) + if err != nil { + return "", err + } + return string(b), nil +} + +// HealthcheckResultFromJSON deserializes a JSON string into a HealthcheckResult struct +func HealthcheckResultFromJSON(s string) (*HealthcheckResult, error) { + var r HealthcheckResult + if err := json.Unmarshal([]byte(s), &r); err != nil { + return nil, err + } + return &r, nil +} diff --git a/pkg/healthcheck/log.go b/pkg/healthcheck/log.go new file mode 100644 index 00000000000..1664b502671 --- /dev/null +++ b/pkg/healthcheck/log.go @@ -0,0 +1,255 @@ +/* + 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 healthcheck + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" + "github.com/containerd/nerdctl/v2/pkg/labels" +) + +// writeHealthLog writes the latest health check result to the log file, appending it to existing logs. +func writeHealthLog(ctx context.Context, container containerd.Container, result *HealthcheckResult) error { + stateDir, err := getContainerStateDir(ctx, container) + if err != nil { + return fmt.Errorf("error fetching container state dir: %v", err) + } + + data, err := result.ToJSONString() + if err != nil { + return fmt.Errorf("failed to marshal health log: %w", err) + } + + // Ensure file exists before writing + if err := ensureHealthLogFile(stateDir); err != nil { + return err + } + + // Write the latest result to the file + logPath := filepath.Join(stateDir, HealthLogFilename) + return filesystem.WithAppendLock(logPath, func(file *os.File) error { + if _, err := file.Seek(0, io.SeekEnd); err != nil { + return fmt.Errorf("seek error: %w", err) + } + if _, err := file.Write(append([]byte(data), '\n')); err != nil { + return fmt.Errorf("failed to write health log: %w", err) + } + return nil + }) +} + +// ReadHealthStatusForInspect reads the health state from labels and the last MaxLogEntries health check result logs. +func ReadHealthStatusForInspect(stateDir, healthState string) (*Health, error) { + state, err := HealthStateFromJSON(healthState) + if err != nil { + return nil, fmt.Errorf("failed to parse health state: %w", err) + } + + logPath := filepath.Join(stateDir, HealthLogFilename) + var logs []*HealthcheckResult + err = filesystem.WithReadOnlyLock(logPath, func() error { + file, err := os.Open(logPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer file.Close() + + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + + line = strings.TrimRight(line, "\n") + result, err := HealthcheckResultFromJSON(line) + if err != nil { + log.L.Warnf("failed to parse healthcheck log line: %v", err) + continue + } + logs = append(logs, result) + } + return nil + }) + if err != nil { + return nil, err + } + + // Keep only the last MaxLogEntries + n := len(logs) + if n > MaxLogEntries { + logs = logs[n-MaxLogEntries:] + } + + // Reverse for newest-first order + for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 { + logs[i], logs[j] = logs[j], logs[i] + } + + // Truncate log outputs to avoid flooding inspect output + for _, logEntry := range logs { + if len(logEntry.Output) > MaxOutputLenForInspect { + buf := NewResizableBuffer(MaxOutputLenForInspect) + _, _ = buf.Write([]byte(logEntry.Output)) + logEntry.Output = buf.String() + } + } + + // Create a Health object with the health state and logs + health := &Health{ + Status: state.Status, + FailingStreak: state.FailingStreak, + Log: logs, + } + + return health, nil +} + +// writeHealthStateToLabels writes the health state to container labels +func writeHealthStateToLabels(ctx context.Context, container containerd.Container, healthState *HealthState) error { + hs, err := healthState.ToJSONString() + if err != nil { + return fmt.Errorf("failed to marshal health healthState: %w", err) + } + + lbs, err := container.Labels(ctx) + if err != nil { + return fmt.Errorf("failed to get container labels: %w", err) + } + + // Update healthState label + lbs[labels.HealthState] = hs + _, err = container.SetLabels(ctx, lbs) + if err != nil { + return fmt.Errorf("failed to update container labels: %w", err) + } + + return nil +} + +// readHealthStateFromLabels reads the health state from container labels +func readHealthStateFromLabels(ctx context.Context, container containerd.Container) (*HealthState, error) { + lbs, err := container.Labels(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get container labels: %w", err) + } + + // Check if health state label exists + stateJSON, ok := lbs[labels.HealthState] + if !ok { + return nil, nil + } + + // HealthCheckFromJSON health state from JSON + state, err := HealthStateFromJSON(stateJSON) + if err != nil { + return nil, fmt.Errorf("failed to parse health state: %w", err) + } + + return state, nil +} + +// ensureHealthLogFile creates the health.json file if it doesn't exist. +func ensureHealthLogFile(stateDir string) error { + healthLogPath := filepath.Join(stateDir, HealthLogFilename) + + // Ensure container state directory exists + if _, err := os.Stat(stateDir); os.IsNotExist(err) { + return fmt.Errorf("container state directory does not exist: %s", stateDir) + } + + // Create health.json if it doesn't exist + if _, err := os.Stat(healthLogPath); os.IsNotExist(err) { + return filesystem.WriteFile(healthLogPath, []byte{}, 0600) + } + + return nil +} + +// getContainerStateDir returns the container's state directory from labels. +func getContainerStateDir(ctx context.Context, container containerd.Container) (string, error) { + info, err := container.Info(ctx) + if err != nil { + return "", err + } + stateDir, ok := info.Labels[labels.StateDir] + if !ok { + return "", err + } + return stateDir, nil +} + +// ResizableBuffer collects output with a configurable upper limit. +type ResizableBuffer struct { + mu sync.Mutex + buf bytes.Buffer + maxSize int + truncated bool +} + +// NewResizableBuffer returns a new buffer with the given size limit in bytes. +func NewResizableBuffer(maxSize int) *ResizableBuffer { + return &ResizableBuffer{maxSize: maxSize} +} + +func (b *ResizableBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + + remaining := b.maxSize - b.buf.Len() + if remaining <= 0 { + b.truncated = true + return len(p), nil + } + + if len(p) > remaining { + b.truncated = true + p = p[:remaining] + } + + return b.buf.Write(p) +} + +func (b *ResizableBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + + s := b.buf.String() + if b.truncated { + s += "... [truncated]" + } + return s +} diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index 3f8076df9f4..08d10be6437 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -40,9 +40,11 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/errutil" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/imgutil/pull" + "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) @@ -271,6 +273,10 @@ func getImageConfig(ctx context.Context, image containerd.Image) (*ocispec.Image if err := json.Unmarshal(b, &ocispecImage); err != nil { return nil, err } + + if err := addHealthCheckToImageConfig(b, &ocispecImage.Config); err != nil { + log.G(ctx).WithError(err).Debug("failed to add health check config") + } return &ocispecImage.Config, nil default: return nil, fmt.Errorf("unknown media type %q", desc.MediaType) @@ -354,6 +360,9 @@ func ReadImageConfig(ctx context.Context, img containerd.Image) (ocispec.Image, if err := json.Unmarshal(p, &config); err != nil { return config, configDesc, err } + if err := addHealthCheckToImageConfig(p, &config.Config); err != nil { + log.G(ctx).WithError(err).Debug("failed to add health check config") + } return config, configDesc, nil } @@ -464,3 +473,28 @@ func GetDanglingImages(ctx context.Context, client *containerd.Client, filters . return ApplyFilters(allImages, filters...) } + +// addHealthCheckToImageConfig extracts health check information from the image content store and adds it to the labels +func addHealthCheckToImageConfig(rawConfigContent []byte, config *ocispec.ImageConfig) error { + var imgConfig struct { + Config struct { + Healthcheck *healthcheck.Healthcheck `json:"Healthcheck,omitempty"` + } `json:"config"` + } + + if err := json.Unmarshal(rawConfigContent, &imgConfig); err != nil { + return err + } + + if imgConfig.Config.Healthcheck != nil { + healthCheckJSON, err := json.Marshal(imgConfig.Config.Healthcheck) + if err != nil { + return err + } + if config.Labels == nil { + config.Labels = make(map[string]string) + } + config.Labels[labels.HealthCheck] = string(healthCheckJSON) + } + return nil +} diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 43321456543..fbcf57d0c75 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -44,6 +44,7 @@ import ( "github.com/containerd/go-cni" "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/ipcutil" @@ -210,9 +211,9 @@ type Config struct { // TODO: Tty bool // Attach standard streams to a tty, including stdin if it is not closed. // TODO: OpenStdin bool // Open stdin // TODO: StdinOnce bool // If true, close stdin after the 1 attached client disconnects. - Env []string `json:",omitempty"` // List of environment variable to set in the container - Cmd []string `json:",omitempty"` // Command to run when starting the container - // TODO Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy + Env []string `json:",omitempty"` // List of environment variable to set in the container + Cmd []string `json:",omitempty"` // Command to run when starting the container + Healthcheck *healthcheck.Healthcheck `json:",omitempty"` // Healthcheck describes how to check the container is healthy // TODO: ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific). // TODO: Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) Volumes map[string]struct{} `json:",omitempty"` // List of volumes (mounts) used for the container @@ -240,7 +241,7 @@ type ContainerState struct { Error string StartedAt string FinishedAt string - // TODO: Health *Health `json:",omitempty"` + Health *healthcheck.Health `json:",omitempty"` } type NetworkSettings struct { @@ -580,6 +581,26 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.Config.User = n.Labels[labels.User] } + // Add health check config if present in labels + if hConfig, ok := n.Labels[labels.HealthCheck]; ok && hConfig != "" { + healthCheckConfig, err := healthcheck.HealthCheckFromJSON(hConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse healthcheck label: %w", err) + } + c.Config.Healthcheck = healthCheckConfig + } + + // Add health status to container state. + if healthState, ok := n.Labels[labels.HealthState]; ok && healthState != "" { + healthStatus, err := healthcheck.ReadHealthStatusForInspect(n.Labels[labels.StateDir], n.Labels[labels.HealthState]) + if err != nil { + return nil, fmt.Errorf("failed to get health status for inspect: %w", err) + } + if healthStatus != nil { + c.State.Health = healthStatus + } + } + return c, nil } @@ -629,6 +650,15 @@ func ImageFromNative(nativeImage *native.Image) (*Image, error) { ExposedPorts: portSet, } + // Add health check if present in labels + if healthStr, ok := imgOCI.Config.Labels[labels.HealthCheck]; ok && healthStr != "" { + healthCheckConfig, err := healthcheck.HealthCheckFromJSON(healthStr) + if err != nil { + return nil, fmt.Errorf("failed to parse healthcheck label: %w", err) + } + image.Config.Healthcheck = healthCheckConfig + } + return image, nil } diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index e271217610c..3e7602d8e67 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -22,16 +22,22 @@ import ( "path/filepath" "runtime" "testing" + "time" "github.com/docker/go-connections/nat" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" "gotest.tools/v3/assert" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" + "github.com/containerd/nerdctl/v2/pkg/labels" ) func TestContainerFromNative(t *testing.T) { @@ -42,6 +48,16 @@ func TestContainerFromNative(t *testing.T) { filesystem.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) defer os.RemoveAll(tempStateDir) + hc := &healthcheck.Healthcheck{ + Test: []string{"CMD-SHELL", "curl -f http://localhost || exit 1"}, + Interval: time.Second * 30, + Timeout: time.Second * 5, + Retries: 3, + StartPeriod: time.Second * 10, + } + hcJSON, err := hc.ToJSONString() + assert.NilError(t, err) + testcase := []struct { name string n *native.Container @@ -299,6 +315,51 @@ func TestContainerFromNative(t *testing.T) { }, }, }, + { + name: "container with healthcheck label", + n: &native.Container{ + Container: containers.Container{ + Labels: map[string]string{ + labels.HealthCheck: hcJSON, + }, + }, + Spec: &specs.Spec{}, + Process: &native.Process{ + Status: containerd.Status{ + Status: "running", + }, + }, + }, + expected: &Container{ + Created: "0001-01-01T00:00:00Z", + Platform: runtime.GOOS, + Mounts: []MountPoint{}, + State: &ContainerState{ + Status: "running", + Running: true, + Pid: 0, + FinishedAt: "", + }, + HostConfig: &HostConfig{ + LogConfig: loggerLogConfig{Driver: "json-file", Opts: map[string]string{}}, + PortBindings: nat.PortMap{}, + GroupAdd: []string{}, + Tmpfs: map[string]string{}, + UTSMode: "host", + LinuxBlkioSettings: getDefaultLinuxBlkioSettings(), + }, + NetworkSettings: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{}, + }, + Config: &Config{ + Labels: map[string]string{ + labels.HealthCheck: hcJSON, + }, + Healthcheck: hc, + }, + }, + }, } for _, tc := range testcase { @@ -512,3 +573,95 @@ func TestCpuSettingsFromNative(t *testing.T) { }) } } + +func TestImageFromNative(t *testing.T) { + t.Run("parses RepoTags/Digests and RootFS Layers", func(t *testing.T) { + createdTime := time.Now().UTC() + + img := native.Image{ + Image: images.Image{ + Name: "myrepo/myimage:custom", + Target: ocispec.Descriptor{ + Digest: digest.Digest("sha256:targetdigest"), + }, + }, + ImageConfigDesc: ocispec.Descriptor{ + Digest: digest.Digest("sha256:configdigest"), + }, + ImageConfig: ocispec.Image{ + RootFS: ocispec.RootFS{ + Type: "layers", + DiffIDs: []digest.Digest{"sha256:layer1", "sha256:layer2"}, + }, + History: []ocispec.History{ + { + Created: &createdTime, + Author: "test-author", + Comment: "test-comment", + }, + }, + }, + } + + out, err := ImageFromNative(&img) + assert.NilError(t, err) + + // ID, tags, digests + assert.Equal(t, out.ID, "sha256:configdigest") + assert.Equal(t, out.RepoTags[0], "myrepo/myimage:custom") + assert.Equal(t, out.RepoDigests[0], "myrepo/myimage@sha256:targetdigest") + + // RootFS + assert.DeepEqual(t, out.RootFS.Layers, []string{"sha256:layer1", "sha256:layer2"}) + + // History + assert.Equal(t, out.Author, "test-author") + assert.Equal(t, out.Comment, "test-comment") + assert.Equal(t, out.Created, createdTime.Format(time.RFC3339Nano)) + }) + + t.Run("parses Healthcheck label", func(t *testing.T) { + testcases := []struct { + name string + labels map[string]string + expected *healthcheck.Healthcheck + }{ + { + name: "Valid Healthcheck Label", + labels: map[string]string{ + labels.HealthCheck: `{ + "test": ["CMD-SHELL", "curl -f http://localhost/ || exit 1"], + "interval": 30000000000, + "timeout": 5000000000 + }`, + }, + expected: &healthcheck.Healthcheck{ + Test: []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}, + Interval: time.Second * 30, + Timeout: time.Second * 5, + }, + }, + { + name: "No Healthcheck Label", + labels: map[string]string{}, + expected: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + img := native.Image{ + ImageConfig: ocispec.Image{ + Config: ocispec.ImageConfig{ + Labels: tc.labels, + }, + }, + } + + out, err := ImageFromNative(&img) + assert.NilError(t, err) + assert.DeepEqual(t, out.Config.Healthcheck, tc.expected) + }) + } + }) +} diff --git a/pkg/internal/filesystem/lock.go b/pkg/internal/filesystem/lock.go index 847e403883d..c278bbe917e 100644 --- a/pkg/internal/filesystem/lock.go +++ b/pkg/internal/filesystem/lock.go @@ -37,7 +37,7 @@ import ( // If Lock returns nil, no other process will be able to place a read or write lock on the file until // this process exits, closes f, or calls Unlock on it. func Lock(path string) (file *os.File, err error) { - return commonlock(path, writeLock) + return commonlock(path, writeLock, false) } // ReadOnlyLock places an advisory read lock on the file, blocking until it can be locked. @@ -45,10 +45,10 @@ func Lock(path string) (file *os.File, err error) { // If ReadOnlyLock returns nil, no other process will be able to place a write lock on // the file until this process exits, closes f, or calls Unlock on it. func ReadOnlyLock(path string) (file *os.File, err error) { - return commonlock(path, readLock) + return commonlock(path, readLock, false) } -func commonlock(path string, mode lockType) (file *os.File, err error) { +func commonlock(path string, mode lockType, appendMode bool) (file *os.File, err error) { defer func() { if err != nil { err = errors.Join(ErrLockFail, err, file.Close()) @@ -65,13 +65,21 @@ func commonlock(path string, mode lockType) (file *os.File, err error) { } } - file, err = os.Open(path) - if errors.Is(err, os.ErrNotExist) { - file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, privateFilePermission) - } - - if err != nil { - return nil, err + // For append mode, open with specific flags + if appendMode { + file, err = os.OpenFile(path, os.O_WRONLY, privateFilePermission) + if err != nil { + return nil, err + } + } else { + // Preserve the original behavior for non-append operations + file, err = os.Open(path) + if errors.Is(err, os.ErrNotExist) { + file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, privateFilePermission) + } + if err != nil { + return nil, err + } } if err = platformSpecificLock(file, mode); err != nil { @@ -123,3 +131,17 @@ func WithReadOnlyLock(path string, function func() error) (err error) { return function() } + +// WithAppendLock executes the function with a file opened in append mode +func WithAppendLock(path string, function func(file *os.File) error) (err error) { + file, err := commonlock(path, writeLock, true) + if err != nil { + return err + } + + defer func() { + err = errors.Join(Unlock(file), err) + }() + + return function(file) +} diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 9356ad03a68..0c50324fee2 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -118,4 +118,10 @@ const ( // User is the username of the container User = Prefix + "user" + + // HealthCheck stores the health check configuration used to run health checks on the container + HealthCheck = Prefix + "healthcheck" + + // HealthState stores the current health state (status and failing streak). + HealthState = Prefix + "healthstate" ) From 27c25728fbf30db3f431c93780472549644acb58 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Fri, 20 Jun 2025 11:44:10 +0100 Subject: [PATCH 073/868] lint: bump cognitive-complexity Signed-off-by: Justin Chadwell --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 483315a995c..d7e9204ae55 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -126,7 +126,7 @@ linters: # 222 occurrences. Could indicate failure to handle broken conditions. disabled: true - name: cognitive-complexity - arguments: [197] + arguments: [205] # 441 occurrences (at default 7). We should try to lower it (involves significant refactoring). ##### P2: nice to have. From 84894d9aaeb7fb2fb40e624c15e33baf8dfe21f0 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 18 Jun 2025 13:07:52 -0700 Subject: [PATCH 074/868] WriteFile with rollback and no inode change WriteFile now: - no longer relies on Rename, hence no longer changing files inodes - have disaster recovery and rollback Signed-off-by: apostasie --- cmd/nerdctl/helpers/flagutil.go | 7 + pkg/fs.go | 27 ++ pkg/internal/filesystem/consts.go | 25 +- pkg/internal/filesystem/helpers.go | 257 ++++++++++++++++++ pkg/internal/filesystem/os.go | 31 ++- pkg/internal/filesystem/writefile_rollback.go | 89 ++++++ .../filesystem/writefile_rollback_test.go | 206 ++++++++++++++ 7 files changed, 638 insertions(+), 4 deletions(-) create mode 100644 pkg/fs.go create mode 100644 pkg/internal/filesystem/helpers.go create mode 100644 pkg/internal/filesystem/writefile_rollback.go create mode 100644 pkg/internal/filesystem/writefile_rollback_test.go diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 9c4e8c018c4..7200ae459a3 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" + "github.com/containerd/nerdctl/v2/pkg" "github.com/containerd/nerdctl/v2/pkg/api/types" ) @@ -145,6 +146,12 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) return types.GlobalCommandOptions{}, err } + // Point to dataRoot for filesystem-helpers implementing rollback / backups. + err = pkg.InitFS(dataRoot) + if err != nil { + return types.GlobalCommandOptions{}, err + } + return types.GlobalCommandOptions{ Debug: debug, DebugFull: debugFull, diff --git a/pkg/fs.go b/pkg/fs.go new file mode 100644 index 00000000000..4b535867b44 --- /dev/null +++ b/pkg/fs.go @@ -0,0 +1,27 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package pkg + +import "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" + +// InitFS will set the root location to store `internal/filesystem` ops files. +// These files are used to allow `WriteFile` to backup and rollback content. +// While they are transient in nature, they should still persist OS crashes / reboots, so, preferably under something +// like XDGData, rather than tmp. +func InitFS(path string) error { + return filesystem.SetFilesystemOpsDirectory(path) +} diff --git a/pkg/internal/filesystem/consts.go b/pkg/internal/filesystem/consts.go index c23112c3fd1..03fbe6953ed 100644 --- a/pkg/internal/filesystem/consts.go +++ b/pkg/internal/filesystem/consts.go @@ -16,14 +16,35 @@ package filesystem -import "io" +import ( + "io" + "os" + "path/filepath" +) const ( + // Max size of path components pathComponentMaxLength = 255 - privateFilePermission = 0o600 + privateFilePermission = os.FileMode(0o600) + privateDirPermission = os.FileMode(0o700) ) var ( // Lightweight indirection to ease testing ioCopy = io.Copy + + // Location (under XDG data home) used for markers and backups + filesystemOpsPath = "filesystem-ops" + // Suffix for markers and backup files + markerSuffix = "in-progress" + backupSuffix = "backup" + + // holdLocation points to where markers and backup files will be held. This should NOT be let to /tmp, + // but instead be explicitly configured with SetFilesystemOpsDirectory. + holdLocation = os.TempDir() ) + +func SetFilesystemOpsDirectory(path string) error { + holdLocation = filepath.Join(path, filesystemOpsPath) + return os.MkdirAll(holdLocation, privateDirPermission) +} diff --git a/pkg/internal/filesystem/helpers.go b/pkg/internal/filesystem/helpers.go new file mode 100644 index 00000000000..f9be0cb2f54 --- /dev/null +++ b/pkg/internal/filesystem/helpers.go @@ -0,0 +1,257 @@ +/* + 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 filesystem + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +const ( + removeMarker = "remove" +) + +func ensureRecovery(filename string) (err error) { + // Check for a marker file. + // No marker means all fine, nothing to be done. + // Any other error is a hard error. + var op string + if op, err = markerRead(filename); err != nil { + if os.IsNotExist(err) { + err = nil + } + return err + } + + // We have a marker. We know we were interrupted. + // Check for a possible backup file. + var exists bool + if exists, err = backupExists(filename); err != nil { + return err + } + + // If we have a backup, restore from it + if exists { + if err = backupRestore(filename); err != nil { + return err + } + } else { + // We do not see a backup. + // Do we have a final destination then? + _, err = os.Stat(filename) + // Any error but does not exist is a hard error. + if err != nil && !os.IsNotExist(err) { + return err + } + + // If we do NOT have a destination, nothing to be done - we already took care of it, though we were interrupted + // mid-recovery. + + // If we DO have a destination: + if err == nil { + // Either: + // - there was no original, so we need to remove it (marker contains `remove`) + // - or we were interrupted ALSO during the recovery attempt, after the backup restore above and before deleting the marker + // in which case we do NOT want to remove as the file has already been restored. + if op == removeMarker { + // Errors on remove are hard errors. + if err = os.Remove(filename); err != nil { + return err + } + } + } + } + + // Ok, we successfully recovered, now, remove the marker and return + return markerRemove(filename) +} + +// backupSave does perform a backup of the provided file at `path`. +func backupSave(path string) error { + return internalCopy(path, backupLocation(path)) +} + +// backupRestore restores a file from its backup. +// On success the backup is deleted. +func backupRestore(path string) error { + err := internalCopy(backupLocation(path), path) + if err == nil { + err = os.Remove(backupLocation(path)) + } + + return err +} + +// backupExists checks if a backup file exists for file located at `path`. +func backupExists(path string) (bool, error) { + _, err := os.Stat(backupLocation(path)) + if os.IsNotExist(err) { + return false, nil + } + + return err == nil, err +} + +// backupLocation returns the location of the backup for path. +func backupLocation(path string) string { + return location(path) + backupSuffix +} + +// markerCreate saves a marker file with the current time. +// Markers are used to indicate an operation is in progress and allow for disaster recovery. +func markerCreate(path string, op string) (err error) { + var marker *os.File + marker, err = os.OpenFile(markerLocation(path), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, privateFilePermission) + if err != nil { + return err + } + + defer func() { + // If we errored on sync or close, remove the marker (ignore removal errors) + if err = errors.Join(err, marker.Close()); err != nil { + _ = markerRemove(path) + } + }() + + _, err = marker.Write([]byte(op)) + if err != nil { + return err + } + + return marker.Sync() +} + +// markerRead reads the content of a marker file if it exists (contains the time at which it was created). +func markerRead(path string) (string, error) { + data, err := os.ReadFile(markerLocation(path)) + if err != nil { + return "", err + } + + return string(data), nil +} + +// markerRemove deletes a marker file. +func markerRemove(path string) error { + return os.Remove(markerLocation(path)) +} + +// markerLocation returns the location of the marker file for a given path. +func markerLocation(path string) string { + return location(path) + markerSuffix +} + +// location returns the filesystem-ops path associated with a given file (where marker and backups are located). +// The location is unique (see hash), and shows the first 16 characters of the filename for readability. +func location(path string) string { + dir := filepath.Dir(path) + base := filepath.Base(path) + pretty := base + // Ensure that we do not blow up filesystem length limits + if len(pretty) > 16 { + pretty = pretty[:16] + } + return filepath.Join(holdLocation, hash(dir)+"-"+pretty+"-"+hash(base)+"-") +} + +// hash does return the first 8 characters of the shasum256 of the provided string. +// Chances of collision are 50% with 77,000 *simultaneous* entries. +func hash(s string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))[0:8] +} + +// internalCopy performs a simple copy from source to destination. +// This in itself is not safe. +func internalCopy(sourcePath, destinationPath string) (err error) { + var source *os.File + + // Open source + source, err = os.OpenFile(sourcePath, os.O_RDONLY, privateFilePermission) + if err != nil { + return err + } + + // Read file length + srcInfo, err := source.Stat() + if err != nil { + return err + } + + defer func() { + err = errors.Join(err, source.Close()) + }() + + return fileWrite(source, srcInfo.Size(), destinationPath, privateFilePermission, srcInfo.ModTime()) +} + +// fileWrite performs a simple write to the destination file from the provided io.Reader. +// This in itself is not safe. +func fileWrite(source io.Reader, size int64, destinationPath string, perm os.FileMode, mTime time.Time) (err error) { + var destination *os.File + mustClose := true + + // Open destination + destination, err = os.OpenFile(destinationPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) + if err != nil { + return err + } + + defer func() { + // Close if need be. + if mustClose { + err = errors.Join(err, destination.Close()) + } + + // Remove destination if we failed anywhere. Ignore removal failures. + if err != nil { + _ = os.Remove(destinationPath) + } + }() + + // Copy over + var n int64 + n, err = ioCopy(destination, source) + if err != nil { + return err + } + + if n < size { + return io.ErrShortWrite + } + + // Ensure data is committed + if err = destination.Sync(); err != nil { + return err + } + + err = destination.Close() + mustClose = false + if err != nil { + return err + } + + if !mTime.IsZero() { + err = os.Chtimes(destinationPath, mTime, mTime) + } + + return err +} diff --git a/pkg/internal/filesystem/os.go b/pkg/internal/filesystem/os.go index 12764795b62..62411c5250f 100644 --- a/pkg/internal/filesystem/os.go +++ b/pkg/internal/filesystem/os.go @@ -16,8 +16,35 @@ package filesystem -import "os" +import ( + "errors" + "os" +) +func ReadFile(filename string) (data []byte, err error) { + if err = ensureRecovery(filename); err != nil { + return nil, err + } + + data, err = os.ReadFile(filename) + if err != nil { + return nil, errors.Join(ErrFilesystemFailure, err) + } + + return data, nil +} + +func Stat(filename string) (os.FileInfo, error) { + if err := ensureRecovery(filename); err != nil { + return nil, errors.Join(ErrFilesystemFailure, err) + } + + return os.Stat(filename) +} + +// WriteFile implements an atomic and durable alternative to os.WriteFile that does not change inodes (unlike the usual +// approach on atomic writes that relies on renaming files). func WriteFile(filename string, data []byte, perm os.FileMode) error { - return WriteFileWithRename(filename, data, perm) + _, err := WriteFileWithRollback(filename, data, perm) + return err } diff --git a/pkg/internal/filesystem/writefile_rollback.go b/pkg/internal/filesystem/writefile_rollback.go new file mode 100644 index 00000000000..31ec35c3d18 --- /dev/null +++ b/pkg/internal/filesystem/writefile_rollback.go @@ -0,0 +1,89 @@ +/* + 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 filesystem + +import ( + "bytes" + "errors" + "os" + "time" +) + +// WriteFileWithRollback implements an atomic and durable file write operation with rollback. +// The rollback callback may be called by higher-level operations in case there is a need to +// revert changes as part of a more complex, multi-prong operation. +// Note that with or without rollback, WriteFileWithRollback does ensure disaster recovery. +func WriteFileWithRollback(filename string, data []byte, perm os.FileMode) (rollback func() error, err error) { + // Ensure there are no interrupted operations (leftover marker file and backup), or restore them if need be. + // If this is failing, we are dead in the water. + if err = ensureRecovery(filename); err != nil { + return nil, errors.Join(ErrFilesystemFailure, err) + } + + // On error, call recovery to rollback changes. + defer func() { + if err != nil { + err = errors.Join(ErrFilesystemFailure, err, ensureRecovery(filename)) + } + }() + + // If the file does not exist + markerData := "" + if _, err = os.Stat(filename); err != nil { + // Any error but does not exist is a hard error. + if !os.IsNotExist(err) { + return nil, err + } + // Otherwise, rollback and marker is "remove" + markerData = removeMarker + rollback = func() error { + return os.Remove(filename) + } + } else { + // Destination exists. + // Rollback will be: restore data from the backup + rollback = func() error { + return backupRestore(filename) + } + } + + // Create the marker. Failure to do so is a hard error. + if err = markerCreate(filename, markerData); err != nil { + return nil, err + } + + // If the file exists, we need to back it up. + if markerData == "" { + // Back it up now. + if err = backupSave(filename); err != nil { + return nil, err + } + } + + // Now, write the content to the destination. + if err = fileWrite(bytes.NewReader(data), int64(len(data)), filename, perm, time.Time{}); err != nil { + return nil, err + } + + // Remove the marker. + if err = markerRemove(filename); err != nil { + return nil, err + } + + // On success, return the rollback + return rollback, nil +} diff --git a/pkg/internal/filesystem/writefile_rollback_test.go b/pkg/internal/filesystem/writefile_rollback_test.go new file mode 100644 index 00000000000..603d7538d14 --- /dev/null +++ b/pkg/internal/filesystem/writefile_rollback_test.go @@ -0,0 +1,206 @@ +/* + 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. +*/ + +//nolint:forbidigo +package filesystem + +import ( + "errors" + "io" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +func TestRollbackForNonExistentFile(t *testing.T) { + // Test that calling the rollback after writing to a new existent file does remove the file + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "non-existent-file") + + // Write to it and check that this went through + rollback, err := WriteFileWithRollback(fp, []byte("new content"), 0o600) + assert.NilError(t, err) + cn, _ := os.ReadFile(fp) + assert.Equal(t, string(cn), "new content") + + // Roll it back and check it has been removed. + err = rollback() + assert.NilError(t, err) + _, err = os.ReadFile(fp) + assert.Assert(t, os.IsNotExist(err)) +} + +func TestRollbackForPreexistingFile(t *testing.T) { + // Test that calling the rollback after writing to a pre-existing file does restore the original + + // Create a file with pre-existing content + dir := t.TempDir() + fp := filepath.Join(dir, "pre-existing-file") + _ = os.WriteFile(fp, []byte("original content"), 0o600) + cn, _ := os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") + + // Write to it and check that this went through + rollback, err := WriteFileWithRollback(fp, []byte("updated content"), 0o600) + assert.NilError(t, err) + + cn, _ = os.ReadFile(fp) + assert.Equal(t, string(cn), "updated content") + + // Roll it back and check we have the original + err = rollback() + assert.NilError(t, err) + cn, _ = os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") +} + +func TestBackupFailure(t *testing.T) { + // Test that if backup is failing, a pre-existing file is restored to its original value. + + // Create a file with pre-existing content + dir := t.TempDir() + fp := filepath.Join(dir, "pre-existing-file") + _ = os.WriteFile(fp, []byte("original content"), 0o600) + cn, _ := os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") + + fakeError := errors.New("fake error") + // Override ioCopy to simulate an error creating the backup + ioCopy = func(dst io.Writer, src io.Reader) (written int64, err error) { + return 0, fakeError + } + + // Write. Check that we still have the original. + rollback, err := WriteFileWithRollback(fp, []byte("updated content"), 0o600) + assert.ErrorIs(t, err, fakeError) + assert.Assert(t, rollback == nil) + cn, _ = os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") +} + +func TestWriteFailure(t *testing.T) { + // Test that if write to a non-existent file is failing, the file is deleted. + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "non-existent-file") + + fakeError := errors.New("fake error") + // Override ioCopy to simulate an error while writing to the destination + // Note: since the file does not exist, there will be no backup + ioCopy = func(dst io.Writer, src io.Reader) (written int64, err error) { + return 0, fakeError + } + + // Write. Check that the file has been removed + rollback, err := WriteFileWithRollback(fp, []byte("update"), 0o600) + assert.ErrorIs(t, err, fakeError) + assert.Assert(t, rollback == nil) + _, err = os.ReadFile(fp) + assert.Assert(t, os.IsNotExist(err)) + + // Restore io copy + ioCopy = io.Copy +} + +func TestShortWriteFailure(t *testing.T) { + // Test that a write failing to write all content to a non-existent file will delete the file. + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "non-existent-file") + + // Override ioCopy to simulate a short write + ioCopy = func(dst io.Writer, src io.Reader) (written int64, err error) { + return 1, nil + } + + // Write. Check that we still have the original. + rollback, err := WriteFileWithRollback(fp, []byte("update"), 0o600) + assert.ErrorIs(t, err, io.ErrShortWrite) + assert.Assert(t, rollback == nil) + _, err = os.ReadFile(fp) + assert.Assert(t, os.IsNotExist(err)) + + // Restore io copy + ioCopy = io.Copy +} + +func TestDisasterRecoveryFromBackup(t *testing.T) { + // Test that a file that has left-over backup and marker will get restored to its original content + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "pre-existing-file") + _ = os.WriteFile(fp, []byte("original content"), 0o600) + + // Artificially create leftover marker + _ = markerCreate(fp, "") + // Artificially create leftover backup + _ = backupSave(fp) + + // Pork the file, to simulate interrupted write with leftover marker and backup + _ = os.WriteFile(fp, []byte("porked"), 0o600) + + // Now, see that disaster recovery got the backup + err := ensureRecovery(fp) + assert.NilError(t, err) + + cn, _ := os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") +} + +func TestDisasterRecoveryNoBackup1(t *testing.T) { + // Test that a previously non-existent file with a marker left-over will get deleted + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "non-existent-file") + + // Artificially create leftover marker + _ = markerCreate(fp, removeMarker) + + // Pork the file. mtime will be > marker mtime, meaning we expect the file to get deleted + _ = os.WriteFile(fp, []byte("porked"), 0o600) + + err := ensureRecovery(fp) + assert.NilError(t, err) + + _, err = os.ReadFile(fp) + assert.Assert(t, os.IsNotExist(err)) +} + +func TestDisasterRecoveryNoBackup2(t *testing.T) { + // Test that a file with a more recent marker leftover and no backup will be left untouched. + + // Create file + dir := t.TempDir() + fp := filepath.Join(dir, "pre-existing-file") + _ = os.WriteFile(fp, []byte("original content"), 0o600) + + // Artificially create leftover marker + _ = markerCreate(fp, "") + + err := ensureRecovery(fp) + assert.NilError(t, err) + + cn, _ := os.ReadFile(fp) + assert.Equal(t, string(cn), "original content") +} From 4c39ab5a7ac39570d80aa8c5708a9e70ca686c20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 22:47:03 +0000 Subject: [PATCH 075/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.6.4 to 2.6.5. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.6.4...v2.6.5) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.6.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 7 +++---- go.sum | 8 ++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 557abcd86e8..b4cafa8c7da 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.6.4 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.6.5 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined @@ -127,9 +127,6 @@ require ( github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.3.0 // indirect github.com/vbatts/tar-split v0.11.6 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -149,4 +146,6 @@ require ( tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) +require github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 35f0f85a807..7fdbac719f9 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.6.4 h1:Gjv6x8eAhqwwWvoXIo0oZ4bDQBh0OMwdU7LUL9PDLiM= -github.com/compose-spec/compose-go/v2 v2.6.4/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= +github.com/compose-spec/compose-go/v2 v2.6.5 h1:H7xP5OMKdkN2p0brx01slxIU6dE/q6ybbG+jozPtIqk= +github.com/compose-spec/compose-go/v2 v2.6.5/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= @@ -86,6 +86,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/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 v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= @@ -275,6 +277,8 @@ github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBC github.com/rootless-containers/rootlesskit/v2 v2.3.5 h1:WGY05oHE7xQpSkCGfYP9lMY5z19tCxA8PhWlvP1cKx8= github.com/rootless-containers/rootlesskit/v2 v2.3.5/go.mod h1:83EIYLeMX8UeNgLHkR1PefoSV76aKEC+OyI3vzrEfvw= 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= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= From d516485683f909fd2971c067b6b28603992d8955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 22:47:15 +0000 Subject: [PATCH 076/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.1.2...v2.1.3) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 557abcd86e8..7014e8da456 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.2 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.1.3 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 35f0f85a807..2aeb58f2eee 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.2 h1:4ZQxB+FVYmwXZgpBcKfar6ieppm3KC5C6FRKvtJ6DRU= -github.com/containerd/containerd/v2 v2.1.2/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd/v2 v2.1.3 h1:eMD2SLcIQPdMlnlNF6fatlrlRLAeDaiGPGwmRKLZKNs= +github.com/containerd/containerd/v2 v2.1.3/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From b2f3b6cd0d0c7781081757d93a1ab81ee3422f79 Mon Sep 17 00:00:00 2001 From: Tushar Gupta Date: Sat, 14 Jun 2025 02:33:07 +0530 Subject: [PATCH 077/868] feat: use stricter regex to filter network labels on the container. This allows us to skip double checking the network names on the container with an additional helper function. Signed-off-by: Tushar Gupta --- pkg/cmd/network/inspect.go | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/pkg/cmd/network/inspect.go b/pkg/cmd/network/inspect.go index a958cc9fa9e..6bacb75e445 100644 --- a/pkg/cmd/network/inspect.go +++ b/pkg/cmd/network/inspect.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "slices" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" @@ -60,7 +59,7 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo network := netList[0] - var filters = []string{fmt.Sprintf("labels.%q~=%q", labels.Networks, network.Name)} + var filters = []string{fmt.Sprintf(`labels.%q~="\\\"%s\\\""`, labels.Networks, network.Name)} filteredContainers, err := client.Containers(ctx, filters...) if err != nil { return err @@ -76,13 +75,7 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo continue } - isNetworkMember, err := isContainerInNetwork(ctx, container, network.Name) - if err != nil { - return err - } - if isNetworkMember { - containers = append(containers, nativeContainer) - } + containers = append(containers, nativeContainer) } r := &native.Network{ @@ -119,20 +112,3 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo return err } - -func isContainerInNetwork(ctx context.Context, container containerd.Container, networkName string) (bool, error) { - info, err := container.Info(ctx) - if err != nil { - return false, err - } - networkLabels, ok := info.Labels[labels.Networks] - if !ok { - return false, nil - } - - var containerNetworks []string - if err := json.Unmarshal([]byte(networkLabels), &containerNetworks); err != nil { - return false, err - } - return slices.Contains(containerNetworks, networkName), nil -} From 5827adbe6c94b66cb81b78bc7b70acdd4b7178ea Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 12 May 2025 01:03:17 -0700 Subject: [PATCH 078/868] Move leftover os.WriteFile and os.ReadFile to `filesystem` Signed-off-by: apostasie --- .golangci.yml | 3 +++ pkg/apparmorutil/apparmorutil_linux.go | 8 +++++--- pkg/buildkitutil/buildkitutil.go | 5 +++-- pkg/cmd/builder/build.go | 2 +- pkg/composer/create.go | 3 ++- pkg/composer/up_service.go | 3 ++- pkg/containerutil/container_network_manager.go | 2 +- pkg/dnsutil/hostsstore/hostsstore.go | 9 +-------- pkg/logging/logging.go | 2 +- pkg/netutil/netutil.go | 3 ++- pkg/resolvconf/resolvconf.go | 6 +++--- pkg/resolvconf/resolvconf_linux_test.go | 10 ++++++---- pkg/rootlessutil/parent_linux.go | 4 +++- pkg/store/filestore.go | 2 +- pkg/testutil/compose.go | 2 +- pkg/testutil/testutil.go | 2 +- 16 files changed, 36 insertions(+), 30 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d7e9204ae55..d283de0545d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,6 +48,9 @@ linters: - pattern: ^os\.WriteFile.*$ pkg: github.com/containerd/nerdctl/v2/pkg msg: os.WriteFile is neither atomic nor durable - use nerdctl filesystem.WriteFile instead + - pattern: ^os\.ReadFile.*$ + pkg: github.com/containerd/nerdctl/v2/pkg + msg: use filesystem.ReadFile instead of os.ReadFile staticcheck: checks: # Below is the default set diff --git a/pkg/apparmorutil/apparmorutil_linux.go b/pkg/apparmorutil/apparmorutil_linux.go index 2a526b81bfb..92fdf3cc684 100644 --- a/pkg/apparmorutil/apparmorutil_linux.go +++ b/pkg/apparmorutil/apparmorutil_linux.go @@ -26,6 +26,8 @@ import ( "github.com/moby/sys/userns" "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) var ( @@ -55,7 +57,7 @@ func hostSupports() bool { return } var buf []byte - buf, err = os.ReadFile("/sys/module/apparmor/parameters/enabled") + buf, err = filesystem.ReadFile("/sys/module/apparmor/parameters/enabled") appArmorSupported = err == nil && len(buf) == 2 && string(buf) == "Y\n" }) return appArmorSupported @@ -88,7 +90,7 @@ var ( // Related: https://gitlab.com/apparmor/apparmor/-/blob/v3.0.3/libraries/libapparmor/src/kernel.c#L311 func CanApplyExistingProfile() bool { paramEnabledOnce.Do(func() { - buf, err := os.ReadFile("/sys/module/apparmor/parameters/enabled") + buf, err := filesystem.ReadFile("/sys/module/apparmor/parameters/enabled") paramEnabled = err == nil && len(buf) == 2 && string(buf) == "Y\n" }) return paramEnabled @@ -132,7 +134,7 @@ func Profiles() ([]Profile, error) { res := make([]Profile, len(ents)) for i, ent := range ents { namePath := filepath.Join(profilesPath, ent.Name(), "name") - b, err := os.ReadFile(namePath) + b, err := filesystem.ReadFile(namePath) if err != nil { log.L.WithError(err).Warnf("failed to read %q", namePath) continue diff --git a/pkg/buildkitutil/buildkitutil.go b/pkg/buildkitutil/buildkitutil.go index 5b9570a1ddb..ecf050e4ed8 100644 --- a/pkg/buildkitutil/buildkitutil.go +++ b/pkg/buildkitutil/buildkitutil.go @@ -39,6 +39,7 @@ import ( "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -196,11 +197,11 @@ func BuildKitFile(dir, inputfile string) (absDir string, file string, err error) _, cErr := os.Lstat(filepath.Join(absDir, ContainerfileName)) if dErr == nil && cErr == nil { // both files exist, prefer Dockerfile. - dockerfile, err := os.ReadFile(filepath.Join(absDir, DefaultDockerfileName)) + dockerfile, err := filesystem.ReadFile(filepath.Join(absDir, DefaultDockerfileName)) if err != nil { return "", "", err } - containerfile, err := os.ReadFile(filepath.Join(absDir, ContainerfileName)) + containerfile, err := filesystem.ReadFile(filepath.Join(absDir, ContainerfileName)) if err != nil { return "", "", err } diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index e9aa654a425..30a5d2aebd2 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -467,7 +467,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option } func getDigestFromMetaFile(path string) (string, error) { - data, err := os.ReadFile(path) + data, err := filesystem.ReadFile(path) if err != nil { return "", err } diff --git a/pkg/composer/create.go b/pkg/composer/create.go index 8b15c4823f6..0dcbfc69cf9 100644 --- a/pkg/composer/create.go +++ b/pkg/composer/create.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" ) @@ -208,7 +209,7 @@ func (c *Composer) createServiceContainer(ctx context.Context, service *servicep return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) } - cid, err := os.ReadFile(cidFilename) + cid, err := filesystem.ReadFile(cidFilename) if err != nil { return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) } diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index 7f6adf9fac5..1af24b6c98b 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" ) @@ -198,7 +199,7 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) } - cid, err := os.ReadFile(cidFilename) + cid, err := filesystem.ReadFile(cidFilename) if err != nil { return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) } diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index b1e192019fd..d28e720a915 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -686,7 +686,7 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe return nil, nil, err } - content, err := os.ReadFile("/etc/hosts") + content, err := filesystem.ReadFile("/etc/hosts") if err != nil { return nil, nil, err } diff --git a/pkg/dnsutil/hostsstore/hostsstore.go b/pkg/dnsutil/hostsstore/hostsstore.go index d7fc28b53db..991a4929c9c 100644 --- a/pkg/dnsutil/hostsstore/hostsstore.go +++ b/pkg/dnsutil/hostsstore/hostsstore.go @@ -352,14 +352,7 @@ func (x *hostsStore) updateAllHosts() (err error) { return err } - // Because the file is mounted, we cannot do atomic writes here as that would change inode. - // The practical implications of this are that a partial / interrupted write would leave the hosts file with - // an invalid entry and/or missing entries. At worse, this would lead to a container losing localhost network - // capabilities. - // Proper consistency requires that we would have a rollback mechanism in case of recoverable failure, - // and a disaster management / cleanup mechanism, presumably at the top-level of the operation. - // nolint:forbidigo - err = os.WriteFile(loc, buf.Bytes(), 0o644) + err = filesystem.WriteFile(loc, buf.Bytes(), 0o644) if err != nil { log.L.WithError(err).Errorf("failed to write hosts file for %q", entry) } diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index e60b28c23c7..91a3231ee3a 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -142,7 +142,7 @@ func LoadLogConfig(dataStore, ns, id string) (LogConfig, error) { logConfig := LogConfig{} logConfigFilePath := LogConfigFilePath(dataStore, ns, id) - logConfigData, err := os.ReadFile(logConfigFilePath) + logConfigData, err := filesystem.ReadFile(logConfigFilePath) if err != nil { return logConfig, fmt.Errorf("failed to read log config file %q: %w", logConfigFilePath, err) } diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index cbcc27bfde9..66c80c430d7 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -37,6 +37,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/netutil/nettype" subnetutil "github.com/containerd/nerdctl/v2/pkg/netutil/subnet" @@ -481,7 +482,7 @@ func cniLoad(fileNames []string) (configList []*NetworkConfig, err error) { for _, fileName = range fileNames { var bytes []byte - bytes, err = os.ReadFile(fileName) + bytes, err = filesystem.ReadFile(fileName) if err != nil { return nil, fmt.Errorf("error reading %s: %w", fileName, err) } diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index 82968afca54..16a809d390d 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -72,7 +72,7 @@ var ( // More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf func Path() string { detectSystemdResolvConfOnce.Do(func() { - candidateResolvConf, err := os.ReadFile(defaultPath) + candidateResolvConf, err := filesystem.ReadFile(defaultPath) if err != nil { // silencing error as it will resurface at next calls trying to read defaultPath return @@ -133,7 +133,7 @@ func Get() (*File, error) { // GetSpecific returns the contents of the user specified resolv.conf file and its hash func GetSpecific(path string) (*File, error) { - resolv, err := os.ReadFile(path) + resolv, err := filesystem.ReadFile(path) if err != nil { return nil, err } @@ -151,7 +151,7 @@ func GetIfChanged() (*File, error) { lastModified.Lock() defer lastModified.Unlock() - resolv, err := os.ReadFile(Path()) + resolv, err := filesystem.ReadFile(Path()) if err != nil { return nil, err } diff --git a/pkg/resolvconf/resolvconf_linux_test.go b/pkg/resolvconf/resolvconf_linux_test.go index 0110eb34a46..2b7790712ac 100644 --- a/pkg/resolvconf/resolvconf_linux_test.go +++ b/pkg/resolvconf/resolvconf_linux_test.go @@ -21,6 +21,8 @@ import ( "bytes" "os" "testing" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) func TestGet(t *testing.T) { @@ -28,7 +30,7 @@ func TestGet(t *testing.T) { if err != nil { t.Fatal(err) } - resolvConfSystem, err := os.ReadFile("/run/systemd/resolve/resolv.conf") + resolvConfSystem, err := filesystem.ReadFile("/run/systemd/resolve/resolv.conf") if err != nil { t.Fatal(err) } @@ -171,7 +173,7 @@ func TestBuild(t *testing.T) { t.Fatal(err) } - content, err := os.ReadFile(file.Name()) + content, err := filesystem.ReadFile(file.Name()) if err != nil { t.Fatal(err) } @@ -193,7 +195,7 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { t.Fatal(err) } - content, err := os.ReadFile(file.Name()) + content, err := filesystem.ReadFile(file.Name()) if err != nil { t.Fatal(err) } @@ -218,7 +220,7 @@ func TestBuildWithNoOptions(t *testing.T) { t.Fatal(err) } - content, err := os.ReadFile(file.Name()) + content, err := filesystem.ReadFile(file.Name()) if err != nil { t.Fatal(err) } diff --git a/pkg/rootlessutil/parent_linux.go b/pkg/rootlessutil/parent_linux.go index d90b9b77dee..7ae9b36c66b 100644 --- a/pkg/rootlessutil/parent_linux.go +++ b/pkg/rootlessutil/parent_linux.go @@ -27,6 +27,8 @@ import ( "syscall" "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) func IsRootlessParent() bool { @@ -55,7 +57,7 @@ func RootlessKitChildPid(stateDir string) (int, error) { return 0, err } - pidFileBytes, err := os.ReadFile(pidFilePath) + pidFileBytes, err := filesystem.ReadFile(pidFilePath) if err != nil { return 0, err } diff --git a/pkg/store/filestore.go b/pkg/store/filestore.go index 0de4e06df5b..1893bfa6c64 100644 --- a/pkg/store/filestore.go +++ b/pkg/store/filestore.go @@ -138,7 +138,7 @@ func (vs *fileStore) Get(key ...string) ([]byte, error) { return nil, errors.Join(ErrFaultyImplementation, fmt.Errorf("%q is a directory and cannot be read as a file", path)) } - content, err := os.ReadFile(filepath.Join(append([]string{vs.dir}, key...)...)) + content, err := filesystem.ReadFile(filepath.Join(append([]string{vs.dir}, key...)...)) if err != nil { return nil, errors.Join(ErrSystemFailure, err) } diff --git a/pkg/testutil/compose.go b/pkg/testutil/compose.go index a432486ebd9..2b00cf58b2f 100644 --- a/pkg/testutil/compose.go +++ b/pkg/testutil/compose.go @@ -75,7 +75,7 @@ func LoadProject(fileName, projectName string, envMap map[string]string) (*compo if envMap == nil { envMap = make(map[string]string) } - b, err := os.ReadFile(fileName) + b, err := filesystem.ReadFile(fileName) if err != nil { return nil, err } diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 0e3f3740fd0..6e41d2641b6 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -292,7 +292,7 @@ func (b *Base) ContainerdAddress() string { xdr = fmt.Sprintf("/run/user/%d", os.Geteuid()) } pidFile := filepath.Join(xdr, "containerd-rootless", "child_pid") - pidB, err := os.ReadFile(pidFile) + pidB, err := filesystem.ReadFile(pidFile) if err != nil { b.T.Fatal(err) } From cfb5a1671196622b5e932bb6bc14b0f25033cff5 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 18 May 2025 14:41:07 -0700 Subject: [PATCH 079/868] Add support for external embed test image list Signed-off-by: apostasie --- pkg/testutil/images.yaml | 77 +++++++++++++++++++ pkg/testutil/images_linux.go | 58 ++++++++++++++ .../nerdtest/platform/platform_darwin.go | 1 - .../nerdtest/platform/platform_freebsd.go | 1 - .../nerdtest/platform/platform_linux.go | 1 - .../nerdtest/platform/platform_windows.go | 16 +--- pkg/testutil/nerdtest/registry/docker.go | 9 --- pkg/testutil/nerdtest/requirements.go | 8 -- .../testregistry/testregistry_linux.go | 15 ---- pkg/testutil/testutil.go | 5 -- pkg/testutil/testutil_darwin.go | 8 +- pkg/testutil/testutil_freebsd.go | 8 +- pkg/testutil/testutil_linux.go | 48 ++++++------ pkg/testutil/testutil_windows.go | 4 +- 14 files changed, 172 insertions(+), 87 deletions(-) create mode 100644 pkg/testutil/images.yaml create mode 100644 pkg/testutil/images_linux.go diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml new file mode 100644 index 00000000000..5ca8bed656f --- /dev/null +++ b/pkg/testutil/images.yaml @@ -0,0 +1,77 @@ +# Current schema (defined in images_linux.go) allows for ref, tag, (index) digest and platform variants. +# Right now, digest and variants are not used for anything, but they should / could be in the future. +# Also note that changing the schema should be easy and straight-forward for now, so, +# this might evolve in the near future. +alpine: + ref: "ghcr.io/stargz-containers/alpine" + tag: "3.13-org" + digest: "sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a" + variants: ["linux/amd64", "linux/arm64"] + +busybox: + ref: "ghcr.io/containerd/busybox" + tag: "1.36" + +docker_auth: + ref: "ghcr.io/stargz-containers/cesanta/docker_auth" + tag: "1.7-org" + +fluentd: + ref: "fluentd" + tag: "v1.18.0-debian-1.0" + +golang: + ref: "golang" + tag: "1.23.8-bookworm" + +kubo: + ref: "ghcr.io/stargz-containers/ipfs/kubo" + tag: "v0.16.0-org" + +mariadb: + ref: "ghcr.io/stargz-containers/mariadb" + tag: "10.5-org" + +nanoserver: + ref: "mcr.microsoft.com/windows/nanoserver" + tag: "ltsc2022" + +nginx: + ref: "ghcr.io/stargz-containers/nginx" + tag: "1.19-alpine-org" + +registry: + ref: "ghcr.io/stargz-containers/registry" + tag: "2-org" + +stargz: + ref: "ghcr.io/containerd/stargz-snapshotter" + tag: "0.15.1-kind" + +wordpress: + ref: "ghcr.io/stargz-containers/wordpress" + tag: "5.7-org" + +fedora_esgz: + ref: "ghcr.io/stargz-containers/fedora" + tag: "30-esgz" + +ffmpeg_soci: + ref: "public.ecr.aws/soci-workshop-examples/ffmpeg" + tag: "latest" + +# Large enough for testing soci index creation +ubuntu: + ref: "public.ecr.aws/docker/library/ubuntu" + tag: "23.10" + +# Future: images to add or update soon. +# busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f +# debian:bookworm-slim@sha256:b1211f6d19afd012477bd34fdcabb6b663d680e0f4b0537da6e6b0fd057a3ec3 +# gitlab/gitlab-ee:17.11.0-ee.0@sha256:e0d9d5e0d0068f4b4bac3e15eb48313b5c3bb508425645f421bf2773a964c4ae +# bitnami/harbor-portal:v2.13.0@sha256:636f39610b359369aeeddd7859cb56274d9a1bc3e467e21d74ea89e1516c1a0c +# mariadb:11.7.2@sha256:81e893032978c4bf8ad43710b7a979774ed90787fa32d199162148ce28fe3b76 +# nginx:alpine3.21@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 +# wordpress:6.8.0-php8.4-fpm-alpine@sha256:309b64fa4266d8a3fe6f0973ae3172fec1023c9b18242ccf1dffbff5dc8b81a8 +# Right now, v3 is breaking tests. +# ghcr.io/distribution/distribution:3.0.0@sha256:4ba3adf47f5c866e9a29288c758c5328ef03396cb8f5f6454463655fa8bc83e2 diff --git a/pkg/testutil/images_linux.go b/pkg/testutil/images_linux.go new file mode 100644 index 00000000000..bfeb5d3bf99 --- /dev/null +++ b/pkg/testutil/images_linux.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package testutil + +import ( + _ "embed" + "fmt" + "sync" + + "gopkg.in/yaml.v3" +) + +//go:embed images.yaml +var rawImagesList string + +var testImagesOnce sync.Once + +type TestImage struct { + Ref string `yaml:"ref"` + Tag string `yaml:"tag,omitempty"` + Digest string `yaml:"digest,omitempty"` + Variants []string `yaml:"variants,omitempty"` +} + +var testImages map[string]TestImage + +func getImage(key string) string { + testImagesOnce.Do(func() { + if err := yaml.Unmarshal([]byte(rawImagesList), &testImages); err != nil { + fmt.Printf("Error unmarshaling test images YAML file: %v\n", err) + panic("testing is broken") + } + }) + + var im TestImage + var ok bool + + if im, ok = testImages[key]; !ok { + fmt.Printf("Image %s was not found in images list\n", key) + panic("testing is broken") + } + + return im.Ref + ":" + im.Tag +} diff --git a/pkg/testutil/nerdtest/platform/platform_darwin.go b/pkg/testutil/nerdtest/platform/platform_darwin.go index 0fa050fe63f..9da2ff17a7d 100644 --- a/pkg/testutil/nerdtest/platform/platform_darwin.go +++ b/pkg/testutil/nerdtest/platform/platform_darwin.go @@ -23,7 +23,6 @@ func DataHome() (string, error) { var ( // The following are here solely for darwin to compile / lint. They are not used, as the corresponding tests are running only on linux. RegistryImageStable = "registry:2" - RegistryImageNext = "ghcr.io/distribution/distribution:" KuboImage = "ipfs/kubo:v0.16.0" DockerAuthImage = "cesanta/docker_auth:1.7" ) diff --git a/pkg/testutil/nerdtest/platform/platform_freebsd.go b/pkg/testutil/nerdtest/platform/platform_freebsd.go index 8128c930167..604d2937705 100644 --- a/pkg/testutil/nerdtest/platform/platform_freebsd.go +++ b/pkg/testutil/nerdtest/platform/platform_freebsd.go @@ -23,7 +23,6 @@ func DataHome() (string, error) { var ( // The following are here solely for freebsd to compile / lint. They are not used, as the corresponding tests are running only on linux. RegistryImageStable = "registry:2" - RegistryImageNext = "ghcr.io/distribution/distribution:" KuboImage = "ipfs/kubo:v0.16.0" DockerAuthImage = "cesanta/docker_auth:1.7" ) diff --git a/pkg/testutil/nerdtest/platform/platform_linux.go b/pkg/testutil/nerdtest/platform/platform_linux.go index 3aeeb0f03c8..57d1b04bec9 100644 --- a/pkg/testutil/nerdtest/platform/platform_linux.go +++ b/pkg/testutil/nerdtest/platform/platform_linux.go @@ -27,7 +27,6 @@ func DataHome() (string, error) { var ( RegistryImageStable = testutil.RegistryImageStable - RegistryImageNext = testutil.RegistryImageNext KuboImage = testutil.KuboImage DockerAuthImage = testutil.DockerAuthImage ) diff --git a/pkg/testutil/nerdtest/platform/platform_windows.go b/pkg/testutil/nerdtest/platform/platform_windows.go index 56be8501931..2b9c07fc937 100644 --- a/pkg/testutil/nerdtest/platform/platform_windows.go +++ b/pkg/testutil/nerdtest/platform/platform_windows.go @@ -16,22 +16,12 @@ package platform -import ( - "fmt" -) - func DataHome() (string, error) { panic("not supported") } -// The following are here solely for windows to compile. They are not used, as the corresponding tests are running only on linux. -func mirrorOf(s string) string { - return fmt.Sprintf("ghcr.io/stargz-containers/%s-org", s) -} - var ( - RegistryImageStable = mirrorOf("registry:2") - RegistryImageNext = "ghcr.io/distribution/distribution:" - KuboImage = mirrorOf("ipfs/kubo:v0.16.0") - DockerAuthImage = mirrorOf("cesanta/docker_auth:1.7") + RegistryImageStable = "there-is-no-such-test-on-windows" + KuboImage = "there-is-no-such-test-on-windows" + DockerAuthImage = "there-is-no-such-test-on-windows" ) diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go index 6e90cdfcfc9..27bd9069ff1 100644 --- a/pkg/testutil/nerdtest/registry/docker.go +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -19,7 +19,6 @@ package registry import ( "fmt" "net" - "os" "strconv" "gotest.tools/v3/assert" @@ -71,15 +70,7 @@ func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *testca.C // Attach authentication params returns by authenticator args = append(args, auth.Params(data)...) - // Get the right registry version registryImage := platform.RegistryImageStable - up := os.Getenv("DISTRIBUTION_VERSION") - if up != "" { - if up[0:1] != "v" { - up = "v" + up - } - registryImage = platform.RegistryImageNext + up - } args = append(args, registryImage) cleanup := func(data test.Data, helpers test.Helpers) { diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index e0e621501ee..3cc9390996a 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "os" "os/exec" "strings" @@ -277,13 +276,6 @@ var Registry = require.All( // - when we start a large number of registries in subtests, no need to round-trip to ghcr everytime // This of course assumes that the subtests are NOT going to prune / rmi images registryImage := platform.RegistryImageStable - up := os.Getenv("DISTRIBUTION_VERSION") - if up != "" { - if up[0:1] != "v" { - up = "v" + up - } - registryImage = platform.RegistryImageNext + up - } helpers.Ensure("pull", "--quiet", registryImage) helpers.Ensure("pull", "--quiet", platform.DockerAuthImage) helpers.Ensure("pull", "--quiet", platform.KuboImage) diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go index c1c3f3f8643..fcc3bde5baf 100644 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ b/pkg/testutil/testregistry/testregistry_linux.go @@ -57,13 +57,6 @@ type TokenAuthServer struct { func EnsureImages(base *testutil.Base) { registryImage := platform.RegistryImageStable - up := os.Getenv("DISTRIBUTION_VERSION") - if up != "" { - if up[0:1] != "v" { - up = "v" + up - } - registryImage = platform.RegistryImageNext + up - } base.Cmd("pull", "--quiet", registryImage).AssertOK() base.Cmd("pull", "--quiet", platform.DockerAuthImage).AssertOK() base.Cmd("pull", "--quiet", platform.KuboImage).AssertOK() @@ -285,14 +278,6 @@ func NewRegistry(base *testutil.Base, ca *testca.CA, port int, auth Auth, boundC args = append(args, auth.Params(base)...) registryImage := testutil.RegistryImageStable - - up := os.Getenv("DISTRIBUTION_VERSION") - if up != "" { - if up[0:1] != "v" { - up = "v" + up - } - registryImage = testutil.RegistryImageNext + up - } args = append(args, registryImage) cleanup := func(err error) { diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 0e3f3740fd0..fb5590c4d83 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -763,8 +763,3 @@ func RegisterBuildCacheCleanup(t *testing.T) { NewBase(t).Cmd("builder", "prune", "--all", "--force").Run() }) } - -func mirrorOf(s string) string { - // plain mirror, NOT stargz-converted images - return fmt.Sprintf("ghcr.io/stargz-containers/%s-org", s) -} diff --git a/pkg/testutil/testutil_darwin.go b/pkg/testutil/testutil_darwin.go index 07990bfef57..bc108cc525a 100644 --- a/pkg/testutil/testutil_darwin.go +++ b/pkg/testutil/testutil_darwin.go @@ -28,8 +28,8 @@ const ( ) var ( - BusyboxImage = "ghcr.io/containerd/busybox:1.36" - AlpineImage = mirrorOf("alpine:3.13") - NginxAlpineImage = mirrorOf("nginx:1.19-alpine") - GolangImage = mirrorOf("golang:1.18") + BusyboxImage = "there-is-no-test-on-darwin" + AlpineImage = "there-is-no-test-on-darwin" + NginxAlpineImage = "there-is-no-test-on-darwin" + GolangImage = "there-is-no-test-on-darwin" ) diff --git a/pkg/testutil/testutil_freebsd.go b/pkg/testutil/testutil_freebsd.go index 9761008585f..0eb44c10614 100644 --- a/pkg/testutil/testutil_freebsd.go +++ b/pkg/testutil/testutil_freebsd.go @@ -28,8 +28,8 @@ const ( ) var ( - BusyboxImage = "ghcr.io/containerd/busybox:1.36" - AlpineImage = mirrorOf("alpine:3.13") - NginxAlpineImage = mirrorOf("nginx:1.19-alpine") - GolangImage = mirrorOf("golang:1.18") + BusyboxImage = "there-is-no-such-test-on-freebsd" + AlpineImage = "there-is-no-such-test-on-freebsd" + NginxAlpineImage = "there-is-no-such-test-on-freebsd" + GolangImage = "there-is-no-such-test-on-freebsd" ) diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index f7ab0688d42..de6e68a58e2 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -17,38 +17,38 @@ package testutil var ( - BusyboxImage = "ghcr.io/containerd/busybox:1.36" - AlpineImage = mirrorOf("alpine:3.13") - NginxAlpineImage = mirrorOf("nginx:1.19-alpine") - NginxAlpineIndexHTMLSnippet = "Welcome to nginx!" - RegistryImageStable = mirrorOf("registry:2") - RegistryImageNext = "ghcr.io/distribution/distribution:" - WordpressImage = mirrorOf("wordpress:5.7") - WordpressIndexHTMLSnippet = "WordPress › Installation" - MariaDBImage = mirrorOf("mariadb:10.5") - DockerAuthImage = mirrorOf("cesanta/docker_auth:1.7") - FluentdImage = "fluent/fluentd:v1.17.0-debian-1.0" - KuboImage = mirrorOf("ipfs/kubo:v0.16.0") - SystemdImage = "ghcr.io/containerd/stargz-snapshotter:0.15.1-kind" - GolangImage = mirrorOf("golang:1.18") - - // Source: https://gist.github.com/cpuguy83/fcf3041e5d8fb1bb5c340915aabeebe0 - NonDistBlobImage = "ghcr.io/cpuguy83/non-dist-blob:latest" - // Foreign layer digest - NonDistBlobDigest = "sha256:be691b1535726014cdf3b715ff39361b19e121ca34498a9ceea61ad776b9c215" + AlpineImage = getImage("alpine") + BusyboxImage = getImage("busybox") + DockerAuthImage = getImage("docker_auth") + FluentdImage = getImage("fluentd") + GolangImage = getImage("golang") + KuboImage = getImage("kubo") + MariaDBImage = getImage("mariadb") + NginxAlpineImage = getImage("nginx") + RegistryImageStable = getImage("registry") + SystemdImage = getImage("stargz") + WordpressImage = getImage("wordpress") CommonImage = AlpineImage + FedoraESGZImage = getImage("fedora_esgz") // eStargz + FfmpegSociImage = getImage("ffmpeg_soci") // SOCI + UbuntuImage = getImage("ubuntu") // Large enough for testing soci index creation +) + +const ( // This error string is expected when attempting to connect to a TCP socket // for a service which actively refuses the connection. // (e.g. attempting to connect using http to an https endpoint). // It should be "connection refused" as per the TCP RFC. // https://www.rfc-editor.org/rfc/rfc793 ExpectedConnectionRefusedError = "connection refused" -) -const ( - FedoraESGZImage = "ghcr.io/stargz-containers/fedora:30-esgz" // eStargz - FfmpegSociImage = "public.ecr.aws/soci-workshop-examples/ffmpeg:latest" // SOCI - UbuntuImage = "public.ecr.aws/docker/library/ubuntu:23.10" // Large enough for testing soci index creation + NginxAlpineIndexHTMLSnippet = "Welcome to nginx!" + WordpressIndexHTMLSnippet = "WordPress › Installation" + + // Source: https://gist.github.com/cpuguy83/fcf3041e5d8fb1bb5c340915aabeebe0 + NonDistBlobImage = "ghcr.io/cpuguy83/non-dist-blob:latest@sha256:8859ffb0bb604463fe19f1e606ceda9f4f8f42e095bf78c42458cf6da7b5c7e7" + // Foreign layer digest + NonDistBlobDigest = "sha256:be691b1535726014cdf3b715ff39361b19e121ca34498a9ceea61ad776b9c215" ) diff --git a/pkg/testutil/testutil_windows.go b/pkg/testutil/testutil_windows.go index d1b830da6bf..1d3c46e4150 100644 --- a/pkg/testutil/testutil_windows.go +++ b/pkg/testutil/testutil_windows.go @@ -53,8 +53,8 @@ const ( ) var ( - GolangImage = mirrorOf("fixme-test-using-this-image-is-disabled-on-windows") - AlpineImage = mirrorOf("fixme-test-using-this-image-is-disabled-on-windows") + GolangImage = "fixme-test-using-this-image-is-disabled-on-windows" + AlpineImage = "fixme-test-using-this-image-is-disabled-on-windows" hypervContainer bool hypervSupported bool From d53e06bf0c4d932eafd36e7fa604991b7f44a126 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 21 Jun 2025 16:05:48 -0700 Subject: [PATCH 080/868] Add example of using nerdctl as a library Signed-off-by: apostasie --- examples/nerdctl-as-a-library/README.md | 3 + .../run-container/main.go | 109 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 examples/nerdctl-as-a-library/README.md create mode 100644 examples/nerdctl-as-a-library/run-container/main.go diff --git a/examples/nerdctl-as-a-library/README.md b/examples/nerdctl-as-a-library/README.md new file mode 100644 index 00000000000..8ee5e3695f1 --- /dev/null +++ b/examples/nerdctl-as-a-library/README.md @@ -0,0 +1,3 @@ +# Using nerdctl as a library + +This directory contains examples showing how to implement a cli communicating with containerd, using nerdctl as a library. diff --git a/examples/nerdctl-as-a-library/run-container/main.go b/examples/nerdctl-as-a-library/run-container/main.go new file mode 100644 index 00000000000..4988ec503d4 --- /dev/null +++ b/examples/nerdctl-as-a-library/run-container/main.go @@ -0,0 +1,109 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + nerdctl "github.com/containerd/nerdctl/v2/pkg/cmd/container" + "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/logging" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" +) + +func main() { + // Implement logging + if len(os.Args) == 3 && os.Args[1] == logging.MagicArgv1 { + err := logging.Main(os.Args[2]) + if err != nil { + fmt.Println(err) + return + } + } + + // Get options + globalOpt := types.GlobalCommandOptions(*config.New()) + + // Rootless + _ = rootlessutil.ParentMain(globalOpt.HostGatewayIP) + + // Printout options for debug + f, _ := json.MarshalIndent(globalOpt, "", " ") + fmt.Printf("%s\n", f) + + // Create container options + createOpt := types.ContainerCreateOptions{ + GOptions: globalOpt, + // TODO: this example should implement oci-hook as well instead of relying on nerdctl + NerdctlCmd: "/usr/local/bin/nerdctl", + Name: "my-container", + Label: []string{}, + Cgroupns: "private", + InRun: true, + Rm: false, + Pull: "missing", + LogDriver: "json-file", + StopSignal: "SIGTERM", + Restart: "unless-stopped", + Interactive: true, + } + + // Create client + client, ctx, cancel, err := clientutil.NewClient(context.Background(), globalOpt.Namespace, globalOpt.Address) + if err != nil { + fmt.Println(err) + return + } + defer cancel() + + // Create network manager + networkManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, types.NetworkOptions{ + NetworkSlice: []string{"bridge"}, + }, client) + + if err != nil { + fmt.Println(err) + return + } + + // Create container + container, _, err := nerdctl.Create(ctx, client, []string{"debian"}, networkManager, createOpt) + if err != nil { + fmt.Println(err) + return + } + + // Start container + err = nerdctl.Start(ctx, client, []string{"my-container"}, types.ContainerStartOptions{ + Attach: true, + Stdout: os.Stdout, + }) + + if err != nil { + fmt.Println(err) + return + } + + cc, _ := json.MarshalIndent(container, "", " ") + fmt.Println(string(cc)) +} From e08d320fefc5b455066bdb87b03acf6210323c9f Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 14:11:50 -0700 Subject: [PATCH 081/868] xMove gopkg.in/yaml.v3 to maintained fork Signed-off-by: apostasie --- go.mod | 6 +++--- go.sum | 2 ++ pkg/composer/config.go | 2 +- pkg/testutil/images_linux.go | 2 +- pkg/testutil/nerdtest/registry/cesanta.go | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 05297a04dd5..6478db4af14 100644 --- a/go.mod +++ b/go.mod @@ -62,13 +62,13 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.5.2 + go.yaml.in/yaml/v3 v3.0.3 golang.org/x/crypto v0.39.0 golang.org/x/net v0.41.0 golang.org/x/sync v0.15.0 //gomodjail:unconfined golang.org/x/sys v0.33.0 //gomodjail:unconfined golang.org/x/term v0.32.0 //gomodjail:unconfined golang.org/x/text v0.26.0 - gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) @@ -117,6 +117,7 @@ require ( github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined github.com/sirupsen/logrus v1.9.3 // indirect @@ -141,11 +142,10 @@ require ( google.golang.org/grpc v1.72.2 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) -require github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect - replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index fe3f303107f..3fe53e8f653 100644 --- a/go.sum +++ b/go.sum @@ -353,6 +353,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= 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= diff --git a/pkg/composer/config.go b/pkg/composer/config.go index 41a5320daf8..c2f6ee583ec 100644 --- a/pkg/composer/config.go +++ b/pkg/composer/config.go @@ -32,7 +32,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/opencontainers/go-digest" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) type ConfigOptions struct { diff --git a/pkg/testutil/images_linux.go b/pkg/testutil/images_linux.go index bfeb5d3bf99..641141ca066 100644 --- a/pkg/testutil/images_linux.go +++ b/pkg/testutil/images_linux.go @@ -21,7 +21,7 @@ import ( "fmt" "sync" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) //go:embed images.yaml diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go index 1a83f73dfcb..824a7bdc22b 100644 --- a/pkg/testutil/nerdtest/registry/cesanta.go +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -25,8 +25,8 @@ import ( "testing" "time" + "go.yaml.in/yaml/v3" "golang.org/x/crypto/bcrypt" - "gopkg.in/yaml.v3" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" From 1784864c45d6320bfaf7bb4ae1f065222fac976b Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 21 Jun 2025 17:51:47 -0700 Subject: [PATCH 082/868] Remove deprecated info parameter in tigron funcs Signed-off-by: apostasie --- .../builder/builder_build_oci_layout_test.go | 3 +- cmd/nerdctl/builder/builder_build_test.go | 10 +-- cmd/nerdctl/compose/compose_config_test.go | 2 +- cmd/nerdctl/compose/compose_cp_linux_test.go | 2 +- .../compose/compose_create_linux_test.go | 6 +- .../compose/compose_images_linux_test.go | 4 +- cmd/nerdctl/compose/compose_rm_linux_test.go | 16 ++-- .../compose/compose_start_linux_test.go | 6 +- cmd/nerdctl/compose/compose_up_test.go | 2 +- .../container/container_attach_linux_test.go | 18 ++-- .../container/container_commit_linux_test.go | 4 +- .../container/container_commit_test.go | 2 +- .../container/container_create_linux_test.go | 8 +- .../container/container_create_test.go | 4 +- .../container/container_health_check_test.go | 30 +++---- .../container/container_list_linux_test.go | 2 +- cmd/nerdctl/container/container_logs_test.go | 8 +- .../container/container_restart_linux_test.go | 4 +- .../container_run_cgroup_linux_test.go | 16 ++-- .../container/container_run_linux_test.go | 6 +- .../container_run_mount_linux_test.go | 2 +- .../container_run_network_linux_test.go | 5 +- .../container_run_soci_linux_test.go | 6 +- cmd/nerdctl/container/container_run_test.go | 8 +- .../container_run_user_linux_test.go | 20 ++--- .../container/container_start_linux_test.go | 2 +- cmd/nerdctl/image/image_history_test.go | 54 ++++++------ cmd/nerdctl/image/image_inspect_test.go | 42 +++++----- cmd/nerdctl/image/image_list_test.go | 28 +++---- cmd/nerdctl/image/image_load_test.go | 2 +- cmd/nerdctl/image/image_prune_test.go | 42 +++++----- cmd/nerdctl/image/image_pull_linux_test.go | 8 +- cmd/nerdctl/image/image_push_linux_test.go | 4 +- cmd/nerdctl/image/image_remove_test.go | 66 +++++++-------- cmd/nerdctl/image/image_save_test.go | 4 +- cmd/nerdctl/inspect/inspect_test.go | 14 ++-- cmd/nerdctl/ipfs/ipfs_compose_linux_test.go | 2 +- cmd/nerdctl/ipfs/ipfs_registry_linux_test.go | 2 +- .../network/network_create_linux_test.go | 8 +- cmd/nerdctl/network/network_inspect_test.go | 82 +++++++++---------- .../network/network_list_linux_test.go | 18 ++-- .../network/network_remove_linux_test.go | 12 +-- cmd/nerdctl/system/system_info_test.go | 6 +- cmd/nerdctl/system/system_prune_linux_test.go | 2 +- cmd/nerdctl/volume/volume_inspect_test.go | 16 ++-- cmd/nerdctl/volume/volume_list_test.go | 76 ++++++++--------- cmd/nerdctl/volume/volume_namespace_test.go | 4 +- cmd/nerdctl/volume/volume_prune_linux_test.go | 4 +- docs/testing/tools.md | 20 ++--- mod/tigron/expect/comparators.go | 18 ++-- mod/tigron/expect/comparators_test.go | 18 ++-- mod/tigron/expect/doc.md | 19 ++--- mod/tigron/test/command.go | 1 - mod/tigron/test/funct.go | 2 +- mod/tigron/test/helpers.go | 2 +- pkg/testutil/nerdtest/registry/cesanta.go | 4 +- pkg/testutil/nerdtest/utilities.go | 12 +-- 57 files changed, 389 insertions(+), 399 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_oci_layout_test.go b/cmd/nerdctl/builder/builder_build_oci_layout_test.go index 758675e85a1..455da80bce7 100644 --- a/cmd/nerdctl/builder/builder_build_oci_layout_test.go +++ b/cmd/nerdctl/builder/builder_build_oci_layout_test.go @@ -100,14 +100,13 @@ CMD ["echo", "test-nerdctl-build-context-oci-layout"]` }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert( t, strings.Contains( helpers.Capture("run", "--rm", data.Identifier("child")), "test-nerdctl-build-context-oci-layout", ), - info, ) }, } diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index a6aff0ea60d..9bce97be1cc 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -342,7 +342,7 @@ COPY %s /`, testFileName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { // Expecting testFileName to exist inside the output target directory assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, @@ -356,7 +356,7 @@ COPY %s /`, testFileName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } @@ -894,7 +894,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-bom")) assert.NilError(t, err, "failed to read directory") @@ -926,7 +926,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-prov")) assert.NilError(t, err, "failed to read directory") @@ -959,7 +959,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { // Check if any file in the directory matches the SBOM file pattern files, err := os.ReadDir(data.Temp().Path("dir-for-attest")) assert.NilError(t, err, "failed to read directory") diff --git a/cmd/nerdctl/compose/compose_config_test.go b/cmd/nerdctl/compose/compose_config_test.go index 5fc09f814fe..25521331ef4 100644 --- a/cmd/nerdctl/compose/compose_config_test.go +++ b/cmd/nerdctl/compose/compose_config_test.go @@ -113,7 +113,7 @@ services: testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert(t, data.Labels().Get("hash") != stdout, "hash should be different") }, } diff --git a/cmd/nerdctl/compose/compose_cp_linux_test.go b/cmd/nerdctl/compose/compose_cp_linux_test.go index c9cc1d1e040..f4d5f16af4c 100644 --- a/cmd/nerdctl/compose/compose_cp_linux_test.go +++ b/cmd/nerdctl/compose/compose_cp_linux_test.go @@ -77,7 +77,7 @@ services: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { copied := data.Temp().Load("test-file2") assert.Equal(t, copied, testFileContent) }, diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 41cc26685bd..1b6324fb848 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -64,7 +64,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") @@ -121,7 +121,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") @@ -133,7 +133,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc1", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") diff --git a/cmd/nerdctl/compose/compose_images_linux_test.go b/cmd/nerdctl/compose/compose_images_linux_test.go index ae4d9f8eb16..f0feba15812 100644 --- a/cmd/nerdctl/compose/compose_images_linux_test.go +++ b/cmd/nerdctl/compose/compose_images_linux_test.go @@ -106,7 +106,7 @@ volumes: return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( - expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, t tig.T) { assert.Equal(t, len(printables), 2) }), expect.Contains(`"ContainerName":"wordpress"`, `"ContainerName":"db"`), @@ -118,7 +118,7 @@ volumes: return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json", "wordpress") }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( - expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, t tig.T) { assert.Equal(t, len(printables), 1) }), expect.Contains(`"ContainerName":"wordpress"`), diff --git a/cmd/nerdctl/compose/compose_rm_linux_test.go b/cmd/nerdctl/compose/compose_rm_linux_test.go index 58d149693a9..0b673cfa5c8 100644 --- a/cmd/nerdctl/compose/compose_rm_linux_test.go +++ b/cmd/nerdctl/compose/compose_rm_linux_test.go @@ -78,12 +78,12 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") comp := expect.Match(regexp.MustCompile("Up|running")) - comp(wp, "", t) - comp(db, "", t) + comp(wp, t) + comp(db, t) }, } }, @@ -97,11 +97,11 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") - expect.DoesNotContain("wordpress")(wp, "", t) - expect.Match(regexp.MustCompile("Up|running"))(db, "", t) + expect.DoesNotContain("wordpress")(wp, t) + expect.Match(regexp.MustCompile("Up|running"))(db, t) }, } }, @@ -114,9 +114,9 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") - expect.DoesNotContain("db")(db, "", t) + expect.DoesNotContain("db")(db, t) }, } }, diff --git a/cmd/nerdctl/compose/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go index bfb001ad5c9..41d568670a2 100644 --- a/cmd/nerdctl/compose/compose_start_linux_test.go +++ b/cmd/nerdctl/compose/compose_start_linux_test.go @@ -61,12 +61,12 @@ services: return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") comp := expect.Match(regexp.MustCompile("Up|running")) - comp(svc0, "", t) - comp(svc1, "", t) + comp(svc0, t) + comp(svc1, t) }, } } diff --git a/cmd/nerdctl/compose/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go index 48f1b5c688f..6bf8aeae96d 100644 --- a/cmd/nerdctl/compose/compose_up_test.go +++ b/cmd/nerdctl/compose/compose_up_test.go @@ -94,7 +94,7 @@ services: return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Equal(t, data.Temp().Load("foo", "test"), "hi\n") }, } diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index 88ab8bb5430..f9a05c379c6 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -64,7 +64,7 @@ func TestAttach(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) @@ -93,7 +93,7 @@ func TestAttach(t *testing.T) { Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -125,7 +125,7 @@ func TestAttachDetachKeys(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) @@ -153,7 +153,7 @@ func TestAttachDetachKeys(t *testing.T) { Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -182,8 +182,8 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"), info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) } @@ -202,7 +202,7 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { ExitCode: 42, Output: expect.All( expect.Contains("markmark"), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier())) }, ), @@ -226,7 +226,7 @@ func TestAttachNoStdin(t *testing.T) { cmd.Feed(bytes.NewReader([]byte{16, 17})) // Ctrl-p, Ctrl-q to detach (https://en.wikipedia.org/wiki/C0_and_C1_control_codes) cmd.Run(&test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) }, }) @@ -243,7 +243,7 @@ func TestAttachNoStdin(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, // Since it's a normal exit and not detach. - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { logs := helpers.Capture("logs", data.Identifier()) assert.Assert(t, !strings.Contains(logs, "should-not-appear")) }, diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index a2b26dbd88e..e0adce00c81 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -41,7 +41,7 @@ func TestKubeCommitSave(t *testing.T) { nerdtest.KubeCtlCommand(helpers, "wait", "pod", identifier, "--for=condition=ready", "--timeout=1m").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "exec", identifier, "--", "mkdir", "-p", "/tmp/whatever").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "get", "pods", identifier, "-o", "jsonpath={ .status.containerStatuses[0].containerID }").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { containerID = strings.TrimPrefix(stdout, "containerd://") }, }) @@ -73,7 +73,7 @@ func TestKubeCommitSave(t *testing.T) { cmd = nerdtest.KubeCtlCommand(helpers, "get", "pods", tID, "-o", "jsonpath={ .status.hostIPs[0].ip }") cmd.Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { registryIP = stdout }, }) diff --git a/cmd/nerdctl/container/container_commit_test.go b/cmd/nerdctl/container/container_commit_test.go index ee16dfbb822..68291c39714 100644 --- a/cmd/nerdctl/container/container_commit_test.go +++ b/cmd/nerdctl/container/container_commit_test.go @@ -121,7 +121,7 @@ func TestZstdCommit(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.JSON([]native.Image{}, func(images []native.Image, s string, t tig.T) { + Output: expect.JSON([]native.Image{}, func(images []native.Image, t tig.T) { assert.Equal(t, len(images), 1) assert.Equal(helpers.T(), images[0].Manifest.Layers[len(images[0].Manifest.Layers)-1].MediaType, "application/vnd.docker.image.rootfs.diff.tar.zstd") }), diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 66da8a19e86..b49aa74a048 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -235,7 +235,7 @@ func TestIssue2993(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("is already used by ID")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) @@ -282,7 +282,7 @@ func TestIssue2993(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 0) @@ -363,10 +363,10 @@ func TestUsernsMappingCreateCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) assert.NilError(t, err, "Failed to get container host UID") - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, diff --git a/cmd/nerdctl/container/container_create_test.go b/cmd/nerdctl/container/container_create_test.go index 07a14a3136c..da264c46144 100644 --- a/cmd/nerdctl/container/container_create_test.go +++ b/cmd/nerdctl/container/container_create_test.go @@ -96,13 +96,13 @@ func TestCreateHyperVContainer(t *testing.T) { helpers.Command("container", "inspect", data.Labels().Get("cID")). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { return } - assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n"+info) + assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") ran = dc[0].State.Status == "exited" }, }) diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go index 66d60e30705..f45fcef0535 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -77,7 +77,7 @@ func TestContainerHealthCheckBasic(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state to be present") @@ -150,7 +150,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -185,7 +185,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -214,7 +214,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -242,7 +242,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -267,7 +267,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) assert.Assert(t, inspect.State.Health == nil, "expected health to be nil with --no-healthcheck") }), @@ -290,7 +290,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_, _ string, t *testing.T) { + Output: expect.All(func(_ string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -320,7 +320,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -350,7 +350,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -382,7 +382,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_, _ string, t *testing.T) { + Output: expect.All(func(_ string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -417,7 +417,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_, _ string, t *testing.T) { + Output: expect.All(func(_ string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -444,7 +444,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_, _ string, t *testing.T) { + Output: expect.All(func(_ string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -480,7 +480,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -512,7 +512,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -540,7 +540,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index 29687730d6a..14693d256a4 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -688,7 +688,7 @@ func TestContainerListStatusFilter(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("cID")), "No container found with status created") }, } diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 632ce955949..ad984b3166d 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -189,7 +189,7 @@ bar cmd := helpers.Custom("journalctl", "-xe") cmd.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { if stdout != "" { works = true } @@ -456,7 +456,7 @@ func TestLogsTailFollowRotate(t *testing.T) { return cmd } - testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout, info string, t *testing.T) { + testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout string, t *testing.T) { tailLogs := strings.Split(strings.TrimSpace(stdout), "\n") for _, line := range tailLogs { if line != "" { @@ -603,10 +603,10 @@ func TestLogsWithStartContainer(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { finalLogsCount := strings.Count(stdout, "foo") initialFooCount, _ := strconv.Atoi(data.Labels().Get("initialFooCount")) - assert.Assert(t, finalLogsCount > initialFooCount, "Expected 'foo' count to increase after restart", info) + assert.Assert(t, finalLogsCount > initialFooCount, "Expected 'foo' count to increase after restart") }, } }, diff --git a/cmd/nerdctl/container/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go index f4d82482f1f..a29ba6f1508 100644 --- a/cmd/nerdctl/container/container_restart_linux_test.go +++ b/cmd/nerdctl/container/container_restart_linux_test.go @@ -153,12 +153,12 @@ func TestRestartWithSignal(t *testing.T) { Output: expect.All( // Check that we saw SIGUSR1 inside the container expect.Contains(nerdtest.SignalCaught), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { // Ensure the container was restarted nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // Check the new pid is different newpid := strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid) - assert.Assert(helpers.T(), newpid != data.Labels().Get("oldpid"), info) + assert.Assert(helpers.T(), newpid != data.Labels().Get("oldpid")) }, ), } diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index dd6e8f93fdf..05d611587ec 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -315,7 +315,7 @@ func TestRunDevice(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device) }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { lo1Read, err := os.ReadFile(lo[1].Device) assert.NilError(t, err) assert.Equal(t, string(bytes.Trim(lo1Read, "\x00")), "overwritten-lo1-content") @@ -528,7 +528,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), "150")) }, ), @@ -550,7 +550,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Weight}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "100")) }, @@ -579,7 +579,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "1048576")) }, @@ -608,7 +608,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "2097152")) }, @@ -637,7 +637,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "1000")) }, @@ -666,7 +666,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "2000")) }, @@ -701,7 +701,7 @@ func TestRunCPURealTimeSettingCgroupV1(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { rtRuntime := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimeRuntime}}", data.Identifier()) rtPeriod := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimePeriod}}", data.Identifier()) assert.Assert(t, strings.Contains(rtRuntime, "950000")) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index b025f681cfa..2d610db9992 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -548,7 +548,7 @@ func TestRunWithDetachKeys(t *testing.T) { Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -616,7 +616,7 @@ func TestIssue3568(t *testing.T) { Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -651,7 +651,7 @@ func TestPortBindingWithCustomHost(t *testing.T) { ExitCode: 0, Errors: []error{}, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { resp, err := nettestutil.HTTPGet(address, 30, false) assert.NilError(t, err) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index 397ccf12969..dc76307529d 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -307,7 +307,7 @@ func TestRunBindMountTmpfs(t *testing.T) { } func mountExistsWithOpt(mountPoint, mountOpt string) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") mountOutput := []string{} for _, line := range lines { diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index cd7e6905a38..7a2e010f73d 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -423,7 +423,7 @@ func TestRunWithInvalidPortThenCleanUp(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errdefs.ErrInvalidArgument}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { getAddrHash := func(addr string) string { const addrHashLen = 8 @@ -599,7 +599,6 @@ func TestSharedNetworkSetup(t *testing.T) { Description: "Test network is shared", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") - }, Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), }, @@ -939,7 +938,7 @@ func TestHostNetworkHostName(t *testing.T) { Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { data.Labels().Set("hostHostname", stdout) }, }) diff --git a/cmd/nerdctl/container/container_run_soci_linux_test.go b/cmd/nerdctl/container/container_run_soci_linux_test.go index 670a15dc7de..16cab356e8e 100644 --- a/cmd/nerdctl/container/container_run_soci_linux_test.go +++ b/cmd/nerdctl/container/container_run_soci_linux_test.go @@ -44,7 +44,7 @@ func TestRunSoci(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Custom("mount").Run(&test.Expected{ ExitCode: 0, - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { data.Labels().Set("beforeCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -60,12 +60,12 @@ func TestRunSoci(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var afterCount int beforeCount, _ := strconv.Atoi(data.Labels().Get("beforeCount")) helpers.Custom("mount").Run(&test.Expected{ - Output: func(stdout, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { afterCount = strings.Count(stdout, "fuse.rawBridge") }, }) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index b621c8522ef..5770aef799b 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -156,7 +156,7 @@ func TestRunExitCode(t *testing.T) { Output: expect.All( expect.Match(regexp.MustCompile("Exited [(]123[)][A-Za-z0-9 ]+"+data.Identifier("exit123"))), expect.Match(regexp.MustCompile("Exited [(]0[)][A-Za-z0-9 ]+"+data.Identifier("exit0"))), - func(stdout, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit0")).State.Status, "exited") assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit123")).State.Status, "exited") }, @@ -953,7 +953,7 @@ func TestRunHealthcheckFlags(t *testing.T) { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( - func(stdout, info string, t *testing.T) { + func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, tc.name) hc := inspect.Config.Healthcheck if tc.expectTest == nil { @@ -1013,7 +1013,7 @@ HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8 Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) hc := inspect.Config.Healthcheck assert.Assert(t, hc != nil, "expected healthcheck config to be present") @@ -1040,7 +1040,7 @@ HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8 Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout, _ string, t *testing.T) { + Output: expect.All(func(stdout string, t *testing.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) hc := inspect.Config.Healthcheck assert.Assert(t, hc != nil, "expected healthcheck config to be present") diff --git a/cmd/nerdctl/container/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go index 61e2d674d77..c583011ade1 100644 --- a/cmd/nerdctl/container/container_run_user_linux_test.go +++ b/cmd/nerdctl/container/container_run_user_linux_test.go @@ -222,12 +222,12 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Fatalf("Failed to get container host UID: %v", err) } - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, @@ -249,12 +249,12 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Fatalf("Failed to get container host UID: %v", err) } - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, @@ -295,12 +295,12 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Fatalf("Failed to get container host UID: %v", err) } - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, @@ -322,12 +322,12 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Fatalf("Failed to get container host UID: %v", err) } - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, } }, @@ -367,12 +367,12 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { t.Fatalf("Failed to get container host UID: %v", err) } - assert.Assert(t, actualHostUID == "0", info) + assert.Assert(t, actualHostUID == "0") }, } }, diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index 6d9ca8c313b..4ef7e5cad9e 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -67,7 +67,7 @@ func TestStartDetachKeys(t *testing.T) { ExitCode: 0, Errors: []error{errors.New("detach keys")}, Output: expect.All( - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index 1281c00fa47..e819e31ebfd 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -90,49 +90,49 @@ func TestImageHistory(t *testing.T) { { Description: "trunc, no quiet, human", Command: test.Command("image", "history", "--human=true", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { history, err := decode(stdout) - assert.NilError(t, err, info) - assert.Equal(t, len(history), 2, info) + assert.NilError(t, err, "decode should not fail") + assert.Equal(t, len(history), 2, "history should be 2 in length") localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) - assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String(), info) - assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", info) - assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String(), info) - assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", info) - - assert.Equal(t, history[0].Size, "0B", info) - assert.Equal(t, history[0].CreatedSince, formatter.TimeSinceInHuman(compTime1), info) - assert.Equal(t, history[0].Snapshot, "", info) - assert.Equal(t, history[0].Comment, "", info) - - assert.Equal(t, history[1].Size, "5.947MB", info) - assert.Equal(t, history[1].CreatedSince, formatter.TimeSinceInHuman(compTime2), info) - assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info) - assert.Equal(t, history[1].Comment, "", info) + assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String()) + assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]") + assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String()) + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…") + + assert.Equal(t, history[0].Size, "0B") + assert.Equal(t, history[0].CreatedSince, formatter.TimeSinceInHuman(compTime1)) + assert.Equal(t, history[0].Snapshot, "") + assert.Equal(t, history[0].Comment, "") + + assert.Equal(t, history[1].Size, "5.947MB") + assert.Equal(t, history[1].CreatedSince, formatter.TimeSinceInHuman(compTime2)) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…") + assert.Equal(t, history[1].Comment, "") }), }, { Description: "no human - dates and sizes and not prettyfied", Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { history, err := decode(stdout) - assert.NilError(t, err, info) - assert.Equal(t, history[0].Size, "0", info) - assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt, info) - assert.Equal(t, history[1].Size, "5947392", info) - assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt, info) + assert.NilError(t, err, "decode should not fail") + assert.Equal(t, history[0].Size, "0") + assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt) + assert.Equal(t, history[1].Size, "5947392") + assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt) }), }, { Description: "no trunc - do not truncate sha or cmd", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { history, err := decode(stdout) - assert.NilError(t, err, info) + assert.NilError(t, err, "decode should not fail") assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") }), @@ -140,14 +140,14 @@ func TestImageHistory(t *testing.T) { { Description: "Quiet has no effect with format, so, go no-json, no-trunc", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") }), }, { Description: "With quiet, trunc has no effect", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") }), }, diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index f0c53db2346..55735d28894 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -45,14 +45,14 @@ func TestImageInspectSimpleCases(t *testing.T) { { Description: "Contains some stuff", Command: test.Command("image", "inspect", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - assert.Assert(t, len(dc[0].RootFS.Layers) > 0, info) - assert.Assert(t, dc[0].Architecture != "", info) - assert.Assert(t, dc[0].Size > 0, info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") + assert.Assert(t, len(dc[0].RootFS.Layers) > 0, "there should be at least one rootfs layer\n") + assert.Assert(t, dc[0].Architecture != "", "architecture should be set\n") + assert.Assert(t, dc[0].Size > 0, "size should be > 0 \n") }), }, { @@ -115,11 +115,11 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") reference := dc[0].ID sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") @@ -140,11 +140,11 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") reference := dc[0].ID sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") @@ -173,11 +173,11 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") for _, id := range []string{"doesnotexist", "doesnotexist:either", "busybox:bogustag"} { @@ -196,11 +196,11 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") for _, id := range []string{"∞∞∞∞∞∞∞∞∞∞", "busybox:∞∞∞∞∞∞∞∞∞∞"} { cmd := helpers.Command("image", "inspect", id) @@ -218,11 +218,11 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 2, len(dc), "Unexpectedly did not get 2 results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 2, len(dc), "Unexpectedly did not get 2 results\n") reference := nerdtest.InspectImage(helpers, "busybox") assert.Equal(t, dc[0].ID, reference.ID) assert.Equal(t, dc[1].ID, reference.ID) diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 3510b4708bc..a61043206e5 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -50,16 +50,16 @@ func TestImages(t *testing.T) { Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, info) + assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" if nerdtest.IsDocker() { header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" } tab := tabutil.NewReader(header) err := tab.ParseHeader(lines[0]) - assert.NilError(t, err, info) + assert.NilError(t, err, "ParseHeader should not fail\n") found := false for _, line := range lines[1:] { repo, _ := tab.ReadRow(line, "REPOSITORY") @@ -69,7 +69,7 @@ func TestImages(t *testing.T) { break } } - assert.Assert(t, found, info) + assert.Assert(t, found, "we should have found an image\n") }, } }, @@ -81,12 +81,12 @@ func TestImages(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(testutil.CommonImage), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, info) + assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") err := tab.ParseHeader(lines[0]) - assert.NilError(t, err, info) + assert.NilError(t, err, "ParseHeader should not fail\n") found := false for _, line := range lines[1:] { name, _ := tab.ReadRow(line, "NAME") @@ -96,7 +96,7 @@ func TestImages(t *testing.T) { } } - assert.Assert(t, found, info) + assert.Assert(t, found, "we should have found an image\n") }, ), } @@ -107,12 +107,12 @@ func TestImages(t *testing.T) { Command: test.Command("images", "--format", "'{{json .CreatedAt}}'"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, info) + assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") createdTimes := lines slices.Reverse(createdTimes) - assert.Assert(t, slices.IsSorted(createdTimes), info) + assert.Assert(t, slices.IsSorted(createdTimes), "created times should be sorted\n") }, } }, @@ -337,7 +337,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { Command: test.Command("--kube-hide-dupe", "images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var imageID string var skipLine int lines := strings.Split(strings.TrimSpace(stdout), "\n") @@ -347,7 +347,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { } tab := tabutil.NewReader(header) err := tab.ParseHeader(lines[0]) - assert.NilError(t, err, info) + assert.NilError(t, err, "ParseHeader should not fail\n") found := true for i, line := range lines[1:] { repo, _ := tab.ReadRow(line, "REPOSITORY") @@ -368,7 +368,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { break } } - assert.Assert(t, found, info) + assert.Assert(t, found, "We should have found the image\n") }, } }, diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 6598ab93db5..4a979994c4b 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -61,7 +61,7 @@ func TestLoadStdinFromPipe(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(fmt.Sprintf("Loaded image: %s:latest", identifier)), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { assert.Assert(t, strings.Contains(helpers.Capture("images"), identifier)) }, ), diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index b7c4f61bfe0..cb16b81d335 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -84,13 +84,13 @@ func TestImagePrune(t *testing.T) { identifier := data.Identifier() return &test.Expected{ Output: expect.All( - func(stdout string, info string, t *testing.T) { - assert.Assert(t, !strings.Contains(stdout, identifier), info) + func(stdout string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, identifier)) }, - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, ""), imgList) - assert.Assert(t, strings.Contains(imgList, identifier), info) + assert.Assert(t, strings.Contains(imgList, identifier)) }, ), } @@ -129,18 +129,18 @@ func TestImagePrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - func(stdout string, info string, t *testing.T) { - assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + func(stdout string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, strings.Contains(imgList, data.Identifier())) assert.Assert(t, !strings.Contains(imgList, ""), imgList) helpers.Ensure("rm", "-f", data.Identifier()) removed := helpers.Capture("image", "prune", "--force", "--all") - assert.Assert(t, strings.Contains(removed, data.Identifier()), info) + assert.Assert(t, strings.Contains(removed, data.Identifier())) imgList = helpers.Capture("images") - assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, data.Identifier())) }, ), } @@ -169,18 +169,18 @@ LABEL version=0.1`, testutil.CommonImage) Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - func(stdout string, info string, t *testing.T) { - assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + func(stdout string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, strings.Contains(imgList, data.Identifier())) }, - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") - assert.Assert(t, strings.Contains(prune, data.Identifier()), info) + assert.Assert(t, strings.Contains(prune, data.Identifier())) imgList := helpers.Capture("images") - assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, data.Identifier())) }, ), } @@ -210,9 +210,9 @@ CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("imageID")), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, data.Labels().Get("imageID")), info) + assert.Assert(t, strings.Contains(imgList, data.Labels().Get("imageID"))) }, ), } @@ -229,9 +229,9 @@ CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("imageID")), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { imgList := helpers.Capture("images") - assert.Assert(t, !strings.Contains(imgList, data.Labels().Get("imageID")), imgList, info) + assert.Assert(t, !strings.Contains(imgList, data.Labels().Get("imageID")), imgList) }, ), } diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index 6dd12b34aba..d409ed94e27 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -182,7 +182,7 @@ func TestImagePullSoci(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -196,7 +196,7 @@ func TestImagePullSoci(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, _ string, t *testing.T) { + Output: func(stdout string, t *testing.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, @@ -218,7 +218,7 @@ func TestImagePullSoci(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -232,7 +232,7 @@ func TestImagePullSoci(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index bf10f371a23..355fad17c7b 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -200,7 +200,7 @@ func TestPush(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") @@ -232,7 +232,7 @@ func TestPush(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index 11f2f050636..c35258518ad 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -63,7 +63,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -83,7 +83,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) @@ -108,7 +108,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -140,7 +140,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) @@ -162,7 +162,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -184,7 +184,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ // a created container with removed image doesn't impact other `rmi` command Output: expect.DoesNotContain(repoName, nginxRepoName), @@ -212,7 +212,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -246,7 +246,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) @@ -272,7 +272,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -293,7 +293,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) @@ -336,10 +336,10 @@ func TestIssue3016(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("images", data.Labels().Get(tagIDKey)).Run(&test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Equal(t, len(strings.Split(stdout, "\n")), 2) }, }) @@ -378,17 +378,17 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numTags+1, info) + assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numNoTags+1, info) + assert.Assert(t, len(lines) == numNoTags+1) }, }) }, @@ -410,17 +410,17 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numTags+1, info) + assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numNoTags+2, info) + assert.Assert(t, len(lines) == numNoTags+2) }, }) }, @@ -440,17 +440,17 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numTags, info) + assert.Assert(t, len(lines) == numTags) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numNoTags, info) + assert.Assert(t, len(lines) == numNoTags) }, }) }, @@ -469,7 +469,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Command("--kube-hide-dupe", "rmi", stdout[0:12]).Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("multiple IDs found with provided prefix: ")}, @@ -478,9 +478,9 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numNoTags, info) + assert.Assert(t, len(lines) == numNoTags) }, }) }, @@ -499,7 +499,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { imgID := strings.Split(stdout, "\n") helpers.Command("--kube-hide-dupe", "rmi", imgID[0]).Run(&test.Expected{ ExitCode: 1, @@ -509,9 +509,9 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) == numNoTags, info) + assert.Assert(t, len(lines) == numNoTags) }, }) }, diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 4f3bf58de6a..31315cd1272 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -48,7 +48,7 @@ func TestSaveContent(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) assert.NilError(t, err) @@ -188,7 +188,7 @@ func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { assert.Equal(t, strings.Count(stdout, data.Labels().Get("id")), 2) }, } diff --git a/cmd/nerdctl/inspect/inspect_test.go b/cmd/nerdctl/inspect/inspect_test.go index 954b0e73eac..e0e1b6167fa 100644 --- a/cmd/nerdctl/inspect/inspect_test.go +++ b/cmd/nerdctl/inspect/inspect_test.go @@ -50,23 +50,23 @@ func TestInspectSimpleCase(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var inspectResult []json.RawMessage err := json.Unmarshal([]byte(stdout), &inspectResult) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, len(inspectResult), 2, "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, len(inspectResult), 2, "Unexpectedly got multiple results\n") var dci dockercompat.Image err = json.Unmarshal(inspectResult[0], &dci) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") inspecti := nerdtest.InspectImage(helpers, testutil.CommonImage) - assert.Equal(t, dci.ID, inspecti.ID, info) + assert.Equal(t, dci.ID, inspecti.ID, "id should match\n") var dcc dockercompat.Container err = json.Unmarshal(inspectResult[1], &dcc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") inspectc := nerdtest.InspectContainer(helpers, data.Identifier()) - assert.Assert(t, dcc.ID == inspectc.ID, info) + assert.Equal(t, dcc.ID, inspectc.ID, "id should match\n") }, } }, diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index 9a7b09805b5..c75653b1dca 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -254,7 +254,7 @@ COPY index.html /usr/share/nginx/html/index.html testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index 99450b7c7d7..10b64b902b1 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -39,7 +39,7 @@ func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { cmd := helpers.Command("push", "ipfs://"+name) cmd.WithArgs(opts...) cmd.Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { lines := strings.Split(stdout, "\n") assert.Equal(t, len(lines) >= 2, true) ipfsCID = lines[len(lines)-2] diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index 01ed943467f..d044631399d 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -58,9 +58,9 @@ func TestNetworkCreate(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet")), info) - assert.Assert(t, !strings.Contains(data.Labels().Get("container2"), data.Labels().Get("subnet")), info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet"))) + assert.Assert(t, !strings.Contains(data.Labels().Get("container2"), data.Labels().Get("subnet"))) }, } }, @@ -98,7 +98,7 @@ func TestNetworkCreate(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { _, subnet, _ := net.ParseCIDR(data.Labels().Get("subnetStr")) ip := nerdtest.FindIPv6(stdout) assert.Assert(t, subnet.Contains(ip), fmt.Sprintf("subnet %s contains ip %s", subnet, ip)) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index e714d7cdc1f..d8d69852e24 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -70,11 +70,11 @@ func TestNetworkInspect(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, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "none") }), }, @@ -82,11 +82,11 @@ func TestNetworkInspect(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, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "host") }), }, @@ -94,11 +94,11 @@ func TestNetworkInspect(t *testing.T) { Description: "bridge", Require: require.Not(require.Windows), Command: test.Command("network", "inspect", "bridge"), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "bridge") }), }, @@ -106,11 +106,11 @@ func TestNetworkInspect(t *testing.T) { Description: "nat", Require: require.Windows, Command: test.Command("network", "inspect", "nat"), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "nat") }), }, @@ -123,11 +123,11 @@ func TestNetworkInspect(t *testing.T) { helpers.Anyhow("network", "remove", "custom") }, Command: test.Command("network", "inspect", "custom"), - Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, "custom") }), }, @@ -140,11 +140,11 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } @@ -161,11 +161,11 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } @@ -189,11 +189,11 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Labels().Get("netname")) }, } @@ -216,20 +216,20 @@ func TestNetworkInspect(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") got := dc[0] - assert.Equal(t, got.Name, data.Identifier(), info) - assert.Equal(t, got.Labels["tag"], "testNetwork", info) - assert.Equal(t, len(got.IPAM.Config), 1, info) - assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet, info) - assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway, info) - assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange, info) + assert.Equal(t, got.Name, data.Identifier()) + assert.Equal(t, got.Labels["tag"], "testNetwork") + assert.Equal(t, len(got.IPAM.Config), 1) + assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet) + assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway) + assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange) }, } }, @@ -249,7 +249,7 @@ func TestNetworkInspect(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need // to retrieve the binary name. // Note that we know this works already, so no need to assert err. @@ -308,11 +308,11 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("nginx-network-1")) // Assert only the "running" containers on the same network are returned. assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") @@ -341,8 +341,8 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, info string, t tig.T) { - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, t tig.T) { + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("network-1")) assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") assert.Equal(t, data.Identifier(), dc[0].Containers[data.Labels().Get("containerID")].Name) @@ -368,8 +368,8 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, info string, t tig.T) { - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, t tig.T) { + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") assert.Equal(t, dc[0].Name, data.Identifier("some-network")) assert.Equal(t, 0, len(dc[0].Containers), "Expected no containers as per configuration, but got multiple.") }), diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index cb6583139b5..55cfb599cc2 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -52,16 +52,16 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) + assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ data.Labels().Get("netID1")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] - assert.Assert(t, ok, info) + assert.Assert(t, ok, "expected to find name\n") } }, } @@ -74,16 +74,16 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) + assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ data.Labels().Get("netID2")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] - assert.Assert(t, ok, info) + assert.Assert(t, ok, "expected to find name\n") } }, } @@ -96,16 +96,16 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) + assert.Assert(t, len(lines) >= 1) netNames := map[string]struct{}{ data.Labels().Get("netID2")[:12]: {}, } for _, name := range lines { _, ok := netNames[name] - assert.Assert(t, ok, info) + assert.Assert(t, ok) } }, } diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go index 7a86ec37962..2dd0e2172b0 100644 --- a/cmd/nerdctl/network/network_remove_linux_test.go +++ b/cmd/nerdctl/network/network_remove_linux_test.go @@ -55,9 +55,9 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) + assert.Error(t, err, "Link not found") }, } }, @@ -96,9 +96,9 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) + assert.Error(t, err, "Link not found") }, } }, @@ -122,9 +122,9 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) + assert.Error(t, err, "Link not found") }, } }, diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go index 8c4bfe10041..eedef027503 100644 --- a/cmd/nerdctl/system/system_info_test.go +++ b/cmd/nerdctl/system/system_info_test.go @@ -34,12 +34,12 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -func testInfoComparator(stdout string, info string, t *testing.T) { +func testInfoComparator(stdout string, t *testing.T) { var dinf dockercompat.Info err := json.Unmarshal([]byte(stdout), &dinf) - assert.NilError(t, err, "failed to unmarshal stdout"+info) + assert.NilError(t, err, "failed to unmarshal stdout") unameM := infoutil.UnameM() - assert.Assert(t, dinf.Architecture == unameM, fmt.Sprintf("expected info.Architecture to be %q, got %q", unameM, dinf.Architecture)+info) + assert.Assert(t, dinf.Architecture == unameM, fmt.Sprintf("expected info.Architecture to be %q, got %q", unameM, dinf.Architecture)) } func TestInfo(t *testing.T) { diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go index 70a4a9df651..163993f791a 100644 --- a/cmd/nerdctl/system/system_prune_linux_test.go +++ b/cmd/nerdctl/system/system_prune_linux_test.go @@ -60,7 +60,7 @@ func TestSystemPrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { volumes := helpers.Capture("volume", "ls") networks := helpers.Capture("network", "ls") images := helpers.Capture("images") diff --git a/cmd/nerdctl/volume/volume_inspect_test.go b/cmd/nerdctl/volume/volume_inspect_test.go index b42b3d41558..8bd545003ea 100644 --- a/cmd/nerdctl/volume/volume_inspect_test.go +++ b/cmd/nerdctl/volume/volume_inspect_test.go @@ -99,10 +99,10 @@ func TestVolumeInspect(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1")), - expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { - assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info) - assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)+info) - assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info) + expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { + assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) + assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) + assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)) }), ), } @@ -117,7 +117,7 @@ func TestVolumeInspect(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol2")), - expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { + expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { labels := *dc[0].Labels assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) @@ -137,7 +137,7 @@ func TestVolumeInspect(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1")), - expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { + expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) }), ), @@ -153,7 +153,7 @@ func TestVolumeInspect(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("vol1"), data.Labels().Get("vol2")), - expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { + expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) assert.Assert(t, dc[1].Name == data.Labels().Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol2"), dc[1].Name)) @@ -173,7 +173,7 @@ func TestVolumeInspect(t *testing.T) { Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, Output: expect.All( expect.Contains(data.Labels().Get("vol1")), - expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { + expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) }), diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index 8535f55b562..78a36e83bbb 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -56,9 +56,9 @@ func TestVolumeLsSize(t *testing.T) { Command: test.Command("volume", "ls", "--size"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) + assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volSizes := map[string]string{ data.Identifier("1"): "100.0 KiB", data.Identifier("2"): "200.0 KiB", @@ -68,7 +68,7 @@ func TestVolumeLsSize(t *testing.T) { var numMatches = 0 var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, info) + assert.NilError(t, err, "ParseHeader should not fail\n") for _, line := range lines { name, _ := tab.ReadRow(line, "VOLUME NAME") @@ -77,10 +77,10 @@ func TestVolumeLsSize(t *testing.T) { if !ok { continue } - assert.Assert(t, size == expectSize, fmt.Sprintf("expected size %s for volume %s, got %s", expectSize, name, size)+info) + assert.Assert(t, size == expectSize, fmt.Sprintf("expected size %s for volume %s, got %s", expectSize, name, size)) numMatches++ } - assert.Assert(t, numMatches == len(volSizes), fmt.Sprintf("expected %d volumes, got: %d", len(volSizes), numMatches)+info) + assert.Assert(t, numMatches == len(volSizes), fmt.Sprintf("expected %d volumes, got: %d", len(volSizes), numMatches)) }, } }, @@ -145,9 +145,9 @@ func TestVolumeLsFilter(t *testing.T) { Command: test.Command("volume", "ls", "--quiet"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) + assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, @@ -174,9 +174,9 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, @@ -184,7 +184,7 @@ func TestVolumeLsFilter(t *testing.T) { } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -197,15 +197,15 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info) + assert.Assert(t, len(lines) >= 1, "expected at least 1 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -218,8 +218,8 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } }, @@ -231,8 +231,8 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } }, @@ -244,16 +244,16 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -266,15 +266,15 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) + assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -287,15 +287,15 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) + assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -308,16 +308,16 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -331,9 +331,9 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, data.Labels().Get("vol4"): {}, @@ -348,7 +348,7 @@ func TestVolumeLsFilter(t *testing.T) { continue } _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -362,9 +362,9 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol2"): {}, data.Labels().Get("vol4"): {}, @@ -379,7 +379,7 @@ func TestVolumeLsFilter(t *testing.T) { continue } _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } @@ -393,9 +393,9 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ data.Labels().Get("vol1"): {}, data.Labels().Get("vol3"): {}, @@ -410,7 +410,7 @@ func TestVolumeLsFilter(t *testing.T) { continue } _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)) } }, } diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go index 341d2d37204..468d1f3b96b 100644 --- a/cmd/nerdctl/volume/volume_namespace_test.go +++ b/cmd/nerdctl/volume/volume_namespace_test.go @@ -76,7 +76,7 @@ func TestVolumeNamespace(t *testing.T) { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("root_volume")), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, ), @@ -94,7 +94,7 @@ func TestVolumeNamespace(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("root_volume")) helpers.Ensure("volume", "rm", data.Labels().Get("root_volume")) helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go index 6565f578733..d7982dd7403 100644 --- a/cmd/nerdctl/volume/volume_prune_linux_test.go +++ b/cmd/nerdctl/volume/volume_prune_linux_test.go @@ -75,7 +75,7 @@ func TestVolumePrune(t *testing.T) { data.Labels().Get("namedBusy"), data.Labels().Get("namedDangling"), ), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) @@ -96,7 +96,7 @@ func TestVolumePrune(t *testing.T) { Output: expect.All( expect.DoesNotContain(data.Labels().Get("anonIDBusy"), data.Labels().Get("namedBusy")), expect.Contains(data.Labels().Get("anonIDDangling"), data.Labels().Get("namedDangling")), - func(stdout string, info string, t *testing.T) { + func(stdout string, t *testing.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) diff --git a/docs/testing/tools.md b/docs/testing/tools.md index 9b4f0d9d1ad..e44257f18c9 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -88,17 +88,13 @@ import ( ) func MyComparator(compare string) test.Comparator { - return func(stdout string, info string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() - assert.Assert(t, stdout == compare, info) + assert.Assert(t, stdout == compare) } } ``` -Note that you have access to an opaque `info` string. -It contains relevant debugging information in case your comparator is going to fail, -and you should make sure it is displayed. - ### Advanced expectations You may want to have expectations that contain a certain piece of data @@ -142,8 +138,8 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, stdout == data.Labels().Get("sometestdata"), info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } }, @@ -255,8 +251,8 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, stdout == data.Labels().Get("sometestdata"), info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } }, @@ -344,8 +340,8 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, stdout == data.Labels().Get("sometestdata"), info) + Output: func(stdout string, t *testing.T) { + assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } }, diff --git a/mod/tigron/expect/comparators.go b/mod/tigron/expect/comparators.go index 84f5fe13bd2..ac4492de822 100644 --- a/mod/tigron/expect/comparators.go +++ b/mod/tigron/expect/comparators.go @@ -30,11 +30,11 @@ import ( // All can be used as a parameter for expected.Output to group a set of comparators. func All(comparators ...test.Comparator) test.Comparator { - return func(stdout, _ string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() for _, comparator := range comparators { - comparator(stdout, "", t) + comparator(stdout, t) } } } @@ -42,7 +42,7 @@ func All(comparators ...test.Comparator) test.Comparator { // Contains can be used as a parameter for expected.Output and ensures a comparison string is found contained in the // output. func Contains(compare string, more ...string) test.Comparator { - return func(stdout, _ string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() assertive.Contains(assertive.WithFailLater(t), stdout, compare, "Inspecting output (contains)") @@ -55,7 +55,7 @@ func Contains(compare string, more ...string) test.Comparator { // DoesNotContain is to be used for expected.Output to ensure a comparison string is NOT found in the output. func DoesNotContain(compare string, more ...string) test.Comparator { - return func(stdout, _ string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, "Inspecting output (does not contain)") @@ -68,7 +68,7 @@ func DoesNotContain(compare string, more ...string) test.Comparator { // Equals is to be used for expected.Output to ensure it is exactly the output. func Equals(compare string) test.Comparator { - return func(stdout, _ string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, "Inspecting output (equals)") } @@ -76,7 +76,7 @@ func Equals(compare string) test.Comparator { // Match is to be used for expected.Output to ensure we match a regexp. func Match(reg *regexp.Regexp) test.Comparator { - return func(stdout, _ string, t *testing.T) { + return func(stdout string, t *testing.T) { t.Helper() assertive.Match(assertive.WithFailLater(t), stdout, reg, "Inspecting output (match)") } @@ -84,15 +84,15 @@ func Match(reg *regexp.Regexp) test.Comparator { // JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided // method. -func JSON[T any](obj T, verifier func(T, string, tig.T)) test.Comparator { - return func(stdout, _ string, t *testing.T) { +func JSON[T any](obj T, verifier func(T, tig.T)) test.Comparator { + return func(stdout string, t *testing.T) { t.Helper() err := json.Unmarshal([]byte(stdout), &obj) assertive.ErrorIsNil(assertive.WithSilentSuccess(t), err, "Unmarshalling JSON from stdout must succeed") if verifier != nil && err == nil { - verifier(obj, "Inspecting output (JSON)", t) + verifier(obj, t) } } } diff --git a/mod/tigron/expect/comparators_test.go b/mod/tigron/expect/comparators_test.go index d0d76c3b701..306ebb28018 100644 --- a/mod/tigron/expect/comparators_test.go +++ b/mod/tigron/expect/comparators_test.go @@ -33,10 +33,10 @@ func TestExpect(t *testing.T) { // TODO: write more tests once we can mock t in Comparator signature t.Parallel() - expect.Contains("b")("a b c", "contains works", t) - expect.DoesNotContain("d")("a b c", "does not contain works", t) - expect.Equals("a b c")("a b c", "equals work", t) - expect.Match(regexp.MustCompile("[a-z ]+"))("a b c", "match works", t) + expect.Contains("b")("a b c", t) + expect.DoesNotContain("d")("a b c", t) + expect.Equals("a b c")("a b c", t) + expect.Match(regexp.MustCompile("[a-z ]+"))("a b c", t) expect.All( expect.Contains("b"), @@ -45,7 +45,7 @@ func TestExpect(t *testing.T) { expect.DoesNotContain("d", "e"), expect.Equals("a b c"), expect.Match(regexp.MustCompile("[a-z ]+")), - )("a b c", "all", t) + )("a b c", t) type foo struct { Foo map[string]string `json:"foo"` @@ -59,9 +59,9 @@ func TestExpect(t *testing.T) { assertive.ErrorIsNil(t, err) - expect.JSON(&foo{}, nil)(string(data), "json, no verifier", t) + expect.JSON(&foo{}, nil)(string(data), t) - expect.JSON(&foo{}, func(obj *foo, info string, t tig.T) { - assertive.IsEqual(t, obj.Foo["foo"], "bar", info) - })(string(data), "json, with verifier", t) + expect.JSON(&foo{}, func(obj *foo, t tig.T) { + assertive.IsEqual(t, obj.Foo["foo"], "bar") + })(string(data), t) } diff --git a/mod/tigron/expect/doc.md b/mod/tigron/expect/doc.md index 566f92d8c55..c8fa0b71c8f 100644 --- a/mod/tigron/expect/doc.md +++ b/mod/tigron/expect/doc.md @@ -58,7 +58,7 @@ The following ready-made `test.Comparator` generators are provided: - `expect.Equals(string)`: strict equality - `expect.Match(*regexp.Regexp)`: regexp matching - `expect.All(comparators ...Comparator)`: allows to bundle together a bunch of other comparators -- `expect.JSON[T any](obj T, verifier func(T, string, tig.T))`: allows to verify the output is valid JSON and optionally +- `expect.JSON[T any](obj T, verifier func(T, tig.T))`: allows to verify the output is valid JSON and optionally pass `verifier(T, string, tig.T)` extra validation ### A complete example @@ -93,8 +93,8 @@ func TestMyThing(t *testing.T) { expect.All( expect.Contains("out"), expect.DoesNotContain("something"), - expect.JSON(&Thing{}, func(obj *Thing, info string, t tig.T) { - assert.Equal(t, obj.Name, "something", info) + expect.JSON(&Thing{}, func(obj *Thing, t tig.T) { + assert.Equal(t, obj.Name, "something") }), ), ) @@ -131,7 +131,7 @@ func TestMyThing(t *testing.T) { myTest.Command = test.Custom("ls") // Set your expectations - myTest.Expected = test.Expects(0, nil, func(stdout, info string, t tig.T){ + myTest.Expected = test.Expects(0, nil, func(stdout string, t tig.T){ t.Helper() // Bla bla, do whatever advanced stuff and some asserts }) @@ -143,7 +143,7 @@ func TestMyThing(t *testing.T) { // You can of course generalize your comparator into a generator if it is going to be useful repeatedly func MyComparatorGenerator(param1, param2 any) test.Comparator { - return func(stdout, info string, t tig.T) { + return func(stdout string, t tig.T) { t.Helper() // Do your thing... // ... @@ -155,10 +155,6 @@ func MyComparatorGenerator(param1, param2 any) test.Comparator { You can now pass along `MyComparator(comparisonString)` as the third parameter of `test.Expects`, or compose it with other comparators using `expect.All(MyComparator(comparisonString), OtherComparator(somethingElse))` -Note that you have access to an opaque `info` string, that provides a brief formatted header message that assert -will use in case of failure to provide context on the error. -You may of course ignore it and write your own message. - ### Advanced expectations You may want to have expectations that contain a certain piece of data that is being used in the command or at @@ -180,6 +176,7 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/mod/tigron/test" ) @@ -206,11 +203,11 @@ func TestMyThing(t *testing.T) { Errors: []error{ errors.New("foobla"), }, - Output: func(stdout, info string, t tig.T) { + Output: func(stdout string, t tig.T) { t.Helper() // Retrieve the data that was set during the Setup phase. - assert.Assert(t, stdout == data.Labels().Get("sometestdata"), info) + assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } } diff --git a/mod/tigron/test/command.go b/mod/tigron/test/command.go index 23b3365016e..546c4fb7614 100644 --- a/mod/tigron/test/command.go +++ b/mod/tigron/test/command.go @@ -294,7 +294,6 @@ func (gc *GenericCommand) Run(expect *Expected) { if expect.Output != nil { expect.Output( result.Stdout, - "", gc.t, ) } diff --git a/mod/tigron/test/funct.go b/mod/tigron/test/funct.go index 45b4abbd9d9..ba36e55a3ba 100644 --- a/mod/tigron/test/funct.go +++ b/mod/tigron/test/funct.go @@ -30,7 +30,7 @@ type Butler func(data Data, helpers Helpers) // - move to tig.T // A Comparator is the function signature to implement for the Output property of an Expected. -type Comparator func(stdout, info string, t *testing.T) +type Comparator func(stdout string, t *testing.T) // A Manager is the function signature meant to produce expectations for a command. type Manager func(data Data, helpers Helpers) *Expected diff --git a/mod/tigron/test/helpers.go b/mod/tigron/test/helpers.go index c148be6e5ac..9d5e069baa7 100644 --- a/mod/tigron/test/helpers.go +++ b/mod/tigron/test/helpers.go @@ -61,7 +61,7 @@ func (help *helpersInternal) Capture(args ...string) string { help.t.Helper() help.Command(args...).Run(&Expected{ //nolint:thelper - Output: func(stdout, _ string, _ *testing.T) { + Output: func(stdout string, _ *testing.T) { ret = stdout }, }) diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go index 1a83f73dfcb..2d772cbf606 100644 --- a/pkg/testutil/nerdtest/registry/cesanta.go +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -95,13 +95,13 @@ func ensureContainerStarted(helpers test.Helpers, con string) { helpers.Command("container", "inspect", con). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { return } - assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n"+info) + assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") started = dc[0].State.Running }, }) diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index 6a52c4afe73..78bfbe6fcc5 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -54,7 +54,7 @@ func InspectContainer(helpers test.Helpers, name string) dockercompat.Container var res dockercompat.Container cmd := helpers.Command("container", "inspect", name) cmd.Run(&test.Expected{ - Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, _ string, t tig.T) { + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results") res = dc[0] }), @@ -67,7 +67,7 @@ func InspectVolume(helpers test.Helpers, name string) native.Volume { var res native.Volume cmd := helpers.Command("volume", "inspect", name) cmd.Run(&test.Expected{ - Output: expect.JSON([]native.Volume{}, func(dc []native.Volume, _ string, t tig.T) { + Output: expect.JSON([]native.Volume{}, func(dc []native.Volume, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results") res = dc[0] }), @@ -80,7 +80,7 @@ func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { var res dockercompat.Network cmd := helpers.Command("network", "inspect", name) cmd.Run(&test.Expected{ - Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, _ string, t tig.T) { + Output: expect.JSON([]dockercompat.Network{}, func(dc []dockercompat.Network, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results") res = dc[0] }), @@ -93,7 +93,7 @@ func InspectImage(helpers test.Helpers, name string) dockercompat.Image { var res dockercompat.Image cmd := helpers.Command("image", "inspect", name) cmd.Run(&test.Expected{ - Output: expect.JSON([]dockercompat.Image{}, func(dc []dockercompat.Image, _ string, t tig.T) { + Output: expect.JSON([]dockercompat.Image{}, func(dc []dockercompat.Image, t tig.T) { assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results") res = dc[0] }), @@ -113,13 +113,13 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { helpers.Command("container", "inspect", con). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, info string, t *testing.T) { + Output: func(stdout string, t *testing.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { return } - assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n"+info) + assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") started = dc[0].State.Running }, }) From ee8c5c1d971a86ffbfc1a2e29e79c383fbc8a891 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 16:51:02 -0700 Subject: [PATCH 083/868] Re-enable tigron lint on the CI Signed-off-by: apostasie --- .github/workflows/workflow-tigron.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 306ef75d55b..4aa477a9790 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -58,16 +58,20 @@ jobs: brew install yamllint shellcheck fi echo "::endgroup::" - - if: ${{ env.GO_VERSION != '' && env.RUNNER_OS == 'Linux' && matrix.goos == '' }} + - if: ${{ env.GO_VERSION != '' && matrix.goos == '' }} name: "lint" env: NO_COLOR: true run: | - echo "::group:: lint" - cd mod/tigron - export LINT_COMMIT_RANGE="$(jq -r '.after + "..HEAD"' ${GITHUB_EVENT_PATH})" - make lint - echo "::endgroup::" + if [ "$RUNNER_OS" == Linux ]; then + echo "::group:: lint" + cd mod/tigron + export LINT_COMMIT_RANGE="$(jq -r '.after + "..HEAD"' ${GITHUB_EVENT_PATH})" + make lint + echo "::endgroup::" + else + echo "Lint is disabled on $RUNNER_OS" + fi - if: ${{ env.GO_VERSION != '' }} name: "test-unit" run: | From 787bba68fb98d1ec5a77504013e9d46f83912c20 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 16:59:04 -0700 Subject: [PATCH 084/868] Fix copy-paste error in Makefile Signed-off-by: apostasie --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9d9602e1711..7e0638d448b 100644 --- a/Makefile +++ b/Makefile @@ -182,7 +182,7 @@ lint-licenses-all: && GOOS=linux make lint-licenses \ && GOOS=windows make lint-licenses \ && GOOS=freebsd make lint-licenses \ - && GOOS=darwin make lint-go + && GOOS=darwin make lint-licenses $(call footer, $@) ########################## @@ -200,7 +200,7 @@ fix-go-all: && GOOS=linux make fix-go \ && GOOS=windows make fix-go \ && GOOS=freebsd make fix-go \ - && GOOS=darwin make lint-go + && GOOS=darwin make fix-go $(call footer, $@) fix-mod: From 2f8c0c091487c0b973c9c9e77ec28739e922b0ca Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 17:45:53 -0700 Subject: [PATCH 085/868] Bump CI timeout Signed-off-by: apostasie --- .github/workflows/workflow-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 526d19cabf7..b11d4578ed7 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -102,7 +102,7 @@ jobs: canary: true with: - timeout: 45 + timeout: 60 runner: ${{ matrix.runner }} target: ${{ matrix.target }} binary: ${{ matrix.binary && matrix.binary || 'nerdctl' }} From 299b2968da5d796330741b331c1bea90234100c0 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sat, 21 Jun 2025 17:57:00 -0700 Subject: [PATCH 086/868] Move tigron funcs from *testing.T to tig.T Signed-off-by: apostasie --- .../builder/builder_build_oci_layout_test.go | 3 +- cmd/nerdctl/builder/builder_build_test.go | 11 ++-- cmd/nerdctl/compose/compose_config_test.go | 3 +- cmd/nerdctl/compose/compose_cp_linux_test.go | 3 +- .../compose/compose_create_linux_test.go | 7 +-- cmd/nerdctl/compose/compose_rm_linux_test.go | 7 +-- .../compose/compose_start_linux_test.go | 3 +- cmd/nerdctl/compose/compose_up_test.go | 3 +- .../container/container_attach_linux_test.go | 17 ++++--- .../container/container_commit_linux_test.go | 5 +- .../container/container_create_linux_test.go | 7 +-- .../container/container_create_test.go | 3 +- .../container/container_health_check_test.go | 31 +++++------ .../container/container_list_linux_test.go | 3 +- cmd/nerdctl/container/container_logs_test.go | 7 +-- .../container/container_remove_linux_test.go | 6 ++- .../container/container_restart_linux_test.go | 3 +- .../container_run_cgroup_linux_test.go | 17 ++++--- .../container/container_run_linux_test.go | 7 +-- .../container_run_mount_linux_test.go | 3 +- .../container_run_network_linux_test.go | 5 +- .../container_run_soci_linux_test.go | 7 +-- cmd/nerdctl/container/container_run_test.go | 9 ++-- .../container_run_user_linux_test.go | 26 ++++++---- .../container/container_start_linux_test.go | 3 +- cmd/nerdctl/image/image_history_test.go | 11 ++-- cmd/nerdctl/image/image_inspect_test.go | 13 ++--- cmd/nerdctl/image/image_list_test.go | 9 ++-- cmd/nerdctl/image/image_load_test.go | 3 +- cmd/nerdctl/image/image_prune_test.go | 19 +++---- cmd/nerdctl/image/image_pull_linux_test.go | 9 ++-- cmd/nerdctl/image/image_push_linux_test.go | 5 +- cmd/nerdctl/image/image_remove_test.go | 51 ++++++++++--------- cmd/nerdctl/image/image_save_test.go | 5 +- cmd/nerdctl/inspect/inspect_test.go | 3 +- cmd/nerdctl/ipfs/ipfs_compose_linux_test.go | 10 ++-- cmd/nerdctl/ipfs/ipfs_registry_linux_test.go | 3 +- .../network/network_create_linux_test.go | 5 +- cmd/nerdctl/network/network_inspect_test.go | 22 ++++---- .../network/network_list_linux_test.go | 7 +-- .../network/network_remove_linux_test.go | 7 +-- cmd/nerdctl/system/system_info_test.go | 3 +- cmd/nerdctl/system/system_prune_linux_test.go | 3 +- cmd/nerdctl/volume/volume_list_test.go | 27 +++++----- cmd/nerdctl/volume/volume_namespace_test.go | 5 +- cmd/nerdctl/volume/volume_prune_linux_test.go | 5 +- docs/testing/tools.md | 8 +-- mod/tigron/expect/comparators.go | 43 +++++++++------- mod/tigron/internal/mocks/t.go | 9 ++++ mod/tigron/test/case.go | 15 +++--- mod/tigron/test/command.go | 12 ++--- mod/tigron/test/funct.go | 6 ++- mod/tigron/test/helpers.go | 10 ++-- mod/tigron/test/interfaces.go | 5 +- mod/tigron/test/test.go | 6 +-- mod/tigron/tig/t.go | 1 + pkg/testutil/compose.go | 14 +++-- pkg/testutil/nerdtest/command.go | 11 ++-- pkg/testutil/nerdtest/registry/cesanta.go | 11 ++-- pkg/testutil/nerdtest/registry/docker.go | 2 +- pkg/testutil/nerdtest/registry/kubo.go | 2 +- pkg/testutil/nerdtest/test.go | 7 ++- pkg/testutil/nerdtest/utilities.go | 7 +-- 63 files changed, 337 insertions(+), 256 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_oci_layout_test.go b/cmd/nerdctl/builder/builder_build_oci_layout_test.go index 455da80bce7..38ae05004e5 100644 --- a/cmd/nerdctl/builder/builder_build_oci_layout_test.go +++ b/cmd/nerdctl/builder/builder_build_oci_layout_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -100,7 +101,7 @@ CMD ["echo", "test-nerdctl-build-context-oci-layout"]` }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert( t, strings.Contains( diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 9bce97be1cc..a8d9b9fb163 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/platformutil" @@ -342,7 +343,7 @@ COPY %s /`, testFileName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { // Expecting testFileName to exist inside the output target directory assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, @@ -356,7 +357,7 @@ COPY %s /`, testFileName) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } @@ -894,7 +895,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-bom")) assert.NilError(t, err, "failed to read directory") @@ -926,7 +927,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { files, err := os.ReadDir(data.Temp().Path("dir-for-prov")) assert.NilError(t, err, "failed to read directory") @@ -959,7 +960,7 @@ func TestBuildAttestation(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { // Check if any file in the directory matches the SBOM file pattern files, err := os.ReadDir(data.Temp().Path("dir-for-attest")) assert.NilError(t, err, "failed to read directory") diff --git a/cmd/nerdctl/compose/compose_config_test.go b/cmd/nerdctl/compose/compose_config_test.go index 25521331ef4..bb439f7026f 100644 --- a/cmd/nerdctl/compose/compose_config_test.go +++ b/cmd/nerdctl/compose/compose_config_test.go @@ -24,6 +24,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -113,7 +114,7 @@ services: testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, data.Labels().Get("hash") != stdout, "hash should be different") }, } diff --git a/cmd/nerdctl/compose/compose_cp_linux_test.go b/cmd/nerdctl/compose/compose_cp_linux_test.go index f4d5f16af4c..b6fd2aea25b 100644 --- a/cmd/nerdctl/compose/compose_cp_linux_test.go +++ b/cmd/nerdctl/compose/compose_cp_linux_test.go @@ -24,6 +24,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -77,7 +78,7 @@ services: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { copied := data.Temp().Load("test-file2") assert.Equal(t, copied, testFileContent) }, diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 1b6324fb848..4aa88efec05 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -25,6 +25,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -64,7 +65,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") @@ -121,7 +122,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") @@ -133,7 +134,7 @@ services: Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc1", "-a") }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), "stdout should contain `created`") diff --git a/cmd/nerdctl/compose/compose_rm_linux_test.go b/cmd/nerdctl/compose/compose_rm_linux_test.go index 0b673cfa5c8..af876eb2bed 100644 --- a/cmd/nerdctl/compose/compose_rm_linux_test.go +++ b/cmd/nerdctl/compose/compose_rm_linux_test.go @@ -23,6 +23,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -78,7 +79,7 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") comp := expect.Match(regexp.MustCompile("Up|running")) @@ -97,7 +98,7 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") expect.DoesNotContain("wordpress")(wp, t) @@ -114,7 +115,7 @@ volumes: }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") expect.DoesNotContain("db")(db, t) }, diff --git a/cmd/nerdctl/compose/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go index 41d568670a2..2bd5dc11af8 100644 --- a/cmd/nerdctl/compose/compose_start_linux_test.go +++ b/cmd/nerdctl/compose/compose_start_linux_test.go @@ -23,6 +23,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -61,7 +62,7 @@ services: return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") comp := expect.Match(regexp.MustCompile("Up|running")) diff --git a/cmd/nerdctl/compose/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go index 6bf8aeae96d..8821d19f6d2 100644 --- a/cmd/nerdctl/compose/compose_up_test.go +++ b/cmd/nerdctl/compose/compose_up_test.go @@ -25,6 +25,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -94,7 +95,7 @@ services: return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Equal(t, data.Temp().Load("foo", "test"), "hi\n") }, } diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index f9a05c379c6..ee265480c2e 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -28,6 +28,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -64,7 +65,7 @@ func TestAttach(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) @@ -93,7 +94,7 @@ func TestAttach(t *testing.T) { Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -125,7 +126,7 @@ func TestAttachDetachKeys(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) @@ -153,7 +154,7 @@ func TestAttachDetachKeys(t *testing.T) { Errors: []error{errors.New("read detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -182,7 +183,7 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { cmd.Run(&test.Expected{ ExitCode: 0, Errors: []error{errors.New("read detach keys")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, }) @@ -202,7 +203,7 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { ExitCode: 42, Output: expect.All( expect.Contains("markmark"), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier())) }, ), @@ -226,7 +227,7 @@ func TestAttachNoStdin(t *testing.T) { cmd.Feed(bytes.NewReader([]byte{16, 17})) // Ctrl-p, Ctrl-q to detach (https://en.wikipedia.org/wiki/C0_and_C1_control_codes) cmd.Run(&test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) }, }) @@ -243,7 +244,7 @@ func TestAttachNoStdin(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, // Since it's a normal exit and not detach. - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { logs := helpers.Capture("logs", data.Identifier()) assert.Assert(t, !strings.Contains(logs, "should-not-appear")) }, diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index e0adce00c81..5ddbf501643 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -41,7 +42,7 @@ func TestKubeCommitSave(t *testing.T) { nerdtest.KubeCtlCommand(helpers, "wait", "pod", identifier, "--for=condition=ready", "--timeout=1m").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "exec", identifier, "--", "mkdir", "-p", "/tmp/whatever").Run(&test.Expected{}) nerdtest.KubeCtlCommand(helpers, "get", "pods", identifier, "-o", "jsonpath={ .status.containerStatuses[0].containerID }").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { containerID = strings.TrimPrefix(stdout, "containerd://") }, }) @@ -73,7 +74,7 @@ func TestKubeCommitSave(t *testing.T) { cmd = nerdtest.KubeCtlCommand(helpers, "get", "pods", tID, "-o", "jsonpath={ .status.hostIPs[0].ip }") cmd.Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { registryIP = stdout }, }) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index b49aa74a048..1551cdf3555 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -34,6 +34,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -235,7 +236,7 @@ func TestIssue2993(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("is already used by ID")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) @@ -282,7 +283,7 @@ func TestIssue2993(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 0) @@ -363,7 +364,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + 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")) diff --git a/cmd/nerdctl/container/container_create_test.go b/cmd/nerdctl/container/container_create_test.go index da264c46144..394a90ed4d0 100644 --- a/cmd/nerdctl/container/container_create_test.go +++ b/cmd/nerdctl/container/container_create_test.go @@ -26,6 +26,7 @@ import ( "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" @@ -96,7 +97,7 @@ func TestCreateHyperVContainer(t *testing.T) { helpers.Command("container", "inspect", data.Labels().Get("cID")). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go index f45fcef0535..d12661a0f57 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -77,7 +78,7 @@ func TestContainerHealthCheckBasic(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state to be present") @@ -150,7 +151,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -185,7 +186,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -214,7 +215,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -242,7 +243,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -267,7 +268,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) assert.Assert(t, inspect.State.Health == nil, "expected health to be nil with --no-healthcheck") }), @@ -290,7 +291,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_ string, t *testing.T) { + Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -320,7 +321,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -350,7 +351,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -382,7 +383,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_ string, t *testing.T) { + Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -417,7 +418,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_ string, t *testing.T) { + Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -444,7 +445,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(_ string, t *testing.T) { + Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -480,7 +481,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -512,7 +513,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") @@ -540,7 +541,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: expect.All(func(stdout string, t *testing.T) { + Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health assert.Assert(t, h != nil, "expected health state") diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index 14693d256a4..cee48d7eb9e 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -27,6 +27,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/strutil" @@ -688,7 +689,7 @@ func TestContainerListStatusFilter(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("cID")), "No container found with status created") }, } diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index ad984b3166d..0110a9f4cdf 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -189,7 +190,7 @@ bar cmd := helpers.Custom("journalctl", "-xe") cmd.Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { if stdout != "" { works = true } @@ -456,7 +457,7 @@ func TestLogsTailFollowRotate(t *testing.T) { return cmd } - testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout string, t *testing.T) { + testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout string, t tig.T) { tailLogs := strings.Split(strings.TrimSpace(stdout), "\n") for _, line := range tailLogs { if line != "" { @@ -603,7 +604,7 @@ func TestLogsWithStartContainer(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { finalLogsCount := strings.Count(stdout, "foo") initialFooCount, _ := strconv.Atoi(data.Labels().Get("initialFooCount")) assert.Assert(t, finalLogsCount > initialFooCount, "Expected 'foo' count to increase after restart") diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go index 997c3e99c7f..53bf4928242 100644 --- a/cmd/nerdctl/container/container_remove_linux_test.go +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -51,7 +51,8 @@ func testContainerRmIptablesExecutor(data test.Data, helpers test.Helpers) test. if rootlessutil.IsRootless() { // In rootless mode, we need to enter the rootlesskit network namespace if netns, err := rootlessutil.DetachedNetNS(); err != nil { - t.Fatalf("Failed to get detached network namespace: %v", err) + t.Log(fmt.Sprintf("Failed to get detached network namespace: %v", err)) + t.FailNow() } else { if netns != "" { // Use containerd-rootless-setuptool.sh to enter the RootlessKit namespace @@ -85,7 +86,8 @@ func TestContainerRmIptables(t *testing.T) { // Get a free port using portlock port, err := portlock.Acquire(0) if err != nil { - helpers.T().Fatalf("Failed to acquire port: %v", err) + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() } data.Labels().Set("port", strconv.Itoa(port)) diff --git a/cmd/nerdctl/container/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go index a29ba6f1508..954c9760db7 100644 --- a/cmd/nerdctl/container/container_restart_linux_test.go +++ b/cmd/nerdctl/container/container_restart_linux_test.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -153,7 +154,7 @@ func TestRestartWithSignal(t *testing.T) { Output: expect.All( // Check that we saw SIGUSR1 inside the container expect.Contains(nerdtest.SignalCaught), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { // Ensure the container was restarted nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // Check the new pid is different diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index 05d611587ec..e7ea488aa9f 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -35,6 +35,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" @@ -315,7 +316,7 @@ func TestRunDevice(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device) }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { lo1Read, err := os.ReadFile(lo[1].Device) assert.NilError(t, err) assert.Equal(t, string(bytes.Trim(lo1Read, "\x00")), "overwritten-lo1-content") @@ -528,7 +529,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), "150")) }, ), @@ -550,7 +551,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Weight}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "100")) }, @@ -579,7 +580,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "1048576")) }, @@ -608,7 +609,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "2097152")) }, @@ -637,7 +638,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "1000")) }, @@ -666,7 +667,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Rate}}{{end}}", data.Identifier()) assert.Assert(t, strings.Contains(inspectOut, "2000")) }, @@ -701,7 +702,7 @@ func TestRunCPURealTimeSettingCgroupV1(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { rtRuntime := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimeRuntime}}", data.Identifier()) rtPeriod := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimePeriod}}", data.Identifier()) assert.Assert(t, strings.Contains(rtRuntime, "950000")) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index 2d610db9992..527f87d9c72 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -36,6 +36,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -548,7 +549,7 @@ func TestRunWithDetachKeys(t *testing.T) { Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -616,7 +617,7 @@ func TestIssue3568(t *testing.T) { Errors: []error{errors.New("detach keys")}, Output: expect.All( expect.Contains("markmark"), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), @@ -651,7 +652,7 @@ func TestPortBindingWithCustomHost(t *testing.T) { ExitCode: 0, Errors: []error{}, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { resp, err := nettestutil.HTTPGet(address, 30, false) assert.NilError(t, err) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index dc76307529d..f66f62e46b2 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -307,7 +308,7 @@ func TestRunBindMountTmpfs(t *testing.T) { } func mountExistsWithOpt(mountPoint, mountOpt string) test.Comparator { - return func(stdout string, t *testing.T) { + return func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") mountOutput := []string{} for _, line := range lines { diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 7a2e010f73d..b8e0c144f1c 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -40,6 +40,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -423,7 +424,7 @@ func TestRunWithInvalidPortThenCleanUp(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errdefs.ErrInvalidArgument}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { getAddrHash := func(addr string) string { const addrHashLen = 8 @@ -938,7 +939,7 @@ func TestHostNetworkHostName(t *testing.T) { Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { data.Labels().Set("hostHostname", stdout) }, }) diff --git a/cmd/nerdctl/container/container_run_soci_linux_test.go b/cmd/nerdctl/container/container_run_soci_linux_test.go index 16cab356e8e..07ad11a0f50 100644 --- a/cmd/nerdctl/container/container_run_soci_linux_test.go +++ b/cmd/nerdctl/container/container_run_soci_linux_test.go @@ -25,6 +25,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -44,7 +45,7 @@ func TestRunSoci(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Custom("mount").Run(&test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { data.Labels().Set("beforeCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -60,12 +61,12 @@ func TestRunSoci(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var afterCount int beforeCount, _ := strconv.Atoi(data.Labels().Get("beforeCount")) helpers.Custom("mount").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { afterCount = strings.Count(stdout, "fuse.rawBridge") }, }) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 5770aef799b..0eaa72b0ac9 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -37,6 +37,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -156,7 +157,7 @@ func TestRunExitCode(t *testing.T) { Output: expect.All( expect.Match(regexp.MustCompile("Exited [(]123[)][A-Za-z0-9 ]+"+data.Identifier("exit123"))), expect.Match(regexp.MustCompile("Exited [(]0[)][A-Za-z0-9 ]+"+data.Identifier("exit0"))), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit0")).State.Status, "exited") assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit123")).State.Status, "exited") }, @@ -953,7 +954,7 @@ func TestRunHealthcheckFlags(t *testing.T) { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, tc.name) hc := inspect.Config.Healthcheck if tc.expectTest == nil { @@ -1013,7 +1014,7 @@ HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8 Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t *testing.T) { + 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") @@ -1040,7 +1041,7 @@ HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8 Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t *testing.T) { + 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") diff --git a/cmd/nerdctl/container/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go index c583011ade1..f9fcf374c76 100644 --- a/cmd/nerdctl/container/container_run_user_linux_test.go +++ b/cmd/nerdctl/container/container_run_user_linux_test.go @@ -24,6 +24,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -222,10 +223,11 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { - t.Fatalf("Failed to get container host UID: %v", err) + t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) + t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, @@ -249,10 +251,11 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { - t.Fatalf("Failed to get container host UID: %v", err) + t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) + t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, @@ -295,10 +298,11 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { - t.Fatalf("Failed to get container host UID: %v", err) + t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) + t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, @@ -322,10 +326,11 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { - t.Fatalf("Failed to get container host UID: %v", err) + t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) + t.FailNow() } assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) }, @@ -367,10 +372,11 @@ func TestUsernsMappingRunCmd(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) if err != nil { - t.Fatalf("Failed to get container host UID: %v", err) + t.Log(fmt.Sprintf("Failed to get container host UID: %v", err)) + t.FailNow() } assert.Assert(t, actualHostUID == "0") }, diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index 4ef7e5cad9e..b8b82c2d83d 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -27,6 +27,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -67,7 +68,7 @@ func TestStartDetachKeys(t *testing.T) { ExitCode: 0, Errors: []error{errors.New("detach keys")}, Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true")) }, ), diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index e819e31ebfd..62238dc52a3 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -28,6 +28,7 @@ import ( "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/formatter" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -90,7 +91,7 @@ func TestImageHistory(t *testing.T) { { Description: "trunc, no quiet, human", Command: test.Command("image", "history", "--human=true", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, len(history), 2, "history should be 2 in length") @@ -118,7 +119,7 @@ func TestImageHistory(t *testing.T) { { Description: "no human - dates and sizes and not prettyfied", Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, history[0].Size, "0") @@ -130,7 +131,7 @@ func TestImageHistory(t *testing.T) { { Description: "no trunc - do not truncate sha or cmd", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") @@ -140,14 +141,14 @@ func TestImageHistory(t *testing.T) { { Description: "Quiet has no effect with format, so, go no-json, no-trunc", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") }), }, { Description: "With quiet, trunc has no effect", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") }), }, diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 55735d28894..5ee549686c6 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -28,6 +28,7 @@ import ( "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" @@ -45,7 +46,7 @@ func TestImageInspectSimpleCases(t *testing.T) { { Description: "Contains some stuff", Command: test.Command("image", "inspect", testutil.CommonImage), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -115,7 +116,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -140,7 +141,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -173,7 +174,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -196,7 +197,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -218,7 +219,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { Command: test.Command("image", "inspect", "busybox", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index a61043206e5..7a19e552c08 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -50,7 +51,7 @@ func TestImages(t *testing.T) { Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" @@ -81,7 +82,7 @@ func TestImages(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(testutil.CommonImage), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") @@ -107,7 +108,7 @@ func TestImages(t *testing.T) { Command: test.Command("images", "--format", "'{{json .CreatedAt}}'"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") createdTimes := lines @@ -337,7 +338,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { Command: test.Command("--kube-hide-dupe", "images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var imageID string var skipLine int lines := strings.Split(strings.TrimSpace(stdout), "\n") diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 4a979994c4b..2618b81c64f 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -61,7 +62,7 @@ func TestLoadStdinFromPipe(t *testing.T) { return &test.Expected{ Output: expect.All( expect.Contains(fmt.Sprintf("Loaded image: %s:latest", identifier)), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("images"), identifier)) }, ), diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index cb16b81d335..ca3cbf62e95 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -84,10 +85,10 @@ func TestImagePrune(t *testing.T) { identifier := data.Identifier() return &test.Expected{ Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, identifier)) }, - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, ""), imgList) assert.Assert(t, strings.Contains(imgList, identifier)) @@ -129,10 +130,10 @@ func TestImagePrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier())) assert.Assert(t, !strings.Contains(imgList, ""), imgList) @@ -169,14 +170,14 @@ LABEL version=0.1`, testutil.CommonImage) Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { assert.Assert(t, !strings.Contains(stdout, data.Identifier())) }, - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier())) }, - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") assert.Assert(t, strings.Contains(prune, data.Identifier())) imgList := helpers.Capture("images") @@ -210,7 +211,7 @@ CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("imageID")), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Labels().Get("imageID"))) }, @@ -229,7 +230,7 @@ CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("imageID")), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, !strings.Contains(imgList, data.Labels().Get("imageID")), imgList) }, diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index d409ed94e27..31d022a264d 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -182,7 +183,7 @@ func TestImagePullSoci(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -196,7 +197,7 @@ func TestImagePullSoci(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, @@ -218,7 +219,7 @@ func TestImagePullSoci(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { cmd := helpers.Custom("mount") cmd.Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) @@ -232,7 +233,7 @@ func TestImagePullSoci(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 355fad17c7b..3a86a123c19 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -27,6 +27,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -200,7 +201,7 @@ func TestPush(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") @@ -232,7 +233,7 @@ func TestPush(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index c35258518ad..6e3f4ad3e36 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -63,7 +64,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -83,7 +84,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) @@ -108,7 +109,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -140,7 +141,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) @@ -162,7 +163,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -184,7 +185,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ // a created container with removed image doesn't impact other `rmi` command Output: expect.DoesNotContain(repoName, nginxRepoName), @@ -212,7 +213,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -246,7 +247,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(""), }) @@ -272,7 +273,7 @@ func TestRemove(t *testing.T) { return &test.Expected{ ExitCode: 1, Errors: []error{errors.New("image is being used")}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.Contains(repoName), }) @@ -293,7 +294,7 @@ func TestRemove(t *testing.T) { Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ Output: expect.DoesNotContain(repoName), }) @@ -336,10 +337,10 @@ func TestIssue3016(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("images", data.Labels().Get(tagIDKey)).Run(&test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Equal(t, len(strings.Split(stdout, "\n")), 2) }, }) @@ -378,15 +379,15 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags+1) }, @@ -410,15 +411,15 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags+1) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags+2) }, @@ -440,15 +441,15 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numTags) }, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, @@ -469,7 +470,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Command("--kube-hide-dupe", "rmi", stdout[0:12]).Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("multiple IDs found with provided prefix: ")}, @@ -478,7 +479,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, @@ -499,7 +500,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { imgID := strings.Split(stdout, "\n") helpers.Command("--kube-hide-dupe", "rmi", imgID[0]).Run(&test.Expected{ ExitCode: 1, @@ -509,7 +510,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { ExitCode: 0, }) helpers.Command("images").Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) == numNoTags) }, diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 31315cd1272..7b97f523761 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -48,7 +49,7 @@ func TestSaveContent(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) assert.NilError(t, err) @@ -188,7 +189,7 @@ func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: []error{}, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Equal(t, strings.Count(stdout, data.Labels().Get("id")), 2) }, } diff --git a/cmd/nerdctl/inspect/inspect_test.go b/cmd/nerdctl/inspect/inspect_test.go index e0e1b6167fa..8047923efa8 100644 --- a/cmd/nerdctl/inspect/inspect_test.go +++ b/cmd/nerdctl/inspect/inspect_test.go @@ -23,6 +23,7 @@ import ( "gotest.tools/v3/assert" "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" @@ -50,7 +51,7 @@ func TestInspectSimpleCase(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var inspectResult []json.RawMessage err := json.Unmarshal([]byte(stdout), &inspectResult) assert.NilError(t, err, "Unable to unmarshal output\n") diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index c75653b1dca..c9ee23631b7 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -254,12 +255,12 @@ COPY index.html /usr/share/nginx/html/index.html testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) - t.Logf("respBody=%q", respBody) + t.Log(fmt.Sprintf("respBody=%q", respBody)) assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml"))) }, } @@ -319,8 +320,9 @@ func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, o if !wordpressWorking { ccc := helpers.Capture("ps", "-a") helpers.T().Log(ccc) - helpers.T().Error(helpers.Err("logs", projectName+"-wordpress-1")) - helpers.T().Fatalf("wordpress is not working %v", err) + helpers.T().Log(helpers.Err("logs", projectName+"-wordpress-1")) + helpers.T().Log(fmt.Sprintf("wordpress is not working %v", err)) + helpers.T().FailNow() } helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v") diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index 10b64b902b1..993c9388ec6 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -39,7 +40,7 @@ func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { cmd := helpers.Command("push", "ipfs://"+name) cmd.WithArgs(opts...) cmd.Run(&test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { lines := strings.Split(stdout, "\n") assert.Equal(t, len(lines) >= 2, true) ipfsCID = lines[len(lines)-2] diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index d044631399d..6d42809f2ff 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -26,6 +26,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -58,7 +59,7 @@ func TestNetworkCreate(t *testing.T) { return &test.Expected{ ExitCode: 0, Errors: nil, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet"))) assert.Assert(t, !strings.Contains(data.Labels().Get("container2"), data.Labels().Get("subnet"))) }, @@ -98,7 +99,7 @@ func TestNetworkCreate(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { _, subnet, _ := net.ParseCIDR(data.Labels().Get("subnetStr")) ip := nerdtest.FindIPv6(stdout) assert.Assert(t, subnet.Contains(ip), fmt.Sprintf("subnet %s contains ip %s", subnet, ip)) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index d8d69852e24..fc2e18e41e4 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -70,7 +70,7 @@ func TestNetworkInspect(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 *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -82,7 +82,7 @@ func TestNetworkInspect(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 *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -94,7 +94,7 @@ func TestNetworkInspect(t *testing.T) { Description: "bridge", Require: require.Not(require.Windows), Command: test.Command("network", "inspect", "bridge"), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -106,7 +106,7 @@ func TestNetworkInspect(t *testing.T) { Description: "nat", Require: require.Windows, Command: test.Command("network", "inspect", "nat"), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -123,7 +123,7 @@ func TestNetworkInspect(t *testing.T) { helpers.Anyhow("network", "remove", "custom") }, Command: test.Command("network", "inspect", "custom"), - Expected: test.Expects(0, nil, func(stdout string, t *testing.T) { + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -140,7 +140,7 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -161,7 +161,7 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -189,7 +189,7 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") @@ -216,7 +216,7 @@ func TestNetworkInspect(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) @@ -249,7 +249,7 @@ func TestNetworkInspect(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need // to retrieve the binary name. // Note that we know this works already, so no need to assert err. @@ -308,7 +308,7 @@ func TestNetworkInspect(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) assert.NilError(t, err, "Unable to unmarshal output\n") diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index 55cfb599cc2..fbc374d6f4d 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -23,6 +23,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -52,7 +53,7 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ @@ -74,7 +75,7 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least one line\n") netNames := map[string]struct{}{ @@ -96,7 +97,7 @@ func TestNetworkLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1) netNames := map[string]struct{}{ diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go index 2dd0e2172b0..8e640c7a482 100644 --- a/cmd/nerdctl/network/network_remove_linux_test.go +++ b/cmd/nerdctl/network/network_remove_linux_test.go @@ -24,6 +24,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -55,7 +56,7 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, @@ -96,7 +97,7 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, @@ -122,7 +123,7 @@ func TestNetworkRemove(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found") }, diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go index eedef027503..e5e4d6e5e88 100644 --- a/cmd/nerdctl/system/system_info_test.go +++ b/cmd/nerdctl/system/system_info_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" @@ -34,7 +35,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -func testInfoComparator(stdout string, t *testing.T) { +func testInfoComparator(stdout string, t tig.T) { var dinf dockercompat.Info err := json.Unmarshal([]byte(stdout), &dinf) assert.NilError(t, err, "failed to unmarshal stdout") diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go index 163993f791a..7719ca7a373 100644 --- a/cmd/nerdctl/system/system_prune_linux_test.go +++ b/cmd/nerdctl/system/system_prune_linux_test.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -60,7 +61,7 @@ func TestSystemPrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 0, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { volumes := helpers.Capture("volume", "ls") networks := helpers.Capture("network", "ls") images := helpers.Capture("images") diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index 78a36e83bbb..22832e1d4ad 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -26,6 +26,7 @@ import ( "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/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -56,7 +57,7 @@ func TestVolumeLsSize(t *testing.T) { Command: test.Command("volume", "ls", "--size"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volSizes := map[string]string{ @@ -145,7 +146,7 @@ func TestVolumeLsFilter(t *testing.T) { Command: test.Command("volume", "ls", "--quiet"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines") volNames := map[string]struct{}{ @@ -174,7 +175,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ @@ -197,7 +198,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 lines") volNames := map[string]struct{}{ @@ -218,7 +219,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } @@ -231,7 +232,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result") }, } @@ -244,7 +245,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ @@ -266,7 +267,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ @@ -287,7 +288,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 1, "expected at least 1 line") volNames := map[string]struct{}{ @@ -308,7 +309,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "expected at least 2 lines") volNames := map[string]struct{}{ @@ -331,7 +332,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ @@ -362,7 +363,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ @@ -393,7 +394,7 @@ func TestVolumeLsFilter(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 3, "expected at least 3 lines") volNames := map[string]struct{}{ diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go index 468d1f3b96b..e91bc17c12b 100644 --- a/cmd/nerdctl/volume/volume_namespace_test.go +++ b/cmd/nerdctl/volume/volume_namespace_test.go @@ -23,6 +23,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -76,7 +77,7 @@ func TestVolumeNamespace(t *testing.T) { return &test.Expected{ Output: expect.All( expect.DoesNotContain(data.Labels().Get("root_volume")), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, ), @@ -94,7 +95,7 @@ func TestVolumeNamespace(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("root_volume")) helpers.Ensure("volume", "rm", data.Labels().Get("root_volume")) helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go index d7982dd7403..db81d1a1be4 100644 --- a/cmd/nerdctl/volume/volume_prune_linux_test.go +++ b/cmd/nerdctl/volume/volume_prune_linux_test.go @@ -22,6 +22,7 @@ import ( "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/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -75,7 +76,7 @@ func TestVolumePrune(t *testing.T) { data.Labels().Get("namedBusy"), data.Labels().Get("namedDangling"), ), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) @@ -96,7 +97,7 @@ func TestVolumePrune(t *testing.T) { Output: expect.All( expect.DoesNotContain(data.Labels().Get("anonIDBusy"), data.Labels().Get("namedBusy")), expect.Contains(data.Labels().Get("anonIDDangling"), data.Labels().Get("namedDangling")), - func(stdout string, t *testing.T) { + func(stdout string, t tig.T) { helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) diff --git a/docs/testing/tools.md b/docs/testing/tools.md index e44257f18c9..ec1eb9056a7 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -88,7 +88,7 @@ import ( ) func MyComparator(compare string) test.Comparator { - return func(stdout string, t *testing.T) { + return func(stdout string, t tig.T) { t.Helper() assert.Assert(t, stdout == compare) } @@ -138,7 +138,7 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } @@ -251,7 +251,7 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } @@ -340,7 +340,7 @@ func TestMyThing(t *testing.T) { errors.New("foobla"), errdefs.ErrNotFound, }, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { assert.Assert(t, stdout == data.Labels().Get("sometestdata")) }, } diff --git a/mod/tigron/expect/comparators.go b/mod/tigron/expect/comparators.go index ac4492de822..fa004051f8e 100644 --- a/mod/tigron/expect/comparators.go +++ b/mod/tigron/expect/comparators.go @@ -15,13 +15,12 @@ */ //revive:disable:package-comments // annoying false positive behavior -//nolint:thelper // FIXME: remove when we move to tig.T + package expect import ( "encoding/json" "regexp" - "testing" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" "github.com/containerd/nerdctl/mod/tigron/test" @@ -30,7 +29,7 @@ import ( // All can be used as a parameter for expected.Output to group a set of comparators. func All(comparators ...test.Comparator) test.Comparator { - return func(stdout string, t *testing.T) { + return func(stdout string, t tig.T) { t.Helper() for _, comparator := range comparators { @@ -42,33 +41,43 @@ func All(comparators ...test.Comparator) test.Comparator { // Contains can be used as a parameter for expected.Output and ensures a comparison string is found contained in the // output. func Contains(compare string, more ...string) test.Comparator { - return func(stdout string, t *testing.T) { - t.Helper() + return func(stdout string, testing tig.T) { + testing.Helper() - assertive.Contains(assertive.WithFailLater(t), stdout, compare, "Inspecting output (contains)") + assertive.Contains(assertive.WithFailLater(testing), stdout, compare, "Inspecting output (contains)") for _, m := range more { - assertive.Contains(assertive.WithFailLater(t), stdout, m, "Inspecting output (contains)") + assertive.Contains(assertive.WithFailLater(testing), stdout, m, "Inspecting output (contains)") } } } // DoesNotContain is to be used for expected.Output to ensure a comparison string is NOT found in the output. func DoesNotContain(compare string, more ...string) test.Comparator { - return func(stdout string, t *testing.T) { - t.Helper() + return func(stdout string, testing tig.T) { + testing.Helper() - assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, "Inspecting output (does not contain)") + assertive.DoesNotContain( + assertive.WithFailLater(testing), + stdout, + compare, + "Inspecting output (does not contain)", + ) for _, m := range more { - assertive.DoesNotContain(assertive.WithFailLater(t), stdout, m, "Inspecting output (does not contain)") + assertive.DoesNotContain( + assertive.WithFailLater(testing), + stdout, + m, + "Inspecting output (does not contain)", + ) } } } // Equals is to be used for expected.Output to ensure it is exactly the output. func Equals(compare string) test.Comparator { - return func(stdout string, t *testing.T) { + return func(stdout string, t tig.T) { t.Helper() assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, "Inspecting output (equals)") } @@ -76,7 +85,7 @@ func Equals(compare string) test.Comparator { // Match is to be used for expected.Output to ensure we match a regexp. func Match(reg *regexp.Regexp) test.Comparator { - return func(stdout string, t *testing.T) { + return func(stdout string, t tig.T) { t.Helper() assertive.Match(assertive.WithFailLater(t), stdout, reg, "Inspecting output (match)") } @@ -85,14 +94,14 @@ func Match(reg *regexp.Regexp) test.Comparator { // JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided // method. func JSON[T any](obj T, verifier func(T, tig.T)) test.Comparator { - return func(stdout string, t *testing.T) { - t.Helper() + return func(stdout string, testing tig.T) { + testing.Helper() err := json.Unmarshal([]byte(stdout), &obj) - assertive.ErrorIsNil(assertive.WithSilentSuccess(t), err, "Unmarshalling JSON from stdout must succeed") + assertive.ErrorIsNil(assertive.WithSilentSuccess(testing), err, "Unmarshalling JSON from stdout must succeed") if verifier != nil && err == nil { - verifier(obj, t) + verifier(obj, testing) } } } diff --git a/mod/tigron/internal/mocks/t.go b/mod/tigron/internal/mocks/t.go index 7665fd4fe3b..281913e0213 100644 --- a/mod/tigron/internal/mocks/t.go +++ b/mod/tigron/internal/mocks/t.go @@ -48,6 +48,9 @@ type ( TTempDirIn struct{} TTempDirOut = string + + TSkipIn []any + TSkipOut struct{} ) type MockT struct { @@ -93,3 +96,9 @@ func (m *MockT) TempDir() string { return "" } + +func (m *MockT) Skip(args ...any) { + if handler := m.Retrieve(); handler != nil { + handler.(mimicry.Function[TSkipIn, TSkipOut])(args) + } +} diff --git a/mod/tigron/test/case.go b/mod/tigron/test/case.go index 67846dfbb0f..f4c9c090d16 100644 --- a/mod/tigron/test/case.go +++ b/mod/tigron/test/case.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/internal/assertive" "github.com/containerd/nerdctl/mod/tigron/internal/formatter" + "github.com/containerd/nerdctl/mod/tigron/tig" ) // Case describes an entire test-case, including data, setup and cleanup routines, command and @@ -63,7 +64,7 @@ type Case struct { // Private helpers Helpers - t *testing.T + t tig.T parent *Case } @@ -151,7 +152,7 @@ func (test *Case) Run(t *testing.T) { if test.Require != nil { shouldRun, message := test.Require.Check(test.Data, test.helpers) if !shouldRun { - test.t.Skipf("test skipped as: %s", message) + test.t.Skip("test skipped as: " + message) } if test.Require.Setup != nil { @@ -180,7 +181,7 @@ func (test *Case) Run(t *testing.T) { // Set parallel unless asked not to if !test.NoParallel { - test.t.Parallel() + subT.Parallel() } // Execute cleanups now @@ -197,7 +198,7 @@ func (test *Case) Run(t *testing.T) { } // Register the cleanups, in reverse - test.t.Cleanup(func() { + subT.Cleanup(func() { test.t.Helper() test.t.Log( "\n\n" + formatter.Table( @@ -263,14 +264,14 @@ func (test *Case) Run(t *testing.T) { if len(test.SubTests) > 0 { // Now go for the subtests - test.t.Logf("\n%s️ %q: into subtests prep", subinDecorator, test.t.Name()) + test.t.Log(fmt.Sprintf("\n%s️ %q: into subtests prep", subinDecorator, test.t.Name())) for _, subTest := range test.SubTests { subTest.parent = test - subTest.Run(test.t) + subTest.Run(subT) } - test.t.Logf("\n%s️ %q: done with subtests prep", suboutDecorator, test.t.Name()) + test.t.Log(fmt.Sprintf("\n%s️ %q: done with subtests prep", suboutDecorator, test.t.Name())) } } diff --git a/mod/tigron/test/command.go b/mod/tigron/test/command.go index 546c4fb7614..e146b5c9e80 100644 --- a/mod/tigron/test/command.go +++ b/mod/tigron/test/command.go @@ -23,13 +23,13 @@ import ( "os" "strconv" "strings" - "testing" "time" "github.com/containerd/nerdctl/mod/tigron/internal" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" "github.com/containerd/nerdctl/mod/tigron/internal/com" "github.com/containerd/nerdctl/mod/tigron/internal/formatter" + "github.com/containerd/nerdctl/mod/tigron/tig" ) const ( @@ -59,7 +59,7 @@ type CustomizableCommand interface { // default it pass any that is defined by WithEnv WithBlacklist(env []string) // T returns the current testing object - T() *testing.T + T() tig.T // withEnv *copies* the passed map to the environment of the command to be executed // Note that this will override any variable defined in the embedding environment @@ -69,7 +69,7 @@ type CustomizableCommand interface { withTempDir(path string) // WithConfig allows passing custom config properties from the test to the base command withConfig(config Config) - withT(t *testing.T) + withT(t tig.T) // Clear does a clone, but will clear binary and arguments while retaining the env, or any other // custom properties Gotcha: if genericCommand is embedded with a custom Run and an overridden // clear to return the embedding type the result will be the embedding command, no longer the @@ -102,7 +102,7 @@ type GenericCommand struct { TempDir string Env map[string]string - t *testing.T + t tig.T cmd *com.Command async bool @@ -337,7 +337,7 @@ func (gc *GenericCommand) Clone() TestableCommand { return &clone } -func (gc *GenericCommand) T() *testing.T { +func (gc *GenericCommand) T() tig.T { return gc.t } @@ -361,7 +361,7 @@ func (gc *GenericCommand) clear() TestableCommand { return &comcopy } -func (gc *GenericCommand) withT(t *testing.T) { +func (gc *GenericCommand) withT(t tig.T) { t.Helper() gc.t = t } diff --git a/mod/tigron/test/funct.go b/mod/tigron/test/funct.go index ba36e55a3ba..f2be434e851 100644 --- a/mod/tigron/test/funct.go +++ b/mod/tigron/test/funct.go @@ -16,7 +16,9 @@ package test -import "testing" +import ( + "github.com/containerd/nerdctl/mod/tigron/tig" +) // An Evaluator is a function that decides whether a test should run or not. type Evaluator func(data Data, helpers Helpers) (bool, string) @@ -30,7 +32,7 @@ type Butler func(data Data, helpers Helpers) // - move to tig.T // A Comparator is the function signature to implement for the Output property of an Expected. -type Comparator func(stdout string, t *testing.T) +type Comparator func(stdout string, t tig.T) // A Manager is the function signature meant to produce expectations for a command. type Manager func(data Data, helpers Helpers) *Expected diff --git a/mod/tigron/test/helpers.go b/mod/tigron/test/helpers.go index 9d5e069baa7..ce5c48cbea2 100644 --- a/mod/tigron/test/helpers.go +++ b/mod/tigron/test/helpers.go @@ -17,9 +17,8 @@ package test import ( - "testing" - "github.com/containerd/nerdctl/mod/tigron/internal" + "github.com/containerd/nerdctl/mod/tigron/tig" ) // This is the implementation of Helpers @@ -27,7 +26,7 @@ import ( type helpersInternal struct { cmdInternal CustomizableCommand - t *testing.T + t tig.T } // Ensure will run a command and make sure it is successful. @@ -60,8 +59,7 @@ func (help *helpersInternal) Capture(args ...string) string { help.t.Helper() help.Command(args...).Run(&Expected{ - //nolint:thelper - Output: func(stdout string, _ *testing.T) { + Output: func(stdout string, _ tig.T) { ret = stdout }, }) @@ -104,6 +102,6 @@ func (help *helpersInternal) Write(key ConfigKey, value ConfigValue) { help.cmdInternal.write(key, value) } -func (help *helpersInternal) T() *testing.T { +func (help *helpersInternal) T() tig.T { return help.t } diff --git a/mod/tigron/test/interfaces.go b/mod/tigron/test/interfaces.go index 12df876747a..c7fefc95eb0 100644 --- a/mod/tigron/test/interfaces.go +++ b/mod/tigron/test/interfaces.go @@ -19,8 +19,9 @@ package test import ( "io" "os" - "testing" "time" + + "github.com/containerd/nerdctl/mod/tigron/tig" ) // DataLabels holds key-value test information set by the test authors. @@ -93,7 +94,7 @@ type Helpers interface { Write(key ConfigKey, value ConfigValue) // T returns the current testing object. - T() *testing.T + T() tig.T } // The TestableCommand interface represents a low-level command to execute, typically to be compared diff --git a/mod/tigron/test/test.go b/mod/tigron/test/test.go index 274a783b8b7..e83f05f86ed 100644 --- a/mod/tigron/test/test.go +++ b/mod/tigron/test/test.go @@ -17,13 +17,13 @@ package test import ( - "testing" + "github.com/containerd/nerdctl/mod/tigron/tig" ) // Testable TODO. type Testable interface { - CustomCommand(testCase *Case, t *testing.T) CustomizableCommand - AmbientRequirements(testCase *Case, t *testing.T) + CustomCommand(testCase *Case, t tig.T) CustomizableCommand + AmbientRequirements(testCase *Case, t tig.T) } // FIXME diff --git a/mod/tigron/tig/t.go b/mod/tigron/tig/t.go index f6256b72404..68293509731 100644 --- a/mod/tigron/tig/t.go +++ b/mod/tigron/tig/t.go @@ -37,4 +37,5 @@ type T interface { Log(args ...any) Name() string TempDir() string + Skip(args ...any) } diff --git a/pkg/testutil/compose.go b/pkg/testutil/compose.go index 2b00cf58b2f..e247dcc7d60 100644 --- a/pkg/testutil/compose.go +++ b/pkg/testutil/compose.go @@ -18,25 +18,28 @@ package testutil import ( "context" + "fmt" "os" "path/filepath" - "testing" "github.com/compose-spec/compose-go/v2/loader" compose "github.com/compose-spec/compose-go/v2/types" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" ) type ComposeDir struct { - t testing.TB + t tig.T dir string yamlBasePath string } func (cd *ComposeDir) WriteFile(name, content string) { if err := filesystem.WriteFile(filepath.Join(cd.dir, name), []byte(content), 0644); err != nil { - cd.t.Fatal(err) + cd.t.Log(fmt.Sprintf("Failed to create file %v", err)) + cd.t.FailNow() } } @@ -56,10 +59,11 @@ func (cd *ComposeDir) CleanUp() { os.RemoveAll(cd.dir) } -func NewComposeDir(t testing.TB, dockerComposeYAML string) *ComposeDir { +func NewComposeDir(t tig.T, dockerComposeYAML string) *ComposeDir { tmpDir, err := os.MkdirTemp("", "nerdctl-compose-test") if err != nil { - t.Fatal(err) + t.Log(fmt.Sprintf("Failed to create temp dir: %v", err)) + t.FailNow() } cd := &ComposeDir{ t: t, diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go index 4f0eca550d1..6dbad324347 100644 --- a/pkg/testutil/nerdtest/command.go +++ b/pkg/testutil/nerdtest/command.go @@ -17,15 +17,16 @@ package nerdtest import ( + "fmt" "os" "os/exec" "path/filepath" "strings" - "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -49,7 +50,7 @@ func isTargetNerdish() bool { return !strings.HasPrefix(filepath.Base(testutil.GetTarget()), "docker") } -func newNerdCommand(conf test.Config, t *testing.T) *nerdCommand { +func newNerdCommand(conf test.Config, t tig.T) *nerdCommand { // Decide what binary we are running var err error var binary string @@ -57,7 +58,8 @@ func newNerdCommand(conf test.Config, t *testing.T) *nerdCommand { binary, err = exec.LookPath(trgt) if err != nil { - t.Fatalf("unable to find binary %q: %v", trgt, err) + t.Log(fmt.Sprintf("unable to find binary %q: %v", trgt, err)) + t.FailNow() } if isTargetNerdish() { @@ -69,7 +71,8 @@ func newNerdCommand(conf test.Config, t *testing.T) *nerdCommand { } } else { if err = exec.Command(binary, "compose", "version").Run(); err != nil { - t.Fatalf("docker does not support compose: %v", err) + t.Log(fmt.Sprintf("docker does not support compose: %v", err)) + t.FailNow() } } diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go index 2d772cbf606..2bed64880af 100644 --- a/pkg/testutil/nerdtest/registry/cesanta.go +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -22,7 +22,6 @@ import ( "net" "os" "strconv" - "testing" "time" "golang.org/x/crypto/bcrypt" @@ -31,6 +30,7 @@ import ( "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/mod/tigron/utils/testca" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" @@ -95,7 +95,7 @@ func ensureContainerStarted(helpers test.Helpers, con string) { helpers.Command("container", "inspect", con). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { @@ -115,7 +115,8 @@ func ensureContainerStarted(helpers test.Helpers, con string) { helpers.T().Log(ins) helpers.T().Log(lgs) helpers.T().Log(ps) - helpers.T().Fatalf("container %s still not running after %d retries", con, 5) + helpers.T().Log(fmt.Sprintf("container %s still not running after %d retries", con, 5)) + helpers.T().FailNow() } } @@ -178,7 +179,7 @@ func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *testca.Cert, helpers.Ensure("rm", "-f", containerName) errPortRelease := portlock.Release(port) if errPortRelease != nil { - helpers.T().Error(errPortRelease.Error()) + helpers.T().Log(fmt.Sprintf("Failed to release port %d: %s", port, errPortRelease)) } } @@ -218,7 +219,7 @@ func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *testca.Cert, Setup: setup, Cleanup: cleanup, Logs: func(data test.Data, helpers test.Helpers) { - helpers.T().Error(helpers.Err("logs", containerName)) + helpers.T().Log(helpers.Err("logs", containerName)) }, } } diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go index 27bd9069ff1..0a38174002f 100644 --- a/pkg/testutil/nerdtest/registry/docker.go +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -135,7 +135,7 @@ func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *testca.C Cleanup: cleanup, Setup: setup, Logs: func(data test.Data, helpers test.Helpers) { - helpers.T().Error(helpers.Err("logs", containerName)) + helpers.T().Log(helpers.Err("logs", containerName)) }, HostsDir: hostsDir, } diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go index 40c0f67f798..eb108813906 100644 --- a/pkg/testutil/nerdtest/registry/kubo.go +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -85,7 +85,7 @@ func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, current Cleanup: cleanup, Setup: setup, Logs: func(data test.Data, helpers test.Helpers) { - helpers.T().Error(helpers.Err("logs", containerName)) + helpers.T().Log(helpers.Err("logs", containerName)) }, } } diff --git a/pkg/testutil/nerdtest/test.go b/pkg/testutil/nerdtest/test.go index 94a42c459de..f9ef3321f91 100644 --- a/pkg/testutil/nerdtest/test.go +++ b/pkg/testutil/nerdtest/test.go @@ -17,9 +17,8 @@ package nerdtest import ( - "testing" - "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" ) var DockerConfig test.ConfigKey = "DockerConfig" @@ -39,11 +38,11 @@ func Setup() *test.Case { type nerdctlSetup struct { } -func (ns *nerdctlSetup) CustomCommand(testCase *test.Case, t *testing.T) test.CustomizableCommand { +func (ns *nerdctlSetup) CustomCommand(testCase *test.Case, t tig.T) test.CustomizableCommand { return newNerdCommand(testCase.Config, t) } -func (ns *nerdctlSetup) AmbientRequirements(testCase *test.Case, t *testing.T) { +func (ns *nerdctlSetup) AmbientRequirements(testCase *test.Case, t tig.T) { // Ambient requirements, bail out now if these do not match if environmentHasIPv6() && testCase.Config.Read(ipv6) != only { t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index 78bfbe6fcc5..ca70166b725 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -18,10 +18,10 @@ package nerdtest import ( "encoding/json" + "fmt" "net" "path/filepath" "strings" - "testing" "time" "gotest.tools/v3/assert" @@ -113,7 +113,7 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { helpers.Command("container", "inspect", con). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, - Output: func(stdout string, t *testing.T) { + Output: func(stdout string, t tig.T) { var dc []dockercompat.Container err := json.Unmarshal([]byte(stdout), &dc) if err != nil || len(dc) == 0 { @@ -133,7 +133,8 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { helpers.T().Log(ins) helpers.T().Log(lgs) helpers.T().Log(ps) - helpers.T().Fatalf("container %s still not running after %d retries", con, maxRetry) + helpers.T().Log(fmt.Sprintf("container %s still not running after %d retries", con, maxRetry)) + helpers.T().FailNow() } } From df3df359a12fafa1638510219d2e3b8484deedf0 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 21:55:01 -0700 Subject: [PATCH 087/868] Ensure container started on healthcheck tests Signed-off-by: apostasie --- .../container/container_health_check_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go index 66d60e30705..76a27db5b5a 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -50,6 +50,7 @@ func TestContainerHealthCheckBasic(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -67,6 +68,7 @@ func TestContainerHealthCheckBasic(t *testing.T) { "--health-interval", "45s", "--health-timeout", "30s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -95,6 +97,7 @@ func TestContainerHealthCheckBasic(t *testing.T) { "--health-cmd", "echo healthy", "--health-interval", "3s", testutil.CommonImage, "sleep", "2") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) helpers.Ensure("stop", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -140,6 +143,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-timeout", "2s", "--health-interval", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -170,6 +174,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-interval", "1s", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -204,6 +209,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-start-period", "5s", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -232,6 +238,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-interval", "1s", "--health-retries", "1", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -257,6 +264,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--no-healthcheck", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -280,6 +288,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "echo shell-format", "--health-interval", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -310,6 +319,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-interval", "1s", "--health-timeout", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -340,6 +350,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-interval", "1s", "--health-timeout", "1s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -368,6 +379,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-cmd", "yes X | head -c 60000", "--health-interval", "1s", "--health-timeout", "2s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -403,6 +415,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-interval", "1s", "--health-retries", "1", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -434,6 +447,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-cmd", "yes X | head -c 1048576", // 1MB output "--health-interval", "1s", "--health-timeout", "2s", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -466,6 +480,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-timeout", "10s", "--health-retries", "3", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -497,6 +512,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { "--health-cmd", "ls /foo || exit 1", "--health-retries", "2", "--health-start-period", "30s", // long enough to stay in "starting" testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -528,6 +544,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "ls || exit 1", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) From d232567a66d103ef105ea7e4ed52be48a8262ed8 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 22:08:47 -0700 Subject: [PATCH 088/868] Pass along GITHUB_TOKEN Signed-off-by: apostasie --- .github/workflows/job-test-in-container.yml | 2 ++ .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 2257de5197d..32a0088822d 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -81,6 +81,8 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - if: ${{ inputs.canary }} name: "Init (canary): prepare updated test image" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | . ./hack/build-integration-canary.sh canary::build::integration diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1881d185058..55cf55111f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,8 @@ jobs: go-version: "1.24" check-latest: true - name: "Compile binaries" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: make artifacts - name: "SHA256SUMS" run: | From df145ce24575d2519addf154e18cdf54aa0fa0eb Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 20 Jun 2025 15:51:59 +0800 Subject: [PATCH 089/868] commit: Add an option to nerdctl commit command for media type selection Fixes: #4329 Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_commit.go | 11 ++++ docs/command-reference.md | 1 + pkg/api/types/container_types.go | 13 +++- pkg/cmd/container/commit.go | 1 + pkg/imgutil/commit/commit.go | 75 ++++++++++++++++++----- 5 files changed, 86 insertions(+), 15 deletions(-) diff --git a/cmd/nerdctl/container/container_commit.go b/cmd/nerdctl/container/container_commit.go index 62dbcced157..2d58e23008b 100644 --- a/cmd/nerdctl/container/container_commit.go +++ b/cmd/nerdctl/container/container_commit.go @@ -43,6 +43,7 @@ func CommitCommand() *cobra.Command { cmd.Flags().StringArrayP("change", "c", nil, "Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT])") cmd.Flags().BoolP("pause", "p", true, "Pause container during commit") cmd.Flags().StringP("compression", "", "gzip", "commit compression algorithm (zstd or gzip)") + cmd.Flags().String("format", "docker", "Format of the committed image (docker or oci)") return cmd } @@ -76,6 +77,15 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { if com != string(types.Zstd) && com != string(types.Gzip) { return types.ContainerCommitOptions{}, errors.New("--compression param only supports zstd or gzip") } + + format, err := cmd.Flags().GetString("format") + if err != nil { + return types.ContainerCommitOptions{}, err + } + if format != string(types.ImageFormatDocker) && format != string(types.ImageFormatOCI) { + return types.ContainerCommitOptions{}, errors.New("--format param only supports docker or oci") + } + return types.ContainerCommitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, @@ -84,6 +94,7 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { Pause: pause, Change: change, Compression: types.CompressionType(com), + Format: types.ImageFormat(format), }, nil } diff --git a/docs/command-reference.md b/docs/command-reference.md index a4173d8f93a..ca833e3209c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -767,6 +767,7 @@ Flags: - :whale: `-c, --change`: Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT]) - :whale: `-p, --pause`: Pause container during commit (default: true) - :nerd_face: `--compression`: Commit compression algorithm (supported values: zstd or gzip) (default: gzip) (zstd is generally better for compression ratio but might not be as widely supported) +- :nerd_face: `--format`: Format of the committed image (supported values: docker or oci) (default: docker) (docker uses Docker Schema2 media types for compatibility, oci uses OCI image format media types) ## Image management diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 7489d449672..a0c8e85a063 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -140,7 +140,7 @@ type ContainerCreateOptions struct { OomKillDisable bool // OomScoreAdjChanged specifies whether the OOM preferences has been changed OomScoreAdjChanged bool - // OomScoreAdj specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) + // OomScoreAdj specifies the tune container's OOM preferences (-1000 to 1000, rootless: 100 to 1000) OomScoreAdj int // PidsLimit specifies the tune container pids limit PidsLimit int64 @@ -385,6 +385,8 @@ type ContainerCommitOptions struct { Pause bool // Compression is set commit compression algorithm Compression CompressionType + // Format specifies the image format for the committed image (docker or oci) + Format ImageFormat } type CompressionType string @@ -394,6 +396,15 @@ const ( Gzip CompressionType = "gzip" ) +type ImageFormat string + +const ( + // ImageFormatDocker uses Docker Schema2 media types for compatibility + ImageFormatDocker ImageFormat = "docker" + // ImageFormatOCI uses OCI Image Format media types + ImageFormatOCI ImageFormat = "oci" +) + // ContainerDiffOptions specifies options for `nerdctl (container) diff`. type ContainerDiffOptions struct { Stdout io.Writer diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go index 1e219575cab..6d13e09eb96 100644 --- a/pkg/cmd/container/commit.go +++ b/pkg/cmd/container/commit.go @@ -50,6 +50,7 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s Pause: options.Pause, Changes: changes, Compression: options.Compression, + Format: options.Format, } walker := &containerwalker.ContainerWalker{ diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index 7eaafd18e4e..b2b43a1610b 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -63,6 +63,7 @@ type Opts struct { Pause bool Changes Changes Compression types.CompressionType + Format types.ImageFormat } var ( @@ -177,7 +178,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd // Sync filesystem to make sure that all the data writes in container could be persisted to disk. Sync() - diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ, opts.Compression) + diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ, opts.Compression, opts) if err != nil { return emptyDigest, fmt.Errorf("failed to export layer: %w", err) } @@ -192,7 +193,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd return emptyDigest, fmt.Errorf("failed to apply diff: %w", err) } - commitManifestDesc, configDigest, err := writeContentsForImage(ctx, snName, baseImg, imageConfig, diffLayerDesc) + commitManifestDesc, configDigest, err := writeContentsForImage(ctx, snName, baseImg, imageConfig, diffLayerDesc, opts) if err != nil { return emptyDigest, err } @@ -287,14 +288,29 @@ func generateCommitImageConfig(ctx context.Context, container containerd.Contain } // writeContentsForImage will commit oci image config and manifest into containerd's content store. -func writeContentsForImage(ctx context.Context, snName string, baseImg containerd.Image, newConfig ocispec.Image, diffLayerDesc ocispec.Descriptor) (ocispec.Descriptor, digest.Digest, error) { +func writeContentsForImage(ctx context.Context, snName string, baseImg containerd.Image, newConfig ocispec.Image, diffLayerDesc ocispec.Descriptor, opts *Opts) (ocispec.Descriptor, digest.Digest, error) { newConfigJSON, err := json.Marshal(newConfig) if err != nil { return ocispec.Descriptor{}, emptyDigest, err } + // Select media types based on format choice + var configMediaType, manifestMediaType string + switch opts.Format { + case types.ImageFormatOCI: + configMediaType = ocispec.MediaTypeImageConfig + manifestMediaType = ocispec.MediaTypeImageManifest + case types.ImageFormatDocker: + configMediaType = images.MediaTypeDockerSchema2Config + manifestMediaType = images.MediaTypeDockerSchema2Manifest + default: + // Default to Docker Schema2 for compatibility + configMediaType = images.MediaTypeDockerSchema2Config + manifestMediaType = images.MediaTypeDockerSchema2Manifest + } + configDesc := ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2Config, + MediaType: configMediaType, Digest: digest.FromBytes(newConfigJSON), Size: int64(len(newConfigJSON)), } @@ -310,7 +326,7 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container MediaType string `json:"mediaType,omitempty"` ocispec.Manifest }{ - MediaType: images.MediaTypeDockerSchema2Manifest, + MediaType: manifestMediaType, Manifest: ocispec.Manifest{ Versioned: specs.Versioned{ SchemaVersion: 2, @@ -326,7 +342,7 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container } newMfstDesc := ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2Manifest, + MediaType: manifestMediaType, Digest: digest.FromBytes(newMfstJSON), Size: int64(len(newMfstJSON)), } @@ -357,14 +373,45 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container } // createDiff creates a layer diff into containerd's content store. -func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer, compression types.CompressionType) (ocispec.Descriptor, digest.Digest, error) { - opts := make([]diff.Opt, 0) - mediaType := images.MediaTypeDockerSchema2LayerGzip - if compression == types.Zstd { - opts = append(opts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd)) - mediaType = images.MediaTypeDockerSchema2LayerZstd - } - newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer, opts...) +func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer, compression types.CompressionType, opts *Opts) (ocispec.Descriptor, digest.Digest, error) { + diffOpts := make([]diff.Opt, 0) + var mediaType string + + // Select media type based on format and compression + switch opts.Format { + case types.ImageFormatOCI: + // Use OCI media types + switch compression { + case types.Zstd: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd)) + mediaType = ocispec.MediaTypeImageLayerZstd + default: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerGzip)) + mediaType = ocispec.MediaTypeImageLayerGzip + } + case types.ImageFormatDocker: + // Use Docker Schema2 media types for compatibility + switch compression { + case types.Zstd: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd)) + mediaType = images.MediaTypeDockerSchema2LayerZstd + default: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerGzip)) + mediaType = images.MediaTypeDockerSchema2LayerGzip + } + default: + // Default to Docker Schema2 media types for compatibility + switch compression { + case types.Zstd: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd)) + mediaType = images.MediaTypeDockerSchema2LayerZstd + default: + diffOpts = append(diffOpts, diff.WithMediaType(ocispec.MediaTypeImageLayerGzip)) + mediaType = images.MediaTypeDockerSchema2LayerGzip + } + } + + newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer, diffOpts...) if err != nil { return ocispec.Descriptor{}, digest.Digest(""), err } From 040bdead826d0fe33dc090ad93fbfffb35408c00 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 17:35:21 -0700 Subject: [PATCH 090/868] Fix broken tigron testca Signed-off-by: apostasie --- mod/tigron/test/data.go | 2 +- mod/tigron/utils/testca/ca.go | 32 ++++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/mod/tigron/test/data.go b/mod/tigron/test/data.go index 9400b88ec03..eabbe81c379 100644 --- a/mod/tigron/test/data.go +++ b/mod/tigron/test/data.go @@ -126,7 +126,7 @@ func (tp *temp) SaveToWriter(writer func(file io.Writer) error, key ...string) s silentT := assertive.WithSilentSuccess(tp.t) //nolint:gosec // it is fine - file, err := os.OpenFile(pth, os.O_CREATE, FilePermissionsDefault) + file, err := os.OpenFile(pth, os.O_CREATE|os.O_WRONLY, FilePermissionsDefault) assertive.ErrorIsNil( silentT, err, diff --git a/mod/tigron/utils/testca/ca.go b/mod/tigron/utils/testca/ca.go index 662be0c810c..4431ca914dc 100644 --- a/mod/tigron/utils/testca/ca.go +++ b/mod/tigron/utils/testca/ca.go @@ -107,7 +107,18 @@ func (ca *Cert) GenerateCustomX509( template *x509.Certificate, ) *Cert { silentT := assertive.WithSilentSuccess(helpers.T()) - key, certPath, keyPath := createCert(silentT, data, underDirectory, template, ca.cert, ca.key) + + var ( + cert *x509.Certificate + key *rsa.PrivateKey + ) + + if ca != nil { + cert = ca.cert + key = ca.key + } + + key, certPath, keyPath := createCert(silentT, data, underDirectory, template, cert, key) return &Cert{ CertPath: certPath, @@ -124,16 +135,16 @@ func createCert( template, caCert *x509.Certificate, caKey *rsa.PrivateKey, ) (key *rsa.PrivateKey, certPath, keyPath string) { - if caCert == nil { - caCert = template - } + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assertive.ErrorIsNil(testing, err, "key generation should succeed") if caKey == nil { caKey = key } - key, err := rsa.GenerateKey(rand.Reader, keyLength) - assertive.ErrorIsNil(testing, err, "key generation should succeed") + if caCert == nil { + caCert = template + } signedCert, err := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey) assertive.ErrorIsNil(testing, err, "certificate creation should succeed") @@ -144,16 +155,17 @@ func createCert( } data.Temp().Dir(dir) - certPath = data.Temp().Path(dir, serial.String()+".cert") - keyPath = data.Temp().Path(dir, serial.String()+".key") data.Temp().SaveToWriter(func(writer io.Writer) error { return pem.Encode(writer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) - }, keyPath) + }, dir, serial.String()+".key") data.Temp().SaveToWriter(func(writer io.Writer) error { return pem.Encode(writer, &pem.Block{Type: "CERTIFICATE", Bytes: signedCert}) - }, keyPath) + }, dir, serial.String()+".cert") + + certPath = data.Temp().Path(dir, serial.String()+".cert") + keyPath = data.Temp().Path(dir, serial.String()+".key") return key, certPath, keyPath } From 875ae4c9da850926b54c4a13814da503673b2c01 Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 16 May 2025 17:36:12 -0700 Subject: [PATCH 091/868] Move tests to new registry testing infrastructure Signed-off-by: apostasie --- cmd/nerdctl/image/image_convert_linux_test.go | 15 ++-- cmd/nerdctl/image/image_encrypt_linux_test.go | 17 +++-- cmd/nerdctl/image/image_pull_linux_test.go | 4 +- cmd/nerdctl/image/image_push_linux_test.go | 71 +++++++++++-------- 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index b26358ec8b9..6298b7bdefc 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImageConvert(t *testing.T) { @@ -100,7 +100,7 @@ func TestImageConvertNydusVerify(t *testing.T) { const remoteImageKey = "remoteImageKey" - var registry *testregistry.RegistryServer + var reg *registry.Server testCase := &test.Case{ Require: require.All( @@ -110,20 +110,21 @@ func TestImageConvertNydusVerify(t *testing.T) { require.Binary("nydusd"), require.Not(nerdtest.Docker), nerdtest.Rootful, + nerdtest.Registry, ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - base := testutil.NewBase(t) - registry = testregistry.NewWithNoAuth(base, 0, false) - data.Labels().Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port)) + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + data.Labels().Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", reg.Port)) helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image")) helpers.Ensure("tag", data.Identifier("converted-image"), data.Labels().Get(remoteImageKey)) helpers.Ensure("push", data.Labels().Get(remoteImageKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) - if registry != nil { - registry.Cleanup(nil) + if reg != nil { + reg.Cleanup(data, helpers) helpers.Anyhow("rmi", "-f", data.Labels().Get(remoteImageKey)) } }, diff --git a/cmd/nerdctl/image/image_encrypt_linux_test.go b/cmd/nerdctl/image/image_encrypt_linux_test.go index 40cb742a10c..abdefb0b38b 100644 --- a/cmd/nerdctl/image/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image/image_encrypt_linux_test.go @@ -28,13 +28,13 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImageEncryptJWE(t *testing.T) { nerdtest.Setup() - var registry *testregistry.RegistryServer + var reg *registry.Server const remoteImageKey = "remoteImageKey" @@ -44,12 +44,14 @@ func TestImageEncryptJWE(t *testing.T) { require.Not(nerdtest.Docker), // This test needs to rmi the common image nerdtest.Private, + nerdtest.Registry, ), Cleanup: func(data test.Data, helpers test.Helpers) { - if registry != nil { - registry.Cleanup(nil) + if reg != nil { + reg.Cleanup(data, helpers) helpers.Anyhow("rmi", "-f", data.Labels().Get(remoteImageKey)) } + helpers.Anyhow("rmi", "-f", data.Identifier("decrypted")) }, Setup: func(data test.Data, helpers test.Helpers) { @@ -57,10 +59,11 @@ func TestImageEncryptJWE(t *testing.T) { data.Labels().Set("private", pri) data.Labels().Set("public", pub) - base := testutil.NewBase(t) - registry = testregistry.NewWithNoAuth(base, 0, false) + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + helpers.Ensure("pull", "--quiet", testutil.CommonImage) - encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", registry.Port, data.Identifier()) + encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.Port, data.Identifier()) helpers.Ensure("image", "encrypt", "--recipient=jwe:"+pub, testutil.CommonImage, encryptImageRef) inspector := helpers.Capture("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef) assert.Equal(t, inspector, "1\n") diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index 31d022a264d..5637680e139 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -130,8 +130,8 @@ CMD ["echo", "nerdctl-build-test-string"] data.Temp().Save(dockerfile, "Dockerfile") reg = nerdtest.RegistryWithNoAuth(data, helpers, 80, false) reg.Setup(data, helpers) - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s/%s", + reg.IP.String(), data.Identifier()) buildCtx := data.Temp().Path() helpers.Ensure("build", "-t", testImageRef, buildCtx) diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 3a86a123c19..dee14a74660 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "net/http" - "strings" "testing" "gotest.tools/v3/assert" @@ -31,33 +30,43 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestPush(t *testing.T) { nerdtest.Setup() - var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *testregistry.RegistryServer + var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *registry.Server + var tokenServer *registry.TokenAuthServer testCase := &test.Case{ - Require: require.Linux, + Require: require.All( + require.Linux, + nerdtest.Registry, + ), Setup: func(data test.Data, helpers test.Helpers) { - base := testutil.NewBase(t) - registryNoAuthHTTPRandom = testregistry.NewWithNoAuth(base, 0, false) - registryNoAuthHTTPDefault = testregistry.NewWithNoAuth(base, 80, false) - registryTokenAuthHTTPSRandom = testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) + registryNoAuthHTTPRandom = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + registryNoAuthHTTPRandom.Setup(data, helpers) + registryNoAuthHTTPDefault = nerdtest.RegistryWithNoAuth(data, helpers, 80, false) + registryNoAuthHTTPDefault.Setup(data, helpers) + registryTokenAuthHTTPSRandom, tokenServer = nerdtest.RegistryWithTokenAuth(data, helpers, "admin", "badmin", 0, true) + tokenServer.Setup(data, helpers) + registryTokenAuthHTTPSRandom.Setup(data, helpers) }, Cleanup: func(data test.Data, helpers test.Helpers) { if registryNoAuthHTTPRandom != nil { - registryNoAuthHTTPRandom.Cleanup(nil) + registryNoAuthHTTPRandom.Cleanup(data, helpers) } if registryNoAuthHTTPDefault != nil { - registryNoAuthHTTPDefault.Cleanup(nil) + registryNoAuthHTTPDefault.Cleanup(data, helpers) } if registryTokenAuthHTTPSRandom != nil { - registryTokenAuthHTTPSRandom.Cleanup(nil) + registryTokenAuthHTTPSRandom.Cleanup(data, helpers) + } + if tokenServer != nil { + tokenServer.Cleanup(data, helpers) } }, @@ -66,8 +75,8 @@ func TestPush(t *testing.T) { Description: "plain http", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, @@ -86,8 +95,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, @@ -105,8 +114,8 @@ func TestPush(t *testing.T) { Description: "plain http with localhost", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, @@ -120,8 +129,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s/%s:%s", - registryNoAuthHTTPDefault.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s/%s", + registryNoAuthHTTPDefault.IP.String(), data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) }, @@ -140,8 +149,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) helpers.Ensure("--insecure-registry", "login", "-u", "admin", "-p", "badmin", @@ -163,8 +172,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.CommonImage, testImageRef) helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", @@ -186,8 +195,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) }, @@ -206,7 +215,7 @@ func TestPush(t *testing.T) { resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") if resp.Body != nil { - resp.Body.Close() + _ = resp.Body.Close() } assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") }, @@ -218,8 +227,8 @@ func TestPush(t *testing.T) { Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) }, @@ -238,7 +247,7 @@ func TestPush(t *testing.T) { resp, err := http.Get(blobURL) assert.Assert(t, err, "error making http request") if resp.Body != nil { - resp.Body.Close() + _ = resp.Body.Close() } assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") }, @@ -253,8 +262,8 @@ func TestPush(t *testing.T) { ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.UbuntuImage) - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.UbuntuImage, ":")[1]) + testImageRef := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) data.Labels().Set("testImageRef", testImageRef) helpers.Ensure("tag", testutil.UbuntuImage, testImageRef) }, From 599aa39d89702859bfef1d7a4f8ef3951ff99bbe Mon Sep 17 00:00:00 2001 From: apostasie Date: Fri, 20 Jun 2025 10:42:38 -0700 Subject: [PATCH 092/868] Update testutil HTTPGet refresh strategy 1. extend the refresh interval from 0.1 second to 1 second 2. normalize all calls to HTTPGet to repeat 5 times (instead of the mix of 10, 30, 5 etc) We can in the future assess case by case if some tests need more time. Note that the replacement does not reduce any existing test timeout (30 * 0.1 < 5) Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_run_linux_test.go | 4 ++-- cmd/nerdctl/compose/compose_up_linux_test.go | 2 +- cmd/nerdctl/container/container_run_linux_test.go | 2 +- cmd/nerdctl/container/container_run_network_base_test.go | 4 ++-- cmd/nerdctl/container/container_run_network_linux_test.go | 6 +++--- cmd/nerdctl/container/container_run_restart_linux_test.go | 2 +- cmd/nerdctl/container/container_stop_linux_test.go | 4 ++-- cmd/nerdctl/container/multi_platform_linux_test.go | 2 +- cmd/nerdctl/helpers/testing_linux.go | 2 +- cmd/nerdctl/ipfs/ipfs_compose_linux_test.go | 2 +- pkg/testutil/nerdtest/registry/cesanta.go | 2 +- pkg/testutil/nerdtest/registry/docker.go | 2 +- pkg/testutil/nerdtest/registry/kubo.go | 2 +- pkg/testutil/nettestutil/nettestutil.go | 2 +- pkg/testutil/testregistry/testregistry_linux.go | 4 ++-- 15 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 3739c25f045..c68d9fa258d 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -142,7 +142,7 @@ services: }() checkNginx := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } @@ -201,7 +201,7 @@ services: }() checkNginx := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 610e0952105..e4b69ee5550 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -100,7 +100,7 @@ COPY index.html /usr/share/nginx/html/index.html base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index 527f87d9c72..4210be47ece 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -653,7 +653,7 @@ func TestPortBindingWithCustomHost(t *testing.T) { Errors: []error{}, Output: expect.All( func(stdout string, t tig.T) { - resp, err := nettestutil.HTTPGet(address, 30, false) + resp, err := nettestutil.HTTPGet(address, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) diff --git a/cmd/nerdctl/container/container_run_network_base_test.go b/cmd/nerdctl/container/container_run_network_base_test.go index 60a27be5202..ae939cf4c4d 100644 --- a/cmd/nerdctl/container/container_run_network_base_test.go +++ b/cmd/nerdctl/container/container_run_network_base_test.go @@ -155,7 +155,7 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri hostPort: "7000-7005", containerPort: "80-85", connectURLPort: 7001, - err: "error after 30 attempts", + err: "error after 5 attempts", runShouldSuccess: true, }, { @@ -209,7 +209,7 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri return } - resp, err := nettestutil.HTTPGet(connectURL, 30, false) + resp, err := nettestutil.HTTPGet(connectURL, 5, false) if tc.err != "" { assert.ErrorContains(t, err, tc.err) return diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index b8e0c144f1c..b10483bae03 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -248,7 +248,7 @@ func TestRunPortWithNoHostPort(t *testing.T) { return } connectURL := fmt.Sprintf("http://%s:%s", "127.0.0.1", paramsMap["portNumber"]) - resp, err := nettestutil.HTTPGet(connectURL, 30, false) + resp, err := nettestutil.HTTPGet(connectURL, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) @@ -333,7 +333,7 @@ func TestUniqueHostPortAssignement(t *testing.T) { // Make HTTP GET request to container 1 connectURL1 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port1) - resp1, err := nettestutil.HTTPGet(connectURL1, 30, false) + resp1, err := nettestutil.HTTPGet(connectURL1, 5, false) assert.NilError(t, err) respBody1, err := io.ReadAll(resp1.Body) assert.NilError(t, err) @@ -341,7 +341,7 @@ func TestUniqueHostPortAssignement(t *testing.T) { // Make HTTP GET request to container 2 connectURL2 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port2) - resp2, err := nettestutil.HTTPGet(connectURL2, 30, false) + resp2, err := nettestutil.HTTPGet(connectURL2, 5, false) assert.NilError(t, err) respBody2, err := io.ReadAll(resp2.Body) assert.NilError(t, err) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index a5ce810bbad..795550696f6 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -69,7 +69,7 @@ func TestRunRestart(t *testing.T) { } return nil } - assert.NilError(t, check(30)) + assert.NilError(t, check(5)) base.KillDaemon() base.EnsureDaemonActive() diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index 135da90a938..19eb58bf9a4 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -66,14 +66,14 @@ func TestStopStart(t *testing.T) { return nil } - assert.NilError(t, check(30)) + assert.NilError(t, check(5)) base.Cmd("stop", testContainerName).AssertOK() base.Cmd("exec", testContainerName, "ps").AssertFail() if check(1) == nil { t.Fatal("expected to get an error") } base.Cmd("start", testContainerName).AssertOK() - assert.NilError(t, check(30)) + assert.NilError(t, check(5)) } func TestStopWithStopSignal(t *testing.T) { diff --git a/cmd/nerdctl/container/multi_platform_linux_test.go b/cmd/nerdctl/container/multi_platform_linux_test.go index eeb7c9f9004..01ae208528c 100644 --- a/cmd/nerdctl/container/multi_platform_linux_test.go +++ b/cmd/nerdctl/container/multi_platform_linux_test.go @@ -163,7 +163,7 @@ RUN uname -m > /usr/share/nginx/html/index.html } for testURL, expectedIndexHTML := range testCases { - resp, err := nettestutil.HTTPGet(testURL, 50, false) + resp, err := nettestutil.HTTPGet(testURL, 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go index bf63686f0c8..60a4cd76beb 100644 --- a/cmd/nerdctl/helpers/testing_linux.go +++ b/cmd/nerdctl/helpers/testing_linux.go @@ -74,7 +74,7 @@ func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK() checkWordpress := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) if err != nil { return err } diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index c9ee23631b7..d3224ec40d6 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -256,7 +256,7 @@ COPY index.html /usr/share/nginx/html/index.html testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, t tig.T) { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 5, false) assert.NilError(t, err) respBody, err := io.ReadAll(resp.Body) assert.NilError(t, err) diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go index b0551db2644..195fcd40c98 100644 --- a/pkg/testutil/nerdtest/registry/cesanta.go +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -201,7 +201,7 @@ func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *testca.Cert, scheme, net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), ), - 10, + 5, true) assert.NilError(helpers.T(), err, fmt.Errorf("failed starting auth container in a timely manner: %w", err)) diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go index 0a38174002f..6824d19b581 100644 --- a/pkg/testutil/nerdtest/registry/docker.go +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -123,7 +123,7 @@ func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *testca.C scheme, net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), ), - 10, + 5, true) assert.NilError(helpers.T(), err, fmt.Errorf("failed starting docker registry in a timely manner: %w", err)) } diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go index eb108813906..1eda4c052d0 100644 --- a/pkg/testutil/nerdtest/registry/kubo.go +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -72,7 +72,7 @@ func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, current scheme, net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), ), - 30, + 5, true) logs := helpers.Capture("logs", containerName) assert.NilError(t, err, fmt.Errorf("failed starting kubo registry in a timely manner: %w - logs: %s", err, logs)) diff --git a/pkg/testutil/nettestutil/nettestutil.go b/pkg/testutil/nettestutil/nettestutil.go index 4937b8acc21..3613a59b22c 100644 --- a/pkg/testutil/nettestutil/nettestutil.go +++ b/pkg/testutil/nettestutil/nettestutil.go @@ -48,7 +48,7 @@ func HTTPGet(urlStr string, attempts int, insecure bool) (*http.Response, error) if err == nil { return resp, nil } - time.Sleep(100 * time.Millisecond) + time.Sleep(1 * time.Second) } return nil, fmt.Errorf("error after %d attempts: %w", attempts, err) } diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go index fcc3bde5baf..fec05871fd9 100644 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ b/pkg/testutil/testregistry/testregistry_linux.go @@ -155,7 +155,7 @@ acl: return cmd.Error } joined := net.JoinHostPort(hostIP.String(), strconv.Itoa(port)) - _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/auth", scheme, joined), 30, true) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/auth", scheme, joined), 5, true) return err }() @@ -340,7 +340,7 @@ func NewRegistry(base *testutil.Base, ca *testca.CA, port int, auth Auth, boundC return "", cmd.Error } - if _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s:%s/v2", scheme, hostIP.String(), strconv.Itoa(port)), 30, true); err != nil { + if _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s:%s/v2", scheme, hostIP.String(), strconv.Itoa(port)), 5, true); err != nil { return "", err } From 4fa13e234294badba22f86e96b3d03da91a166c2 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Jun 2025 13:46:08 -0700 Subject: [PATCH 093/868] Fixes for TestImageConvertNydusVerify Signed-off-by: apostasie --- cmd/nerdctl/image/image_convert_linux_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 6298b7bdefc..e514300aede 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -19,6 +19,7 @@ package image import ( "fmt" "testing" + "time" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -102,6 +103,10 @@ func TestImageConvertNydusVerify(t *testing.T) { var reg *registry.Server + // It is unclear what is problematic here, but we use the kernel version to discriminate against EL + // See: https://github.com/containerd/nerdctl/issues/4332 + testutil.RequireKernelVersion(t, ">= 6.0.0-0") + testCase := &test.Case{ Require: require.All( require.Linux, @@ -116,6 +121,7 @@ func TestImageConvertNydusVerify(t *testing.T) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) reg.Setup(data, helpers) + data.Labels().Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", reg.Port)) helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image")) helpers.Ensure("tag", data.Identifier("converted-image"), data.Labels().Get(remoteImageKey)) @@ -129,8 +135,10 @@ func TestImageConvertNydusVerify(t *testing.T) { } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("nydusify", + cmd := helpers.Custom("nydusify", "check", + "--work-dir", + data.Temp().Dir("nydusify-temp"), "--source", testutil.CommonImage, "--target", @@ -138,6 +146,8 @@ func TestImageConvertNydusVerify(t *testing.T) { "--source-insecure", "--target-insecure", ) + cmd.WithTimeout(30 * time.Second) + return cmd }, Expected: test.Expects(0, nil, nil), } From 08b026a1c0020b1c9b4cae2c15e5d1f5578c5734 Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 13:28:43 -0700 Subject: [PATCH 094/868] Cleanup TestImageHistory Signed-off-by: apostasie --- cmd/nerdctl/image/image_history_test.go | 87 ++++++++++++++++++------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index 62238dc52a3..1ab939d3f7d 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -44,6 +44,43 @@ type historyObj struct { Comment string } +const createdAt1 = "2021-03-31T10:21:21-07:00" +const createdAt2 = "2021-03-31T10:21:23-07:00" + +// Expected content of the common image on arm64 +var ( + createdAtTime, _ = time.Parse(time.RFC3339, createdAt2) + expectedHistory = []historyObj{ + { + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Size: "0B", + CreatedAt: createdAt2, + Snapshot: "", + Comment: "", + CreatedSince: formatter.TimeSinceInHuman(createdAtTime), + }, + { + CreatedBy: "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", + Size: "5.947MB", + CreatedAt: createdAt1, + Snapshot: "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", + Comment: "", + CreatedSince: formatter.TimeSinceInHuman(createdAtTime), + }, + } + expectedHistoryNoTrunc = []historyObj{ + { + Snapshot: "", + Size: "0", + }, + { + Snapshot: "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a", + CreatedBy: "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ", + Size: "5947392", + }, + } +) + func decode(stdout string) ([]historyObj, error) { dec := json.NewDecoder(strings.NewReader(stdout)) object := []historyObj{} @@ -96,35 +133,35 @@ func TestImageHistory(t *testing.T) { assert.NilError(t, err, "decode should not fail") assert.Equal(t, len(history), 2, "history should be 2 in length") - localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") - localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") - compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) - compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) - assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String()) - assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]") - assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String()) - assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…") - - assert.Equal(t, history[0].Size, "0B") - assert.Equal(t, history[0].CreatedSince, formatter.TimeSinceInHuman(compTime1)) - assert.Equal(t, history[0].Snapshot, "") - assert.Equal(t, history[0].Comment, "") - - assert.Equal(t, history[1].Size, "5.947MB") - assert.Equal(t, history[1].CreatedSince, formatter.TimeSinceInHuman(compTime2)) - assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…") - assert.Equal(t, history[1].Comment, "") + h0Time, _ := time.Parse(time.RFC3339, history[0].CreatedAt) + h1Time, _ := time.Parse(time.RFC3339, history[1].CreatedAt) + comp0Time, _ := time.Parse(time.RFC3339, expectedHistory[0].CreatedAt) + comp1Time, _ := time.Parse(time.RFC3339, expectedHistory[1].CreatedAt) + + assert.Equal(t, h0Time.UTC().String(), comp0Time.UTC().String()) + assert.Equal(t, history[0].CreatedBy, expectedHistory[0].CreatedBy) + assert.Equal(t, history[0].Size, expectedHistory[0].Size) + assert.Equal(t, history[0].CreatedSince, expectedHistory[0].CreatedSince) + assert.Equal(t, history[0].Snapshot, expectedHistory[0].Snapshot) + assert.Equal(t, history[0].Comment, expectedHistory[0].Comment) + + assert.Equal(t, h1Time.UTC().String(), comp1Time.UTC().String()) + assert.Equal(t, history[1].CreatedBy, expectedHistory[1].CreatedBy) + assert.Equal(t, history[1].Size, expectedHistory[1].Size) + assert.Equal(t, history[1].CreatedSince, expectedHistory[1].CreatedSince) + assert.Equal(t, history[1].Snapshot, expectedHistory[1].Snapshot) + assert.Equal(t, history[1].Comment, expectedHistory[1].Comment) }), }, { - Description: "no human - dates and sizes and not prettyfied", + Description: "no human - dates and sizes are not prettyfied", Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") - assert.Equal(t, history[0].Size, "0") + assert.Equal(t, history[0].Size, expectedHistoryNoTrunc[0].Size) assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt) - assert.Equal(t, history[1].Size, "5947392") + assert.Equal(t, history[1].Size, expectedHistoryNoTrunc[1].Size) assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt) }), }, @@ -134,22 +171,22 @@ func TestImageHistory(t *testing.T) { Expected: test.Expects(0, nil, func(stdout string, t tig.T) { history, err := decode(stdout) assert.NilError(t, err, "decode should not fail") - assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") - assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") + assert.Equal(t, history[1].Snapshot, expectedHistoryNoTrunc[1].Snapshot) + assert.Equal(t, history[1].CreatedBy, expectedHistoryNoTrunc[1].CreatedBy) }), }, { Description: "Quiet has no effect with format, so, go no-json, no-trunc", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { - assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + assert.Equal(t, stdout, expectedHistoryNoTrunc[0].Snapshot+"\n"+expectedHistoryNoTrunc[1].Snapshot+"\n") }), }, { Description: "With quiet, trunc has no effect", Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { - assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + assert.Equal(t, stdout, expectedHistoryNoTrunc[0].Snapshot+"\n"+expectedHistoryNoTrunc[1].Snapshot+"\n") }), }, }, From 554005ccc9f759b7d9e1c42247c5522df7271c09 Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 14:12:51 -0700 Subject: [PATCH 095/868] Build examples and enable on CI Signed-off-by: apostasie --- .github/workflows/job-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 2bf843e2ab6..d20822d68d3 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -70,6 +70,8 @@ jobs: local goarm="${3:-}" local result + GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" go build ./examples/... + github::timer::begin GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" make binaries \ From 228be771f1d1a656eb74f17c3728331d126efbd1 Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 16:00:13 -0700 Subject: [PATCH 096/868] Add delay for windows network test Signed-off-by: apostasie --- cmd/nerdctl/network/network_inspect_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index fc2e18e41e4..3b7e5276420 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -20,8 +20,10 @@ import ( "encoding/json" "errors" "os/exec" + "runtime" "strings" "testing" + "time" "gotest.tools/v3/assert" @@ -290,6 +292,13 @@ func TestNetworkInspect(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("nginx-network-1")) helpers.Ensure("network", "create", data.Identifier("nginx-network-2")) + + // See https://github.com/containerd/nerdctl/issues/4322 + // Maybe network create on windows is asynchronous? + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + helpers.Ensure("create", "--name", data.Identifier("nginx-container-1"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) helpers.Ensure("create", "--name", data.Identifier("nginx-container-2"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) helpers.Ensure("create", "--name", data.Identifier("nginx-container-on-diff-network"), "--network", data.Identifier("nginx-network-2"), testutil.NginxAlpineImage) @@ -327,6 +336,12 @@ func TestNetworkInspect(t *testing.T) { helpers.Ensure("network", "create", data.Identifier("network-1")) helpers.Ensure("network", "create", data.Identifier("network-2")) + // See https://github.com/containerd/nerdctl/issues/4322 + // Maybe network create on windows is asynchronous? + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("network-1"), "--network", data.Identifier("network-2"), testutil.CommonImage, "sleep", nerdtest.Infinity) data.Labels().Set("containerID", strings.Trim(containerID, "\n")) @@ -356,6 +371,12 @@ func TestNetworkInspect(t *testing.T) { helpers.Ensure("network", "create", data.Identifier("some-network")) helpers.Ensure("network", "create", data.Identifier("some-network-as-well")) + // See https://github.com/containerd/nerdctl/issues/4322 + // Maybe network create on windows is asynchronous? + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--network", data.Identifier("some-network-as-well"), testutil.CommonImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { From 421872fee8217e2513364ef0107c9398b3f9b8fd Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 17:28:57 -0700 Subject: [PATCH 097/868] Simplify healthcheck file handling Signed-off-by: apostasie --- pkg/healthcheck/log.go | 36 ++++++++--------------------- pkg/internal/filesystem/lock.go | 41 ++++++++------------------------- 2 files changed, 19 insertions(+), 58 deletions(-) diff --git a/pkg/healthcheck/log.go b/pkg/healthcheck/log.go index 1664b502671..9695acc7002 100644 --- a/pkg/healthcheck/log.go +++ b/pkg/healthcheck/log.go @@ -47,21 +47,22 @@ func writeHealthLog(ctx context.Context, container containerd.Container, result return fmt.Errorf("failed to marshal health log: %w", err) } - // Ensure file exists before writing - if err := ensureHealthLogFile(stateDir); err != nil { - return err - } - // Write the latest result to the file logPath := filepath.Join(stateDir, HealthLogFilename) - return filesystem.WithAppendLock(logPath, func(file *os.File) error { - if _, err := file.Seek(0, io.SeekEnd); err != nil { + return filesystem.WithLock(stateDir, func() error { + file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY, 0o600) + if err != nil { + return err + } + defer file.Close() + if _, err = file.Seek(0, io.SeekEnd); err != nil { return fmt.Errorf("seek error: %w", err) } - if _, err := file.Write(append([]byte(data), '\n')); err != nil { + if _, err = file.Write(append([]byte(data), '\n')); err != nil { return fmt.Errorf("failed to write health log: %w", err) } - return nil + + return file.Sync() }) } @@ -182,23 +183,6 @@ func readHealthStateFromLabels(ctx context.Context, container containerd.Contain return state, nil } -// ensureHealthLogFile creates the health.json file if it doesn't exist. -func ensureHealthLogFile(stateDir string) error { - healthLogPath := filepath.Join(stateDir, HealthLogFilename) - - // Ensure container state directory exists - if _, err := os.Stat(stateDir); os.IsNotExist(err) { - return fmt.Errorf("container state directory does not exist: %s", stateDir) - } - - // Create health.json if it doesn't exist - if _, err := os.Stat(healthLogPath); os.IsNotExist(err) { - return filesystem.WriteFile(healthLogPath, []byte{}, 0600) - } - - return nil -} - // getContainerStateDir returns the container's state directory from labels. func getContainerStateDir(ctx context.Context, container containerd.Container) (string, error) { info, err := container.Info(ctx) diff --git a/pkg/internal/filesystem/lock.go b/pkg/internal/filesystem/lock.go index c278bbe917e..d993e380b41 100644 --- a/pkg/internal/filesystem/lock.go +++ b/pkg/internal/filesystem/lock.go @@ -37,7 +37,7 @@ import ( // If Lock returns nil, no other process will be able to place a read or write lock on the file until // this process exits, closes f, or calls Unlock on it. func Lock(path string) (file *os.File, err error) { - return commonlock(path, writeLock, false) + return commonlock(path, writeLock) } // ReadOnlyLock places an advisory read lock on the file, blocking until it can be locked. @@ -45,10 +45,10 @@ func Lock(path string) (file *os.File, err error) { // If ReadOnlyLock returns nil, no other process will be able to place a write lock on // the file until this process exits, closes f, or calls Unlock on it. func ReadOnlyLock(path string) (file *os.File, err error) { - return commonlock(path, readLock, false) + return commonlock(path, readLock) } -func commonlock(path string, mode lockType, appendMode bool) (file *os.File, err error) { +func commonlock(path string, mode lockType) (file *os.File, err error) { defer func() { if err != nil { err = errors.Join(ErrLockFail, err, file.Close()) @@ -65,21 +65,12 @@ func commonlock(path string, mode lockType, appendMode bool) (file *os.File, err } } - // For append mode, open with specific flags - if appendMode { - file, err = os.OpenFile(path, os.O_WRONLY, privateFilePermission) - if err != nil { - return nil, err - } - } else { - // Preserve the original behavior for non-append operations - file, err = os.Open(path) - if errors.Is(err, os.ErrNotExist) { - file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, privateFilePermission) - } - if err != nil { - return nil, err - } + file, err = os.Open(path) + if errors.Is(err, os.ErrNotExist) { + file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, privateFilePermission) + } + if err != nil { + return nil, err } if err = platformSpecificLock(file, mode); err != nil { @@ -131,17 +122,3 @@ func WithReadOnlyLock(path string, function func() error) (err error) { return function() } - -// WithAppendLock executes the function with a file opened in append mode -func WithAppendLock(path string, function func(file *os.File) error) (err error) { - file, err := commonlock(path, writeLock, true) - if err != nil { - return err - } - - defer func() { - err = errors.Join(Unlock(file), err) - }() - - return function(file) -} From 17916156129fa40fadd57c99bdcd285e763f5859 Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 14:26:45 -0700 Subject: [PATCH 098/868] Kube test: wait for image Signed-off-by: apostasie --- .../container/container_commit_linux_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index 5ddbf501643..3bd5b98cf42 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -19,6 +19,7 @@ package container import ( "strings" "testing" + "time" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -55,6 +56,20 @@ func TestKubeCommitSave(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("commit", data.Labels().Get("containerID"), data.Identifier("testcommitsave")) + // Wait for the image to show up + for range 5 { + found := false + cmd := helpers.Command("images", data.Identifier("testcommitsave"), "--format", "json") + cmd.Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + found = strings.TrimSpace(stdout) != "" + }, + }) + if found { + break + } + time.Sleep(1 * time.Second) + } return helpers.Command("save", data.Identifier("testcommitsave")) } From fe8d6afd79bdd4ef883f086d2da27bc6d07e6b7d Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 23 Jun 2025 17:40:39 +0800 Subject: [PATCH 099/868] image: extract image format options into separate structs Reorganize `ImageConvertOptions` by extracting format-specific options into dedicated structs (EstargzOptions, ZstdOptions, ZstdChunkedOptions, NydusOptions, OverlaybdOptions) and embedding them for better code organization and maintainability. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/image/image_convert.go | 63 +++++++++++++++--------------- pkg/api/types/image_types.go | 33 ++++++++++------ 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 871d9c97d81..ff7caade58d 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -237,37 +237,6 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { return types.ImageConvertOptions{ GOptions: globalOptions, Format: format, - // #region estargz flags - Estargz: estargz, - EstargzRecordIn: estargzRecordIn, - EstargzCompressionLevel: estargzCompressionLevel, - EstargzChunkSize: estargzChunkSize, - EstargzMinChunkSize: estargzMinChunkSize, - EstargzExternalToc: estargzExternalTOC, - EstargzKeepDiffID: estargzKeepDiffID, - // #endregion - // #region zstd flags - Zstd: zstd, - ZstdCompressionLevel: zstdCompressionLevel, - // #endregion - // #region zstd:chunked flags - ZstdChunked: zstdchunked, - ZstdChunkedCompressionLevel: zstdChunkedCompressionLevel, - ZstdChunkedChunkSize: zstdChunkedChunkSize, - ZstdChunkedRecordIn: zstdChunkedRecordIn, - // #endregion - // #region nydus flags - Nydus: nydus, - NydusBuilderPath: nydusBuilderPath, - NydusWorkDir: nydusWorkDir, - NydusPrefetchPatterns: nydusPrefetchPatterns, - NydusCompressor: nydusCompressor, - // #endregion - // #region overlaybd flags - Overlaybd: overlaybd, - OverlayFsType: overlaybdFsType, - OverlaydbDBStr: overlaybdDbstr, - // #endregion // #region generic flags Uncompress: uncompress, Oci: oci, @@ -276,6 +245,38 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { Platforms: platforms, AllPlatforms: allPlatforms, // #endregion + // Embed image format options + EstargzOptions: types.EstargzOptions{ + Estargz: estargz, + EstargzRecordIn: estargzRecordIn, + EstargzCompressionLevel: estargzCompressionLevel, + EstargzChunkSize: estargzChunkSize, + EstargzMinChunkSize: estargzMinChunkSize, + EstargzExternalToc: estargzExternalTOC, + EstargzKeepDiffID: estargzKeepDiffID, + }, + ZstdOptions: types.ZstdOptions{ + Zstd: zstd, + ZstdCompressionLevel: zstdCompressionLevel, + }, + ZstdChunkedOptions: types.ZstdChunkedOptions{ + ZstdChunked: zstdchunked, + ZstdChunkedCompressionLevel: zstdChunkedCompressionLevel, + ZstdChunkedChunkSize: zstdChunkedChunkSize, + ZstdChunkedRecordIn: zstdChunkedRecordIn, + }, + NydusOptions: types.NydusOptions{ + Nydus: nydus, + NydusBuilderPath: nydusBuilderPath, + NydusWorkDir: nydusWorkDir, + NydusPrefetchPatterns: nydusPrefetchPatterns, + NydusCompressor: nydusCompressor, + }, + OverlaybdOptions: types.OverlaybdOptions{ + Overlaybd: overlaybd, + OverlayFsType: overlaybdFsType, + OverlaydbDBStr: overlaybdDbstr, + }, Stdout: cmd.OutOrStdout(), }, nil } diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index d48e6318026..ddc08facf68 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -67,7 +67,16 @@ type ImageConvertOptions struct { // Format the output using the given Go template, e.g, 'json' Format string - // #region estargz flags + // Embed image format options + EstargzOptions + ZstdOptions + ZstdChunkedOptions + NydusOptions + OverlaybdOptions +} + +// EstargzOptions contains eStargz conversion options +type EstargzOptions struct { // Estargz convert legacy tar(.gz) layers to eStargz for lazy pulling. Should be used in conjunction with '--oci' Estargz bool // EstargzRecordIn read 'ctr-remote optimize --record-out=' record file (EXPERIMENTAL) @@ -82,16 +91,18 @@ type ImageConvertOptions struct { EstargzExternalToc bool // EstargzKeepDiffID convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc') EstargzKeepDiffID bool - // #endregion +} - // #region zstd flags +// ZstdOptions contains zstd conversion options +type ZstdOptions struct { // Zstd convert legacy tar(.gz) layers to zstd. Should be used in conjunction with '--oci' Zstd bool // ZstdCompressionLevel zstd compression level ZstdCompressionLevel int - // #endregion +} - // #region zstd:chunked flags +// ZstdChunkedOptions contains zstd:chunked conversion options +type ZstdChunkedOptions struct { // ZstdChunked convert legacy tar(.gz) layers to zstd:chunked for lazy pulling. Should be used in conjunction with '--oci' ZstdChunked bool // ZstdChunkedCompressionLevel zstd compression level @@ -100,9 +111,10 @@ type ImageConvertOptions struct { ZstdChunkedChunkSize int // ZstdChunkedRecordIn read 'ctr-remote optimize --record-out=' record file (EXPERIMENTAL) ZstdChunkedRecordIn string - // #endregion +} - // #region nydus flags +// NydusOptions contains nydus conversion options +type NydusOptions struct { // Nydus convert legacy tar(.gz) layers to nydus for lazy pulling. Should be used in conjunction with '--oci' Nydus bool // NydusBuilderPath the nydus-image binary path, if unset, search in PATH environment @@ -113,17 +125,16 @@ type ImageConvertOptions struct { NydusPrefetchPatterns string // NydusCompressor nydus blob compression algorithm, possible values: `none`, `lz4_block`, `zstd`, default is `lz4_block` NydusCompressor string - // #endregion +} - // #region overlaybd flags +// OverlaybdOptions contains overlaybd conversion options +type OverlaybdOptions struct { // Overlaybd convert tar.gz layers to overlaybd layers Overlaybd bool // OverlayFsType filesystem type for overlaybd OverlayFsType string // OverlaydbDBStr database config string for overlaybd OverlaydbDBStr string - // #endregion - } // ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`. From 9fa90717457b32ccc46d7303abc7e9ef65acf52f Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 23 Jun 2025 15:16:57 +0800 Subject: [PATCH 100/868] commit: support estargz conversion with writable layer in container commit support estargz conversion with writable layer in container commit Fixes: #4351 Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_commit.go | 27 +++++++++++++ docs/command-reference.md | 4 ++ pkg/api/types/container_types.go | 2 + pkg/cmd/container/commit.go | 15 ++++---- pkg/imgutil/commit/commit.go | 47 +++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/cmd/nerdctl/container/container_commit.go b/cmd/nerdctl/container/container_commit.go index 2d58e23008b..4c22cff25aa 100644 --- a/cmd/nerdctl/container/container_commit.go +++ b/cmd/nerdctl/container/container_commit.go @@ -44,6 +44,10 @@ func CommitCommand() *cobra.Command { cmd.Flags().BoolP("pause", "p", true, "Pause container during commit") cmd.Flags().StringP("compression", "", "gzip", "commit compression algorithm (zstd or gzip)") cmd.Flags().String("format", "docker", "Format of the committed image (docker or oci)") + cmd.Flags().Bool("estargz", false, "Convert the committed layer to eStargz for lazy pulling") + cmd.Flags().Int("estargz-compression-level", 9, "eStargz compression level (1-9)") + cmd.Flags().Int("estargz-chunk-size", 0, "eStargz chunk size") + cmd.Flags().Int("estargz-min-chunk-size", 0, "The minimal number of bytes of data must be written in one gzip stream") return cmd } @@ -86,6 +90,23 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { return types.ContainerCommitOptions{}, errors.New("--format param only supports docker or oci") } + estargz, err := cmd.Flags().GetBool("estargz") + if err != nil { + return types.ContainerCommitOptions{}, err + } + estargzCompressionLevel, err := cmd.Flags().GetInt("estargz-compression-level") + if err != nil { + return types.ContainerCommitOptions{}, err + } + estargzChunkSize, err := cmd.Flags().GetInt("estargz-chunk-size") + if err != nil { + return types.ContainerCommitOptions{}, err + } + estargzMinChunkSize, err := cmd.Flags().GetInt("estargz-min-chunk-size") + if err != nil { + return types.ContainerCommitOptions{}, err + } + return types.ContainerCommitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, @@ -95,6 +116,12 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { Change: change, Compression: types.CompressionType(com), Format: types.ImageFormat(format), + EstargzOptions: types.EstargzOptions{ + Estargz: estargz, + EstargzCompressionLevel: estargzCompressionLevel, + EstargzChunkSize: estargzChunkSize, + EstargzMinChunkSize: estargzMinChunkSize, + }, }, nil } diff --git a/docs/command-reference.md b/docs/command-reference.md index 68be808a94e..5e17ad03a6d 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -778,6 +778,10 @@ Flags: - :whale: `-p, --pause`: Pause container during commit (default: true) - :nerd_face: `--compression`: Commit compression algorithm (supported values: zstd or gzip) (default: gzip) (zstd is generally better for compression ratio but might not be as widely supported) - :nerd_face: `--format`: Format of the committed image (supported values: docker or oci) (default: docker) (docker uses Docker Schema2 media types for compatibility, oci uses OCI image format media types) +- :nerd_face: `--estargz`: Convert the committed layer to eStargz for lazy pulling +- :nerd_face: `--estargz-compression-level`: eStargz compression level (1-9) (default: 9) +- :nerd_face: `--estargz-chunk-size`: eStargz chunk size +- :nerd_face: `--estargz-min-chunk-size`: The minimal number of bytes of data must be written in one gzip stream ## Image management diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 45f06d40692..b90034abd01 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -396,6 +396,8 @@ type ContainerCommitOptions struct { Compression CompressionType // Format specifies the image format for the committed image (docker or oci) Format ImageFormat + // Embed EstargzOptions for eStargz conversion options + EstargzOptions } type CompressionType string diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go index 6d13e09eb96..fdafe3ae776 100644 --- a/pkg/cmd/container/commit.go +++ b/pkg/cmd/container/commit.go @@ -44,13 +44,14 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s } opts := &commit.Opts{ - Author: options.Author, - Message: options.Message, - Ref: parsedReference.String(), - Pause: options.Pause, - Changes: changes, - Compression: options.Compression, - Format: options.Format, + Author: options.Author, + Message: options.Message, + Ref: parsedReference.String(), + Pause: options.Pause, + Changes: changes, + Compression: options.Compression, + Format: options.Format, + EstargzOptions: options.EstargzOptions, } walker := &containerwalker.ContainerWalker{ diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index b2b43a1610b..1f3292178e8 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -43,6 +43,8 @@ import ( "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/platforms" + "github.com/containerd/stargz-snapshotter/estargz" + estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -64,6 +66,7 @@ type Opts struct { Changes Changes Compression types.CompressionType Format types.ImageFormat + types.EstargzOptions } var ( @@ -431,6 +434,50 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c return ocispec.Descriptor{}, digest.Digest(""), err } + // Convert to eStargz if requested + if opts.Estargz { + log.G(ctx).Infof("Converting diff layer to eStargz format") + + esgzOpts := []estargz.Option{ + estargz.WithCompressionLevel(opts.EstargzCompressionLevel), + } + if opts.EstargzChunkSize > 0 { + esgzOpts = append(esgzOpts, estargz.WithChunkSize(opts.EstargzChunkSize)) + } + if opts.EstargzMinChunkSize > 0 { + esgzOpts = append(esgzOpts, estargz.WithMinChunkSize(opts.EstargzMinChunkSize)) + } + + convertFunc := estargzconvert.LayerConvertFunc(esgzOpts...) + + esgzDesc, err := convertFunc(ctx, cs, newDesc) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("failed to convert diff layer to eStargz: %w", err) + } else if esgzDesc != nil { + esgzDesc.MediaType = mediaType + esgzInfo, err := cs.Info(ctx, esgzDesc.Digest) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), err + } + + esgzDiffIDStr, ok := esgzInfo.Labels["containerd.io/uncompressed"] + if !ok { + return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("invalid differ response with no diffID") + } + + esgzDiffID, err := digest.Parse(esgzDiffIDStr) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), err + } + return ocispec.Descriptor{ + MediaType: esgzDesc.MediaType, + Digest: esgzDesc.Digest, + Size: esgzDesc.Size, + Annotations: esgzDesc.Annotations, + }, esgzDiffID, nil + } + } + return ocispec.Descriptor{ MediaType: mediaType, Digest: newDesc.Digest, From 5e5e96e74364b4ef4cfbf44fcbf6244c765198e8 Mon Sep 17 00:00:00 2001 From: Justin Alvarez Date: Tue, 24 Jun 2025 16:42:48 +0000 Subject: [PATCH 101/868] refactor: move BUILDKIT_HOST to buildkitutil Signed-off-by: Justin Alvarez --- cmd/nerdctl/builder/builder_build.go | 8 -------- pkg/buildkitutil/buildkitutil.go | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build.go b/cmd/nerdctl/builder/builder_build.go index 8b9691fb8a3..32a2937ed75 100644 --- a/cmd/nerdctl/builder/builder_build.go +++ b/cmd/nerdctl/builder/builder_build.go @@ -19,7 +19,6 @@ package builder import ( "errors" "fmt" - "os" "strconv" "strings" @@ -263,13 +262,6 @@ func GetBuildkitHost(cmd *cobra.Command, namespace string) (string, error) { return buildkitHost, nil } - if buildkitHost := os.Getenv("BUILDKIT_HOST"); buildkitHost != "" { - if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil { - return "", err - } - return buildkitHost, nil - - } return buildkitutil.GetBuildkitHost(namespace) } diff --git a/pkg/buildkitutil/buildkitutil.go b/pkg/buildkitutil/buildkitutil.go index ecf050e4ed8..10ed05379b1 100644 --- a/pkg/buildkitutil/buildkitutil.go +++ b/pkg/buildkitutil/buildkitutil.go @@ -60,6 +60,13 @@ func BuildctlBaseArgs(buildkitHost string) []string { } func GetBuildkitHost(namespace string) (string, error) { + if buildkitHost := os.Getenv("BUILDKIT_HOST"); buildkitHost != "" { + if _, err := pingBKDaemon(buildkitHost); err != nil { + return "", err + } + return buildkitHost, nil + } + paths, err := getBuildkitHostCandidates(namespace) if err != nil { return "", err From 792e7f9cae140505c6251fa8469974c85f078817 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Jun 2025 10:25:02 -0700 Subject: [PATCH 102/868] Fix healthcheck test Signed-off-by: apostasie --- cmd/nerdctl/container/container_health_check_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go index ff77571b9e8..43c549441c1 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -207,7 +207,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--health-cmd", "exit 1", "--health-interval", "1s", - "--health-start-period", "5s", + "--health-start-period", "60s", "--health-retries", "2", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) From b3a87c3938196561f808a0afd089ce0d7c3b42e7 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Jun 2025 18:52:28 -0700 Subject: [PATCH 103/868] Add DoesNotMatch comparator Signed-off-by: apostasie --- mod/tigron/expect/comparators.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mod/tigron/expect/comparators.go b/mod/tigron/expect/comparators.go index fa004051f8e..f7b33fe32d6 100644 --- a/mod/tigron/expect/comparators.go +++ b/mod/tigron/expect/comparators.go @@ -91,6 +91,14 @@ func Match(reg *regexp.Regexp) test.Comparator { } } +// DoesNotMatch returns a comparator verifying the output does not match the provided regexp. +func DoesNotMatch(reg *regexp.Regexp) test.Comparator { + return func(stdout string, t tig.T) { + t.Helper() + assertive.DoesNotMatch(assertive.WithFailLater(t), stdout, reg, "Inspecting output (!match)") + } +} + // JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided // method. func JSON[T any](obj T, verifier func(T, tig.T)) test.Comparator { From 96ae9256ba881e5e8330945f37c6f9242288a592 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Jun 2025 18:58:14 -0700 Subject: [PATCH 104/868] Fix broken image parsing and hard-coded data in tests Signed-off-by: apostasie --- cmd/nerdctl/builder/builder_builder_test.go | 14 +++++-- cmd/nerdctl/image/image_list_test.go | 42 ++++++++++++--------- pkg/testutil/testutil.go | 8 ---- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/cmd/nerdctl/builder/builder_builder_test.go b/cmd/nerdctl/builder/builder_builder_test.go index da912cc0af9..75100795e70 100644 --- a/cmd/nerdctl/builder/builder_builder_test.go +++ b/cmd/nerdctl/builder/builder_builder_test.go @@ -30,6 +30,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -152,14 +153,19 @@ CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) // FIXME: this test should be rewritten to dynamically retrieve the ids, and use images // available on all platforms oldImage := testutil.BusyboxImage - oldImageSha := "7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c" + parsedOldImage, err := referenceutil.Parse(oldImage) + assert.NilError(helpers.T(), err) + oldImageSha := parsedOldImage.Digest.String() + newImage := testutil.AlpineImage - newImageSha := "ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a" + parsedNewImage, err := referenceutil.Parse(newImage) + assert.NilError(helpers.T(), err) + newImageSha := parsedNewImage.Digest.String() helpers.Ensure("pull", "--quiet", oldImage) - helpers.Ensure("tag", oldImage, newImage) + helpers.Ensure("tag", oldImage, parsedNewImage.Domain+"/"+parsedNewImage.Path+":"+parsedNewImage.Tag) - dockerfile := fmt.Sprintf(`FROM %s`, newImage) + dockerfile := fmt.Sprintf(`FROM %s`, parsedNewImage.Domain+"/"+parsedNewImage.Path+":"+parsedNewImage.Tag) data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("oldImageSha", oldImageSha) data.Labels().Set("newImageSha", newImageSha) diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 7a19e552c08..228b3a08d1a 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -19,6 +19,7 @@ package image import ( "errors" "fmt" + "regexp" "runtime" "slices" "strings" @@ -31,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -39,10 +41,12 @@ import ( func TestImages(t *testing.T) { nerdtest.Setup() + commonImage, _ := referenceutil.Parse(testutil.CommonImage) + testCase := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("pull", "--quiet", testutil.CommonImage) + helpers.Ensure("pull", "--quiet", commonImage.String()) helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) }, SubTests: []*test.Case{ @@ -65,7 +69,7 @@ func TestImages(t *testing.T) { for _, line := range lines[1:] { repo, _ := tab.ReadRow(line, "REPOSITORY") tag, _ := tab.ReadRow(line, "TAG") - if repo+":"+tag == testutil.CommonImage { + if repo+":"+tag == commonImage.FamiliarName()+":"+commonImage.Tag { found = true break } @@ -77,11 +81,11 @@ func TestImages(t *testing.T) { }, { Description: "With names", - Command: test.Command("images", "--names", testutil.CommonImage), + Command: test.Command("images", "--names", commonImage.String()), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(testutil.CommonImage), + expect.Contains(commonImage.String()), func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") @@ -91,7 +95,7 @@ func TestImages(t *testing.T) { found := false for _, line := range lines[1:] { name, _ := tab.ReadRow(line, "NAME") - if name == testutil.CommonImage { + if name == commonImage.String() { found = true break } @@ -134,19 +138,21 @@ func TestImages(t *testing.T) { func TestImagesFilter(t *testing.T) { nerdtest.Setup() + commonImage, _ := referenceutil.Parse(testutil.CommonImage) + testCase := &test.Case{ Require: nerdtest.Build, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("pull", "--quiet", testutil.CommonImage) - helpers.Ensure("tag", testutil.CommonImage, "taggedimage:one-fragment-one") - helpers.Ensure("tag", testutil.CommonImage, "taggedimage:two-fragment-two") + helpers.Ensure("pull", "--quiet", commonImage.String()) + helpers.Ensure("tag", commonImage.String(), "taggedimage:one-fragment-one") + helpers.Ensure("tag", commonImage.String(), "taggedimage:two-fragment-two") dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] \n LABEL foo=bar LABEL version=0.1 RUN echo "actually creating a layer so that docker sets the createdAt time" -`, testutil.CommonImage) +`, commonImage.String()) buildCtx := data.Temp().Path() data.Temp().Save(dockerfile, "Dockerfile") data.Labels().Set("buildCtx", buildCtx) @@ -235,32 +241,32 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(testutil.ImageRepo(testutil.CommonImage)), + expect.Contains(commonImage.FamiliarName(), commonImage.Tag), expect.DoesNotContain(data.Labels().Get("builtImageID")), ), } }, }, { - Description: "since=" + testutil.CommonImage, - Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)), + Description: "since=" + commonImage.String(), + Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", commonImage.String())), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(data.Labels().Get("builtImageID")), - expect.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + expect.DoesNotMatch(regexp.MustCompile(commonImage.FamiliarName()+"[\\s]+"+commonImage.Tag)), ), } }, }, { - Description: "since=" + testutil.CommonImage + " " + testutil.CommonImage, - Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage), + Description: "since=" + commonImage.String() + " " + commonImage.String(), + Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", commonImage.String()), commonImage.String()), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.DoesNotContain( - data.Labels().Get("builtImageID"), - testutil.ImageRepo(testutil.CommonImage), + Output: expect.All( + expect.DoesNotContain(data.Labels().Get("builtImageID")), + expect.DoesNotMatch(regexp.MustCompile(commonImage.FamiliarName()+"[\\s]+"+commonImage.Tag)), ), } }, diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 4c4b03066ea..f6bf39dda7a 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -40,7 +40,6 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" - "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" @@ -749,13 +748,6 @@ func Identifier(t testing.TB) string { return s } -// ImageRepo returns the image repo that can be used to, e.g, validate output -// from `nerdctl images`. -func ImageRepo(s string) string { - repo, _ := imgutil.ParseRepoTag(s) - return repo -} - // RegisterBuildCacheCleanup adds a 'builder prune --all --force' cleanup function // to run on test teardown. func RegisterBuildCacheCleanup(t *testing.T) { From 7d768be794517e919bf1e9aed9ec683b4024ebc1 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 25 Jun 2025 09:33:58 +0000 Subject: [PATCH 105/868] fix: allow containers to start using a large number of ports Suppose we have a compose.yaml that allocates a large numbers of ports as follows. ``` > cat compose.yaml services: svc0: image: alpine command: "sleep infinity" ports: - '32000-32060:32000-32060' ``` When we run `nerdctl compose up -d` using this compose.yaml, we will get the following error. ``` FATA[0000] create container failed validation: containers.Labels: label key and value length (4711 bytes) greater than maximum size (4096 bytes), key: nerdctl/ports: invalid argument FATA[0000] error while creating container haytok-svc0-1: error while creating container haytok-svc0-1: exit status 1 ``` This issue is reported in the following issue. - https://github.com/containerd/nerdctl/issues/4027 This issue is considered to be the same as the one with errors when trying to perform many port mappings, such as `nerdctl run -p 80:80 -p 81:81 ~ -p 1000:1000 ...` The current implementation is processing to create a container with the information specified in -p to the label. And as can be seen from the error message, as the number of ports to be port mapped increases, the creation of the container fails because it violates the limit of the maximum number of bytes on the containerd side that can be allocated for a label. Therefore, this PR modifies the container creation process so that containers can be launched without having to assign the information specified in the -p option to the labels. Specifically, port mapping information is stored in the following path, and when port mapping information is required, it is retrieved from this file. ``` //containers///network-config.json ``` Signed-off-by: Hayato Kiwata --- cmd/nerdctl/compose/compose_port.go | 7 ++ .../compose/compose_port_linux_test.go | 43 +++++++ cmd/nerdctl/compose/compose_ps.go | 48 +++++--- cmd/nerdctl/container/container_port.go | 16 ++- .../container_run_network_linux_test.go | 9 +- pkg/cmd/container/create.go | 16 +-- pkg/cmd/container/inspect.go | 26 ++++- pkg/cmd/container/kill.go | 15 ++- pkg/cmd/container/list.go | 16 ++- pkg/cmd/container/remove.go | 13 +++ pkg/composer/port.go | 14 ++- .../container_network_manager.go | 6 - pkg/containerutil/containerutil.go | 13 +-- pkg/formatter/formatter.go | 10 +- pkg/inspecttypes/dockercompat/dockercompat.go | 21 ++-- .../dockercompat/dockercompat_test.go | 13 ++- pkg/inspecttypes/native/container.go | 2 + pkg/labels/labels.go | 1 + pkg/netutil/networkstore/networkstore.go | 110 ++++++++++++++++++ pkg/ocihook/ocihook.go | 9 +- pkg/portutil/portutil.go | 34 ++++-- pkg/portutil/portutil_test.go | 76 ------------ 22 files changed, 350 insertions(+), 168 deletions(-) create mode 100644 pkg/netutil/networkstore/networkstore.go diff --git a/cmd/nerdctl/compose/compose_port.go b/cmd/nerdctl/compose/compose_port.go index f08b5e9eed7..b4f7b5453d7 100644 --- a/cmd/nerdctl/compose/compose_port.go +++ b/cmd/nerdctl/compose/compose_port.go @@ -88,11 +88,18 @@ func portAction(cmd *cobra.Command, args []string) error { return err } + dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) + if err != nil { + return err + } + po := composer.PortOptions{ ServiceName: args[0], Index: index, Port: port, Protocol: protocol, + DataStore: dataStore, + Namespace: globalOptions.Namespace, } return c.Port(ctx, cmd.OutOrStdout(), po) diff --git a/cmd/nerdctl/compose/compose_port_linux_test.go b/cmd/nerdctl/compose/compose_port_linux_test.go index e066a873401..514740be130 100644 --- a/cmd/nerdctl/compose/compose_port_linux_test.go +++ b/cmd/nerdctl/compose/compose_port_linux_test.go @@ -20,7 +20,11 @@ import ( "fmt" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposePort(t *testing.T) { @@ -75,3 +79,42 @@ services: base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "udp", "svc0", "10000").AssertFail() base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "tcp", "svc0", "10001").AssertFail() } + +// TestComposeMultiplePorts tests whether it is possible to allocate a large +// number of ports. (https://github.com/containerd/nerdctl/issues/4027) +func TestComposeMultiplePorts(t *testing.T) { + var dockerComposeYAML = fmt.Sprintf(` +services: + svc0: + image: %s + command: "sleep infinity" + ports: + - '32000-32060:32000-32060' +`, testutil.AlpineImage) + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", compYamlPath) + + helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.SubTests = []*test.Case{ + { + Description: "Issue #4027 - Allocate a large number of ports.", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "32000") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("0.0.0.0:32000")), + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/compose/compose_ps.go b/cmd/nerdctl/compose/compose_ps.go index badee1755b9..f73b3407d09 100644 --- a/cmd/nerdctl/compose/compose_ps.go +++ b/cmd/nerdctl/compose/compose_ps.go @@ -29,9 +29,9 @@ import ( "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/errdefs" "github.com/containerd/go-cni" - "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" "github.com/containerd/nerdctl/v2/pkg/containerutil" @@ -183,9 +183,9 @@ func psAction(cmd *cobra.Command, args []string) error { var p composeContainerPrintable var err error if format == "json" { - p, err = composeContainerPrintableJSON(ctx, container) + p, err = composeContainerPrintableJSON(ctx, container, globalOptions) } else { - p, err = composeContainerPrintableTab(ctx, container) + p, err = composeContainerPrintableTab(ctx, container, globalOptions) } if err != nil { return err @@ -234,7 +234,7 @@ func psAction(cmd *cobra.Command, args []string) error { // composeContainerPrintableTab constructs composeContainerPrintable with fields // only for console output. -func composeContainerPrintableTab(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { +func composeContainerPrintableTab(ctx context.Context, container containerd.Container, gOptions types.GlobalCommandOptions) (composeContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return composeContainerPrintable{}, err @@ -251,6 +251,18 @@ func composeContainerPrintableTab(ctx context.Context, container containerd.Cont if err != nil { return composeContainerPrintable{}, err } + dataStore, err := clientutil.DataStore(gOptions.DataRoot, gOptions.Address) + if err != nil { + return composeContainerPrintable{}, err + } + containerLabels, err := container.Labels(ctx) + if err != nil { + return composeContainerPrintable{}, err + } + ports, err := portutil.LoadPortMappings(dataStore, gOptions.Namespace, info.ID, containerLabels) + if err != nil { + return composeContainerPrintable{}, err + } return composeContainerPrintable{ Name: info.Labels[labels.Name], @@ -258,13 +270,13 @@ func composeContainerPrintableTab(ctx context.Context, container containerd.Cont Command: formatter.InspectContainerCommandTrunc(spec), Service: info.Labels[labels.ComposeService], State: status, - Ports: formatter.FormatPorts(info.Labels), + Ports: formatter.FormatPorts(ports), }, nil } // composeContainerPrintableJSON constructs composeContainerPrintable with fields // only for json output and compatible docker output. -func composeContainerPrintableJSON(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { +func composeContainerPrintableJSON(ctx context.Context, container containerd.Container, gOptions types.GlobalCommandOptions) (composeContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { return composeContainerPrintable{}, err @@ -294,6 +306,18 @@ func composeContainerPrintableJSON(ctx context.Context, container containerd.Con if err != nil { return composeContainerPrintable{}, err } + dataStore, err := clientutil.DataStore(gOptions.DataRoot, gOptions.Address) + if err != nil { + return composeContainerPrintable{}, err + } + containerLabels, err := container.Labels(ctx) + if err != nil { + return composeContainerPrintable{}, err + } + portMappings, err := portutil.LoadPortMappings(dataStore, gOptions.Namespace, info.ID, containerLabels) + if err != nil { + return composeContainerPrintable{}, err + } return composeContainerPrintable{ ID: container.ID(), @@ -305,7 +329,7 @@ func composeContainerPrintableJSON(ctx context.Context, container containerd.Con State: state, Health: "", ExitCode: exitCode, - Publishers: formatPublishers(info.Labels), + Publishers: formatPublishers(portMappings), }, nil } @@ -321,7 +345,7 @@ type PortPublisher struct { // formatPublishers parses and returns docker-compatible []PortPublisher from // label map. If an error happens, an empty slice is returned. -func formatPublishers(labelMap map[string]string) []PortPublisher { +func formatPublishers(portMappings []cni.PortMapping) []PortPublisher { mapper := func(pm cni.PortMapping) PortPublisher { return PortPublisher{ URL: pm.HostIP, @@ -332,12 +356,8 @@ func formatPublishers(labelMap map[string]string) []PortPublisher { } var dockerPorts []PortPublisher - if portMappings, err := portutil.ParsePortsLabel(labelMap); err == nil { - for _, p := range portMappings { - dockerPorts = append(dockerPorts, mapper(p)) - } - } else { - log.L.Error(err.Error()) + for _, p := range portMappings { + dockerPorts = append(dockerPorts, mapper(p)) } return dockerPorts } diff --git a/cmd/nerdctl/container/container_port.go b/cmd/nerdctl/container/container_port.go index a6237749789..180cacb3d12 100644 --- a/cmd/nerdctl/container/container_port.go +++ b/cmd/nerdctl/container/container_port.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/portutil" ) func PortCommand() *cobra.Command { @@ -81,13 +82,26 @@ func portAction(cmd *cobra.Command, args []string) error { } defer cancel() + dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) + if err != nil { + return err + } + walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - return containerutil.PrintHostPort(ctx, cmd.OutOrStdout(), found.Container, argPort, argProto) + containerLabels, err := found.Container.Labels(ctx) + if err != nil { + return err + } + ports, err := portutil.LoadPortMappings(dataStore, globalOptions.Namespace, found.Container.ID(), containerLabels) + if err != nil { + return err + } + return containerutil.PrintHostPort(ctx, cmd.OutOrStdout(), found.Container, argPort, argProto, ports) }, } req := args[0] diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index b8e0c144f1c..8fb99d42c5c 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -36,7 +36,6 @@ import ( "github.com/containerd/containerd/v2/defaults" "github.com/containerd/containerd/v2/pkg/netns" - "github.com/containerd/errdefs" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -409,21 +408,21 @@ func TestRunPort(t *testing.T) { baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true) } -func TestRunWithInvalidPortThenCleanUp(t *testing.T) { +func TestRunWithManyPortsThenCleanUp(t *testing.T) { testCase := nerdtest.Setup() // docker does not set label restriction to 4096 bytes testCase.Require = require.Not(nerdtest.Docker) testCase.SubTests = []*test.Case{ { - Description: "Run a container with invalid ports, and then clean up.", + Description: "Run a container with many ports, and then clean up.", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--data-root", data.Temp().Path(), "--rm", "-p", "22200-22299:22200-22299", testutil.CommonImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, - Errors: []error{errdefs.ErrInvalidArgument}, + ExitCode: 0, + Errors: []error{}, Output: func(stdout string, t tig.T) { getAddrHash := func(addr string) string { const addrHashLen = 8 diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 76b0ee24137..0dc2cfdc52e 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -37,7 +37,6 @@ import ( "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/cio" "github.com/containerd/containerd/v2/pkg/oci" - "github.com/containerd/go-cni" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/annotations" @@ -61,6 +60,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/mountutil" "github.com/containerd/nerdctl/v2/pkg/namestore" "github.com/containerd/nerdctl/v2/pkg/platformutil" + "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/store" @@ -390,6 +390,11 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } cOpts = append(cOpts, ilOpt) + err = portutil.GeneratePortMappingsConfig(dataStore, options.GOptions.Namespace, id, netLabelOpts.PortMappings) + if err != nil { + return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), fmt.Errorf("Error writing to network-config.json: %v", err) + } + opts = append(opts, propagateInternalContainerdLabelsToOCIAnnotations(), oci.WithAnnotations(strutil.ConvertKVStringsToMap(options.Annotations))) @@ -689,7 +694,6 @@ type internalLabels struct { networks []string ipAddress string ip6Address string - ports []cni.PortMapping macAddress string dnsServers []string dnsSearchDomains []string @@ -741,13 +745,6 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO return nil, err } m[labels.Networks] = string(networksJSON) - if len(internalLabels.ports) > 0 { - portsJSON, err := json.Marshal(internalLabels.ports) - if err != nil { - return nil, err - } - m[labels.Ports] = string(portsJSON) - } if internalLabels.logURI != "" { m[labels.LogURI] = internalLabels.logURI logConfigJSON, err := json.Marshal(internalLabels.logConfig) @@ -909,7 +906,6 @@ func withHealthcheck(options types.ContainerCreateOptions, ensuredImage *imgutil func (il *internalLabels) loadNetOpts(opts types.NetworkOptions) { il.hostname = opts.Hostname il.domainname = opts.Domainname - il.ports = opts.PortMappings il.ipAddress = opts.IPAddress il.ip6Address = opts.IP6Address il.networks = opts.NetworkSlice diff --git a/pkg/cmd/container/inspect.go b/pkg/cmd/container/inspect.go index 63c359ae51a..f9cdb18308a 100644 --- a/pkg/cmd/container/inspect.go +++ b/pkg/cmd/container/inspect.go @@ -25,19 +25,28 @@ import ( "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerdutil" "github.com/containerd/nerdctl/v2/pkg/containerinspector" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/portutil" ) // Inspect prints detailed information for each container in `containers`. func Inspect(ctx context.Context, client *containerd.Client, containers []string, options types.ContainerInspectOptions) ([]any, error) { + dataStore, err := clientutil.DataStore(options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return []any{}, err + } + f := &containerInspector{ mode: options.Mode, size: options.Size, snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter), + dataStore: dataStore, + namespace: options.GOptions.Namespace, } walker := &containerwalker.ContainerWalker{ @@ -45,7 +54,7 @@ func Inspect(ctx context.Context, client *containerd.Client, containers []string OnFound: f.Handler, } - err := walker.WalkAll(ctx, containers, true) + err = walker.WalkAll(ctx, containers, true) if err != nil { return []any{}, err } @@ -58,6 +67,8 @@ type containerInspector struct { size bool snapshotter snapshots.Snapshotter entries []interface{} + dataStore string + namespace string } func (x *containerInspector) Handler(ctx context.Context, found containerwalker.Found) error { @@ -68,6 +79,19 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker. if err != nil { return err } + + containerLabels, err := found.Container.Labels(ctx) + if err != nil { + return err + } + ports, err := portutil.LoadPortMappings(x.dataStore, x.namespace, n.ID, containerLabels) + if err != nil { + return err + } + if n.Process != nil && n.Process.NetNS != nil && len(ports) > 0 { + n.Process.NetNS.PortMappings = ports + } + switch x.mode { case "native": x.entries = append(x.entries, n) diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go index 4f750d54784..080336d9f87 100644 --- a/pkg/cmd/container/kill.go +++ b/pkg/cmd/container/kill.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/labels" @@ -122,14 +123,18 @@ func killContainer(ctx context.Context, container containerd.Container, signal s // cleanupNetwork removes cni network setup, specifically the forwards func cleanupNetwork(ctx context.Context, container containerd.Container, globalOpts types.GlobalCommandOptions) error { return rootlessutil.WithDetachedNetNSIfAny(func() error { - // retrieve info to get current active port mappings - info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) + // retrieve current active port mappings + dataStore, err := clientutil.DataStore(globalOpts.DataRoot, globalOpts.Address) if err != nil { return err } - ports, portErr := portutil.ParsePortsLabel(info.Labels) - if portErr != nil { - return fmt.Errorf("no oci spec: %q", portErr) + containerLabels, err := container.Labels(ctx) + if err != nil { + return err + } + ports, err := portutil.LoadPortMappings(dataStore, globalOpts.Namespace, container.ID(), containerLabels) + if err != nil { + return fmt.Errorf("no oci spec: %q", err) } portMappings := []cni.NamespaceOpts{ cni.WithCapabilityPortMap(ports), diff --git a/pkg/cmd/container/list.go b/pkg/cmd/container/list.go index b23dbb9e14b..3a1d28269e9 100644 --- a/pkg/cmd/container/list.go +++ b/pkg/cmd/container/list.go @@ -32,11 +32,13 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerdutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/labels" + "github.com/containerd/nerdctl/v2/pkg/portutil" ) // List prints containers according to `options`. @@ -162,6 +164,18 @@ func prepareContainers(ctx context.Context, client *containerd.Client, container } else { return nil, fmt.Errorf("can't get container %s status", c.ID()) } + dataStore, err := clientutil.DataStore(options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return nil, err + } + containerLabels, err := c.Labels(ctx) + if err != nil { + return nil, err + } + ports, err := portutil.LoadPortMappings(dataStore, options.GOptions.Namespace, c.ID(), containerLabels) + if err != nil { + return nil, err + } li := ListItem{ Command: formatter.InspectContainerCommand(spec, options.Truncate, true), CreatedAt: info.CreatedAt, @@ -169,7 +183,7 @@ func prepareContainers(ctx context.Context, client *containerd.Client, container Image: info.Image, Platform: info.Labels[labels.Platform], Names: containerutil.GetContainerName(info.Labels), - Ports: formatter.FormatPorts(info.Labels), + Ports: formatter.FormatPorts(ports), Status: status, Runtime: info.Runtime.Name, Labels: formatter.FormatLabels(info.Labels), diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index 1fedcc50432..28048a2f6a1 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -39,6 +39,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" "github.com/containerd/nerdctl/v2/pkg/namestore" + "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/store" ) @@ -191,6 +192,18 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions } netOpts, err := containerutil.NetworkOptionsFromSpec(spec) + if err != nil { + retErr = err + return + } + + portSlice, err := portutil.LoadPortMappings(dataStore, globalOptions.Namespace, id, containerLabels) + if err != nil { + retErr = err + return + } + netOpts.PortMappings = portSlice + if err == nil { networkManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netOpts, client) if err != nil { diff --git a/pkg/composer/port.go b/pkg/composer/port.go index f786b4a3923..db2dac8befb 100644 --- a/pkg/composer/port.go +++ b/pkg/composer/port.go @@ -22,6 +22,7 @@ import ( "io" "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/portutil" ) // PortOptions has args for getting the public port of a given private port/protocol @@ -31,6 +32,8 @@ type PortOptions struct { Index int Port int Protocol string + DataStore string + Namespace string } // Port gets the corresponding public port of a given private port/protocol @@ -48,6 +51,13 @@ func (c *Composer) Port(ctx context.Context, writer io.Writer, po PortOptions) e po.Index, len(containers), po.ServiceName) } container := containers[po.Index-1] - - return containerutil.PrintHostPort(ctx, writer, container, po.Port, po.Protocol) + containerLabels, err := container.Labels(ctx) + if err != nil { + return err + } + ports, err := portutil.LoadPortMappings(po.DataStore, po.Namespace, container.ID(), containerLabels) + if err != nil { + return err + } + return containerutil.PrintHostPort(ctx, writer, container, po.Port, po.Protocol, ports) } diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index d28e720a915..d41e3c7e17a 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -893,12 +893,6 @@ func NetworkOptionsFromSpec(spec *specs.Spec) (types.NetworkOptions, error) { } opts.NetworkSlice = networks - if portsJSON := spec.Annotations[labels.Ports]; portsJSON != "" { - if err := json.Unmarshal([]byte(portsJSON), &opts.PortMappings); err != nil { - return opts, err - } - } - return opts, nil } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 0bebf2310ea..1559e203196 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -42,6 +42,7 @@ import ( "github.com/containerd/containerd/v2/pkg/cio" "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/errdefs" + "github.com/containerd/go-cni" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/consoleutil" @@ -50,7 +51,6 @@ import ( "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels" - "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/signalutil" "github.com/containerd/nerdctl/v2/pkg/strutil" @@ -59,16 +59,7 @@ import ( // PrintHostPort writes to `writer` the public (HostIP:HostPort) of a given `containerPort/protocol` in a container. // if `containerPort < 0`, it writes all public ports of the container. -func PrintHostPort(ctx context.Context, writer io.Writer, container containerd.Container, containerPort int, proto string) error { - l, err := container.Labels(ctx) - if err != nil { - return err - } - ports, err := portutil.ParsePortsLabel(l) - if err != nil { - return err - } - +func PrintHostPort(ctx context.Context, writer io.Writer, container containerd.Container, containerPort int, proto string, ports []cni.PortMapping) error { if containerPort < 0 { for _, p := range ports { fmt.Fprintf(writer, "%d/%s -> %s:%d\n", p.ContainerPort, p.Protocol, p.HostIP, p.HostPort) diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 3801e1ab208..b72952ce68a 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -33,9 +33,7 @@ import ( "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/errdefs" - "github.com/containerd/log" - - "github.com/containerd/nerdctl/v2/pkg/portutil" + "github.com/containerd/go-cni" ) func ContainerStatus(ctx context.Context, c containerd.Container) string { @@ -112,11 +110,7 @@ func Ellipsis(str string, maxDisplayWidth int) string { return str[:maxDisplayWidth-1] + "…" } -func FormatPorts(labelMap map[string]string) string { - ports, err := portutil.ParsePortsLabel(labelMap) - if err != nil { - log.L.Error(err.Error()) - } +func FormatPorts(ports []cni.PortMapping) string { if len(ports) == 0 { return "" } diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index fbcf57d0c75..407d7985ab6 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -694,7 +694,7 @@ func statusFromNative(x containerd.Status, labels map[string]string) string { } } -func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSettings, error) { +func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings, error) { res := &NetworkSettings{ Networks: make(map[string]*NetworkEndpointSettings), } @@ -737,19 +737,12 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting fakeDockerNetworkName := fmt.Sprintf("unknown-%s", x.Name) res.Networks[fakeDockerNetworkName] = nes - if portsLabel, ok := sp.Annotations[labels.Ports]; ok { - var ports []cni.PortMapping - err := json.Unmarshal([]byte(portsLabel), &ports) - if err != nil { - return nil, err - } - nports, err := convertToNatPort(ports) - if err != nil { - return nil, err - } - for portLabel, portBindings := range *nports { - resPortMap[portLabel] = portBindings - } + nports, err := convertToNatPort(n.PortMappings) + if err != nil { + return nil, err + } + for portLabel, portBindings := range *nports { + resPortMap[portLabel] = portBindings } if x.Index == n.PrimaryInterface { diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 3e7602d8e67..621e64bf2ff 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -33,6 +33,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/go-cni" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" @@ -413,11 +414,17 @@ func TestNetworkSettingsFromNative(t *testing.T) { Addrs: []string{"10.0.4.30/24"}, }, }, + PortMappings: []cni.PortMapping{ + { + HostPort: 8075, + ContainerPort: 77, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + }, }, s: &specs.Spec{ - Annotations: map[string]string{ - "nerdctl/ports": "[{\"HostPort\":8075,\"ContainerPort\":77,\"Protocol\":\"tcp\",\"HostIP\":\"127.0.0.1\"}]", - }, + Annotations: map[string]string{}, }, expected: &NetworkSettings{ Ports: &nat.PortMap{ diff --git a/pkg/inspecttypes/native/container.go b/pkg/inspecttypes/native/container.go index de015dd5f94..1bd421a2d62 100644 --- a/pkg/inspecttypes/native/container.go +++ b/pkg/inspecttypes/native/container.go @@ -21,6 +21,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" + "github.com/containerd/go-cni" ) // Container corresponds to a containerd-native container object. @@ -43,6 +44,7 @@ type NetNS struct { // Zero means unset. PrimaryInterface int `json:"PrimaryInterface,omitempty"` Interfaces []NetInterface `json:"Interfaces,omitempty"` + PortMappings []cni.PortMapping } // NetInterface wraps net.Interface for JSON marshallability. diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 0c50324fee2..792c74dbf9f 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -57,6 +57,7 @@ const ( // Currently, the length of the slice must be 1. Networks = Prefix + "networks" + // DEPRECATED : https://github.com/containerd/nerdctl/pull/4290 // Ports is a JSON-marshalled string of []cni.PortMapping . Ports = Prefix + "ports" diff --git a/pkg/netutil/networkstore/networkstore.go b/pkg/netutil/networkstore/networkstore.go new file mode 100644 index 00000000000..0faa78ba9cf --- /dev/null +++ b/pkg/netutil/networkstore/networkstore.go @@ -0,0 +1,110 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package networkstore + +import ( + "encoding/json" + "errors" + "fmt" + "path/filepath" + + "github.com/containerd/go-cni" + + "github.com/containerd/nerdctl/v2/pkg/store" +) + +const ( + containersDirBaseName = "containers" + networkConfigName = "network-config.json" +) + +var ErrNetworkStore = errors.New("network-store error") + +func New(dataStore, namespace, containerID string) (ns *NetworkStore, err error) { + defer func() { + if err != nil { + err = errors.Join(ErrNetworkStore, err) + } + }() + + if dataStore == "" || namespace == "" || containerID == "" { + return nil, fmt.Errorf("either dataStore or namespace or containerID is empty") + } + + st, err := store.New(filepath.Join(dataStore, containersDirBaseName, namespace, containerID), 0, 0o600) + if err != nil { + return nil, err + } + + return &NetworkStore{ + safeStore: st, + }, nil +} + +type NetworkStore struct { + safeStore store.Store + + PortMappings []cni.PortMapping +} + +func (ns *NetworkStore) Acquire(portMappings []cni.PortMapping) (err error) { + defer func() { + if err != nil { + err = errors.Join(ErrNetworkStore, err) + } + }() + + portsJSON, err := json.Marshal(portMappings) + if err != nil { + return fmt.Errorf("failed to marshal port mappings to JSON: %w", err) + } + + return ns.safeStore.WithLock(func() error { + return ns.safeStore.Set(portsJSON, networkConfigName) + }) +} + +func (ns *NetworkStore) Load() (err error) { + defer func() { + if err != nil { + err = errors.Join(ErrNetworkStore, err) + } + }() + + return ns.safeStore.WithLock(func() error { + doesExist, err := ns.safeStore.Exists(networkConfigName) + if err != nil || !doesExist { + return err + } + + data, err := ns.safeStore.Get(networkConfigName) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + err = nil + } + return err + } + + var ports []cni.PortMapping + if err := json.Unmarshal(data, &ports); err != nil { + return fmt.Errorf("failed to parse port mappings %v: %w", ports, err) + } + ns.PortMappings = ports + + return err + }) +} diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index a83275e907c..89b6c6b1410 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -45,6 +45,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/netutil/nettype" "github.com/containerd/nerdctl/v2/pkg/ocihook/state" + "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/store" ) @@ -208,11 +209,11 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath, brid } } - if portsJSON := o.state.Annotations[labels.Ports]; portsJSON != "" { - if err := json.Unmarshal([]byte(portsJSON), &o.ports); err != nil { - return nil, err - } + ports, err := portutil.LoadPortMappings(o.dataStore, namespace, o.state.ID, o.state.Annotations) + if err != nil { + return nil, err } + o.ports = ports if ipAddress, ok := o.state.Annotations[labels.IPAddress]; ok { o.containerIP = ipAddress diff --git a/pkg/portutil/portutil.go b/pkg/portutil/portutil.go index a832470abce..681988c654f 100644 --- a/pkg/portutil/portutil.go +++ b/pkg/portutil/portutil.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/labels" + "github.com/containerd/nerdctl/v2/pkg/netutil/networkstore" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -139,16 +140,35 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) { return mr, nil } -// ParsePortsLabel parses JSON-marshalled string from label map -// (under `labels.Ports` key) and returns []cni.PortMapping. -func ParsePortsLabel(labelMap map[string]string) ([]cni.PortMapping, error) { - portsJSON := labelMap[labels.Ports] - if portsJSON == "" { - return []cni.PortMapping{}, nil +func GeneratePortMappingsConfig(dataStore, namespace, id string, portMappings []cni.PortMapping) error { + ns, err := networkstore.New(dataStore, namespace, id) + if err != nil { + return err } + return ns.Acquire(portMappings) +} + +func LoadPortMappings(dataStore, namespace, id string, containerLabels map[string]string) ([]cni.PortMapping, error) { var ports []cni.PortMapping + + ns, err := networkstore.New(dataStore, namespace, id) + if err != nil { + return ports, err + } + if err = ns.Load(); err != nil { + return ports, err + } + if len(ns.PortMappings) != 0 { + return ns.PortMappings, nil + } + + portsJSON := containerLabels[labels.Ports] + if portsJSON == "" { + return ports, nil + } if err := json.Unmarshal([]byte(portsJSON), &ports); err != nil { - return nil, fmt.Errorf("failed to parse label %q=%q: %s", labels.Ports, portsJSON, err.Error()) + return ports, fmt.Errorf("failed to parse label %q=%q: %s", labels.Ports, portsJSON, err.Error()) } + log.L.Warnf("container %s (%s) is using legacy port mapping configuration. To ensure compatibility with the new port mapping logic, please recreate this container. For more details, see: https://github.com/containerd/nerdctl/pull/4290", containerLabels[labels.Name], id[:12]) return ports, nil } diff --git a/pkg/portutil/portutil_test.go b/pkg/portutil/portutil_test.go index 46b9eff7544..02f390bb9ab 100644 --- a/pkg/portutil/portutil_test.go +++ b/pkg/portutil/portutil_test.go @@ -26,7 +26,6 @@ import ( "github.com/containerd/go-cni" - "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -152,81 +151,6 @@ func TestParseFlagPWithPlatformSpec(t *testing.T) { } } -func TestParsePortsLabel(t *testing.T) { - tests := []struct { - name string - labelMap map[string]string - want []cni.PortMapping - wantErr bool - }{ - { - name: "normal", - labelMap: map[string]string{ - labels.Ports: "[{\"HostPort\":12345,\"ContainerPort\":10000,\"Protocol\":\"tcp\",\"HostIP\":\"0.0.0.0\"}]", - }, - want: []cni.PortMapping{ - { - HostPort: 12345, - ContainerPort: 10000, - Protocol: "tcp", - HostIP: "0.0.0.0", - }, - }, - wantErr: false, - }, - { - name: "empty ports (value empty)", - labelMap: map[string]string{ - labels.Ports: "", - }, - want: []cni.PortMapping{}, - wantErr: false, - }, - { - name: "empty ports (key not exists)", - labelMap: map[string]string{}, - want: []cni.PortMapping{}, - wantErr: false, - }, - { - name: "parse error (wrong format)", - labelMap: map[string]string{ - labels.Ports: "{\"HostPort\":12345,\"ContainerPort\":10000,\"Protocol\":\"tcp\",\"HostIP\":\"0.0.0.0\"}", - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParsePortsLabel(tt.labelMap) - if err != nil { - t.Log(err) - assert.Equal(t, true, tt.wantErr) - } - if !reflect.DeepEqual(got, tt.want) { - assert.Equal(t, len(got), len(tt.want)) - if len(got) > 0 { - sort.Slice(got, func(i, j int) bool { - return got[i].HostPort < got[j].HostPort - }) - assert.Equal( - t, - got[len(got)-1].HostPort-got[0].HostPort, - got[len(got)-1].ContainerPort-got[0].ContainerPort, - ) - for i := range len(got) { - assert.Equal(t, got[i].HostPort, tt.want[i].HostPort) - assert.Equal(t, got[i].ContainerPort, tt.want[i].ContainerPort) - assert.Equal(t, got[i].Protocol, tt.want[i].Protocol) - assert.Equal(t, got[i].HostIP, tt.want[i].HostIP) - } - } - } - }) - } -} - func TestParseFlagP(t *testing.T) { type args struct { s string From c13417d74a5d1d310f16dc830edef6e24f3b0f00 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 25 Jun 2025 09:34:46 +0000 Subject: [PATCH 106/868] docs: add network-config.json description to dir.md Signed-off-by: Hayato Kiwata --- docs/dir.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dir.md b/docs/dir.md index b3350cddabc..61f5efae3a7 100644 --- a/docs/dir.md +++ b/docs/dir.md @@ -35,6 +35,7 @@ Files: - `-json.log`: used by `nerdctl logs` - `oci-hook.*.log`: logs of the OCI hook - `lifecycle.json`: used to store stateful information about the container that can only be retrieved through OCI hooks +- `network-config.json`: used to store port mapping information for containers run with the `-p` option. ### `//names/` e.g. `/var/lib/nerdctl/1935db59/names/default` From dc7971fb4387d383534739a67ac00aaeb454935a Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 11:11:44 -0700 Subject: [PATCH 107/868] internal/filesystem minor fix In case of error during write, the destination is being removed (before being possibly restored). This may lead to certain (failure) scenarios where the inode would change, effectively breaking container mounted files. Signed-off-by: apostasie --- pkg/internal/filesystem/helpers.go | 18 +++++++++--------- pkg/internal/filesystem/writefile_rollback.go | 9 ++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/internal/filesystem/helpers.go b/pkg/internal/filesystem/helpers.go index f9be0cb2f54..75ce37109b8 100644 --- a/pkg/internal/filesystem/helpers.go +++ b/pkg/internal/filesystem/helpers.go @@ -54,6 +54,7 @@ func ensureRecovery(filename string) (err error) { if err = backupRestore(filename); err != nil { return err } + _ = backupRemove(filename) } else { // We do not see a backup. // Do we have a final destination then? @@ -101,6 +102,10 @@ func backupRestore(path string) error { return err } +func backupRemove(path string) error { + return os.Remove(backupLocation(path)) +} + // backupExists checks if a backup file exists for file located at `path`. func backupExists(path string) (bool, error) { _, err := os.Stat(backupLocation(path)) @@ -190,16 +195,16 @@ func internalCopy(sourcePath, destinationPath string) (err error) { return err } + defer func() { + err = errors.Join(err, source.Close()) + }() + // Read file length srcInfo, err := source.Stat() if err != nil { return err } - defer func() { - err = errors.Join(err, source.Close()) - }() - return fileWrite(source, srcInfo.Size(), destinationPath, privateFilePermission, srcInfo.ModTime()) } @@ -220,11 +225,6 @@ func fileWrite(source io.Reader, size int64, destinationPath string, perm os.Fil if mustClose { err = errors.Join(err, destination.Close()) } - - // Remove destination if we failed anywhere. Ignore removal failures. - if err != nil { - _ = os.Remove(destinationPath) - } }() // Copy over diff --git a/pkg/internal/filesystem/writefile_rollback.go b/pkg/internal/filesystem/writefile_rollback.go index 31ec35c3d18..4608526406f 100644 --- a/pkg/internal/filesystem/writefile_rollback.go +++ b/pkg/internal/filesystem/writefile_rollback.go @@ -61,6 +61,11 @@ func WriteFileWithRollback(filename string, data []byte, perm os.FileMode) (roll } } + // Make sure no leftover backup file is here + // Note: this happens after a successful write. Generally not a problem, except if the file is then deleted, + // then written to again, and that second write would fail. + _ = backupRemove(filename) + // Create the marker. Failure to do so is a hard error. if err = markerCreate(filename, markerData); err != nil { return nil, err @@ -68,8 +73,10 @@ func WriteFileWithRollback(filename string, data []byte, perm os.FileMode) (roll // If the file exists, we need to back it up. if markerData == "" { - // Back it up now. + // Back it up now. Remove on failure. if err = backupSave(filename); err != nil { + _ = backupRemove(filename) + _ = markerRemove(filename) return nil, err } } From f7a92b1c3d39ac6a2aa5f5350154c93e748b317f Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 14:41:58 -0700 Subject: [PATCH 108/868] Improve debugging for healthcheck tests Signed-off-by: apostasie --- .golangci.yml | 2 +- .../container/container_health_check_test.go | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d283de0545d..058e0ec87d3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -121,7 +121,7 @@ linters: arguments: [7] - name: function-length # 155 occurrences (at default 0, 75). Really long functions should really be broken up in most cases. - arguments: [0, 450] + arguments: [0, 500] - name: cyclomatic # 204 occurrences (at default 10) arguments: [100] diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_test.go index 43c549441c1..aa2d1603313 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_test.go @@ -17,7 +17,9 @@ package container import ( + "encoding/json" "errors" + "fmt" "strings" "testing" "time" @@ -83,6 +85,8 @@ func TestContainerHealthCheckBasic(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state to be present") assert.Equal(t, healthcheck.Healthy, h.Status) assert.Equal(t, 0, h.FailingStreak) @@ -158,6 +162,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.FailingStreak, 1) assert.Assert(t, len(inspect.State.Health.Log) > 0, "expected health log to have entries") @@ -194,6 +200,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Equal(t, h.FailingStreak, 2) @@ -224,6 +232,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Starting) assert.Equal(t, h.FailingStreak, 0) @@ -253,6 +263,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Equal(t, h.FailingStreak, 1) @@ -303,6 +315,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, len(h.Log) > 0) @@ -334,6 +348,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, h.FailingStreak == 0) @@ -365,6 +381,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Equal(t, h.FailingStreak, 0) @@ -398,11 +416,13 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Assert(t, len(h.Log) >= 3, "expected at least 3 health log entries") for _, log := range h.Log { - assert.Assert(t, len(log.Output) >= 1024, "each output should be >= 1024 bytes") + assert.Assert(t, len(log.Output) >= 1024, fmt.Sprintf("each output should be >= 1024 bytes, was: %s", log.Output)) } }), } @@ -434,6 +454,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, len(h.Log) <= 5, "expected health log to contain at most 5 entries") @@ -462,6 +484,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(_ string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Equal(t, h.FailingStreak, 0) @@ -499,6 +523,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) assert.Assert(t, h.FailingStreak >= 3) @@ -532,6 +558,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Starting) assert.Equal(t, h.FailingStreak, 0, "failing streak should not increase during start period") @@ -561,6 +589,8 @@ func TestContainerHealthCheckAdvance(t *testing.T) { Output: expect.All(func(stdout string, t tig.T) { inspect := nerdtest.InspectContainer(helpers, data.Identifier()) h := inspect.State.Health + debug, _ := json.MarshalIndent(h, "", " ") + t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy, "expected healthy status even during start-period") assert.Equal(t, h.FailingStreak, 0) From 53e69a38005f5d4b123c020b8c0d140af6c7e43b Mon Sep 17 00:00:00 2001 From: apostasie Date: Mon, 23 Jun 2025 15:01:37 -0700 Subject: [PATCH 109/868] Rewrite compose test (fix 4146) Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_up_linux_test.go | 63 ++++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index e4b69ee5550..d9e50812411 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -29,11 +29,14 @@ import ( "gotest.tools/v3/icmd" "github.com/containerd/log" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) @@ -375,10 +378,10 @@ services: } func TestComposeUpWithExternalNetwork(t *testing.T) { - containerName1 := testutil.Identifier(t) + "-1" - containerName2 := testutil.Identifier(t) + "-2" - networkName := testutil.Identifier(t) + "-network" - var dockerComposeYaml1 = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var dockerComposeYaml1 = fmt.Sprintf(` services: %s: image: %s @@ -390,8 +393,8 @@ services: networks: %s: external: true -`, containerName1, testutil.NginxAlpineImage, containerName1, networkName, networkName) - var dockerComposeYaml2 = fmt.Sprintf(` +`, data.Identifier("con-1"), testutil.NginxAlpineImage, data.Identifier("con-1"), data.Identifier("network"), data.Identifier("network")) + var dockerComposeYaml2 = fmt.Sprintf(` services: %s: image: %s @@ -403,26 +406,34 @@ services: networks: %s: external: true -`, containerName2, testutil.NginxAlpineImage, containerName2, networkName, networkName) - comp1 := testutil.NewComposeDir(t, dockerComposeYaml1) - defer comp1.CleanUp() - comp2 := testutil.NewComposeDir(t, dockerComposeYaml2) - defer comp2.CleanUp() - base := testutil.NewBase(t) - // Create the test network - base.Cmd("network", "create", networkName).AssertOK() - defer base.Cmd("network", "rm", networkName).Run() - // Run the first compose - base.ComposeCmd("-f", comp1.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp1.YAMLFullPath(), "down", "-v").Run() - // Run the second compose - base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").Run() - // Down the second compose - base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").AssertOK() - // Run the second compose again - base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() - base.Cmd("exec", containerName1, "wget", "-qO-", "http://"+containerName2).AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) +`, data.Identifier("con-2"), testutil.NginxAlpineImage, data.Identifier("con-2"), data.Identifier("network"), data.Identifier("network")) + tmp := data.Temp() + + tmp.Save(dockerComposeYaml1, "project-1", "compose.yaml") + tmp.Save(dockerComposeYaml2, "project-2", "compose.yaml") + + helpers.Ensure("network", "create", data.Identifier("network")) + helpers.Ensure("compose", "-f", tmp.Path("project-1", "compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "down", "-v") + helpers.Ensure("compose", "-f", tmp.Path("project-2", "compose.yaml"), "up", "-d") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("con-2")) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("exec", data.Identifier("con-1"), "cat", "/etc/hosts") + return helpers.Command("exec", data.Identifier("con-1"), "wget", "-qO-", "http://"+data.Identifier("con-2")) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("project-1", "compose.yaml"), "down", "-v") + helpers.Anyhow("compose", "-f", data.Temp().Path("project-2", "compose.yaml"), "down", "-v") + helpers.Anyhow("network", "rm", data.Identifier("network")) + } + + testCase.Run(t) } func TestComposeUpWithBypass4netns(t *testing.T) { From b6ceafb667709548124fc5424d597c8ad01cddc0 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 25 Jun 2025 12:48:06 -0700 Subject: [PATCH 110/868] Allow compose exec concurrency Signed-off-by: apostasie --- cmd/nerdctl/compose/compose_exec_linux_test.go | 5 +++++ pkg/composer/exec.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/cmd/nerdctl/compose/compose_exec_linux_test.go b/cmd/nerdctl/compose/compose_exec_linux_test.go index 8fca8d2376b..d0ee72403b5 100644 --- a/cmd/nerdctl/compose/compose_exec_linux_test.go +++ b/cmd/nerdctl/compose/compose_exec_linux_test.go @@ -279,6 +279,11 @@ services: data.Labels().Set("projectName", strings.ToLower(filepath.Base(data.Temp().Dir()))) helpers.Ensure("compose", "-f", yamlPath, "up", "-d", "svc0") + + // Make sure all containers are started so that /etc/hosts is consistent. + for _, index := range []string{"1", "2", "3"} { + nerdtest.EnsureContainerStarted(helpers, fmt.Sprintf("%s-svc0-%s", data.Labels().Get("projectName"), index)) + } } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { diff --git a/pkg/composer/exec.go b/pkg/composer/exec.go index 4e34bfa2a86..bd4d0d8b8d2 100644 --- a/pkg/composer/exec.go +++ b/pkg/composer/exec.go @@ -49,6 +49,11 @@ type ExecOptions struct { // Exec executes a given command on a running container specified by // `ServiceName` (and `Index` if it has multiple instances). func (c *Composer) Exec(ctx context.Context, eo ExecOptions) error { + // Exec does not need to lock and should allow concurrency. + if err := Unlock(); err != nil { + return err + } + containers, err := c.Containers(ctx, eo.ServiceName) if err != nil { return fmt.Errorf("fail to get containers for service %s: %w", eo.ServiceName, err) From 9f3eab454f067b7678fb6953544888771c965889 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:13:21 +0000 Subject: [PATCH 111/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.2.2+incompatible to 28.3.0+incompatible - [Commits](https://github.com/docker/cli/compare/v28.2.2...v28.3.0) Updates `github.com/docker/docker` from 28.2.2+incompatible to 28.3.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.2.2...v28.3.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.3.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.3.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6478db4af14..d1352b940b2 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.2.2+incompatible //gomodjail:unconfined - github.com/docker/docker v28.2.2+incompatible //gomodjail:unconfined + github.com/docker/cli v28.3.0+incompatible //gomodjail:unconfined + github.com/docker/docker v28.3.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 3fe53e8f653..680c32cac7b 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= -github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.3.0+incompatible h1:s+ttruVLhB5ayeuf2BciwDVxYdKi+RoUlxmwNHV3Vfo= +github.com/docker/cli v28.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ= +github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= From 7f8422b33fcbfd67ee357998fc1a49ea29ac01a5 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Fri, 27 Jun 2025 15:54:21 +0800 Subject: [PATCH 112/868] bugfix:wait health check log complete Signed-off-by: ningmingxiao --- pkg/healthcheck/executor.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/healthcheck/executor.go b/pkg/healthcheck/executor.go index 1b3f16a7460..f5c4216b302 100644 --- a/pkg/healthcheck/executor.go +++ b/pkg/healthcheck/executor.go @@ -89,8 +89,9 @@ func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck select { case <-time.After(hc.Timeout): _ = process.Kill(ctx, syscall.SIGKILL) - go func() { <-exitStatusC }() - + <-exitStatusC + process.IO().Wait() + process.IO().Close() msg := fmt.Sprintf("Health check exceeded timeout (%v)", hc.Timeout) if out := outputBuf.String(); len(out) > 0 { msg = fmt.Sprintf("Health check exceeded timeout (%v): %s", hc.Timeout, out) @@ -105,6 +106,8 @@ func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck }, nil case exitStatus := <-exitStatusC: + process.IO().Wait() + process.IO().Close() code, _, _ := exitStatus.Result() return &HealthcheckResult{ ExitCode: int(code), From 59890cbd8784f3c00fd5f3d813e7be00a8e7b538 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Fri, 27 Jun 2025 17:47:01 +0000 Subject: [PATCH 113/868] fix: allow storing additional network info in network-config.json When we specify the `-p` option with `nerdctl run`, etc., the port mapping information is stored in the following `network-config.json`. ``` //containers///network-config.json ``` However, the current implementation can only store port mapping information and does not have the extensibility to store other network-related information. This point was feedback in the following PR. - https://github.com/containerd/nerdctl/pull/4376 Therefore, this commit updates the `network-config.json` to allow for the expansion of the information that can be stored. Signed-off-by: Hayato Kiwata --- docs/dir.md | 2 +- pkg/cmd/container/create.go | 6 +++++- pkg/netutil/networkstore/networkstore.go | 22 +++++++++++++--------- pkg/portutil/portutil.go | 8 ++++---- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/dir.md b/docs/dir.md index 61f5efae3a7..43da6dff528 100644 --- a/docs/dir.md +++ b/docs/dir.md @@ -35,7 +35,7 @@ Files: - `-json.log`: used by `nerdctl logs` - `oci-hook.*.log`: logs of the OCI hook - `lifecycle.json`: used to store stateful information about the container that can only be retrieved through OCI hooks -- `network-config.json`: used to store port mapping information for containers run with the `-p` option. +- `network-config.json`: used to store container-specific network configuration, such as port mappings. ### `//names/` e.g. `/var/lib/nerdctl/1935db59/names/default` diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 0dc2cfdc52e..a0c8fc2bf9b 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -59,6 +59,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/maputil" "github.com/containerd/nerdctl/v2/pkg/mountutil" "github.com/containerd/nerdctl/v2/pkg/namestore" + "github.com/containerd/nerdctl/v2/pkg/netutil/networkstore" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" @@ -390,7 +391,10 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } cOpts = append(cOpts, ilOpt) - err = portutil.GeneratePortMappingsConfig(dataStore, options.GOptions.Namespace, id, netLabelOpts.PortMappings) + netConf := networkstore.NetworkConfig{ + PortMappings: netLabelOpts.PortMappings, + } + err = portutil.StoreNetworkConfig(dataStore, options.GOptions.Namespace, id, netConf) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), fmt.Errorf("Error writing to network-config.json: %v", err) } diff --git a/pkg/netutil/networkstore/networkstore.go b/pkg/netutil/networkstore/networkstore.go index 0faa78ba9cf..2df313459b1 100644 --- a/pkg/netutil/networkstore/networkstore.go +++ b/pkg/netutil/networkstore/networkstore.go @@ -55,26 +55,30 @@ func New(dataStore, namespace, containerID string) (ns *NetworkStore, err error) }, nil } +type NetworkConfig struct { + PortMappings []cni.PortMapping `json:"portMappings,omitempty"` +} + type NetworkStore struct { safeStore store.Store - PortMappings []cni.PortMapping + NetConf NetworkConfig } -func (ns *NetworkStore) Acquire(portMappings []cni.PortMapping) (err error) { +func (ns *NetworkStore) Acquire(netConf NetworkConfig) (err error) { defer func() { if err != nil { err = errors.Join(ErrNetworkStore, err) } }() - portsJSON, err := json.Marshal(portMappings) + netConfJSON, err := json.Marshal(netConf) if err != nil { - return fmt.Errorf("failed to marshal port mappings to JSON: %w", err) + return fmt.Errorf("failed to marshal network config to JSON: %w", err) } return ns.safeStore.WithLock(func() error { - return ns.safeStore.Set(portsJSON, networkConfigName) + return ns.safeStore.Set(netConfJSON, networkConfigName) }) } @@ -99,11 +103,11 @@ func (ns *NetworkStore) Load() (err error) { return err } - var ports []cni.PortMapping - if err := json.Unmarshal(data, &ports); err != nil { - return fmt.Errorf("failed to parse port mappings %v: %w", ports, err) + var netConf NetworkConfig + if err := json.Unmarshal(data, &netConf); err != nil { + return fmt.Errorf("failed to parse network config %v: %w", netConf, err) } - ns.PortMappings = ports + ns.NetConf = netConf return err }) diff --git a/pkg/portutil/portutil.go b/pkg/portutil/portutil.go index 681988c654f..73853767f16 100644 --- a/pkg/portutil/portutil.go +++ b/pkg/portutil/portutil.go @@ -140,12 +140,12 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) { return mr, nil } -func GeneratePortMappingsConfig(dataStore, namespace, id string, portMappings []cni.PortMapping) error { +func StoreNetworkConfig(dataStore, namespace, id string, netConf networkstore.NetworkConfig) error { ns, err := networkstore.New(dataStore, namespace, id) if err != nil { return err } - return ns.Acquire(portMappings) + return ns.Acquire(netConf) } func LoadPortMappings(dataStore, namespace, id string, containerLabels map[string]string) ([]cni.PortMapping, error) { @@ -158,8 +158,8 @@ func LoadPortMappings(dataStore, namespace, id string, containerLabels map[strin if err = ns.Load(); err != nil { return ports, err } - if len(ns.PortMappings) != 0 { - return ns.PortMappings, nil + if len(ns.NetConf.PortMappings) != 0 { + return ns.NetConf.PortMappings, nil } portsJSON := containerLabels[labels.Ports] From 53ee7d84d2c3551aa8a21d7dc37aa0963928e225 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:00:55 +0000 Subject: [PATCH 114/868] build(deps): bump github.com/Masterminds/semver/v3 from 3.3.1 to 3.4.0 Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/Masterminds/semver/releases) - [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md) - [Commits](https://github.com/Masterminds/semver/compare/v3.3.1...v3.4.0) --- updated-dependencies: - dependency-name: github.com/Masterminds/semver/v3 dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d1352b940b2..968288fe37b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ module github.com/containerd/nerdctl/v2 go 1.23.5 require ( - github.com/Masterminds/semver/v3 v3.3.1 + github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 github.com/compose-spec/compose-go/v2 v2.6.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 680c32cac7b..b0262d3a5e0 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= From c731c259a91fe372e3b45c57b6f01e474dbb6392 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 27 Jun 2025 16:29:10 +0900 Subject: [PATCH 115/868] Add build tag `no_ipfs` ``` BUILDTAGS=no_ipfs make ``` Discussed in issue 1986 Signed-off-by: Akihiro Suda --- .github/workflows/job-build.yml | 9 + BUILDING.md | 22 ++ Makefile | 5 +- pkg/ipfs/{image.go => image_ipfs.go} | 2 + pkg/ipfs/image_noipfs.go | 39 ++++ pkg/ipfs/noipfs.go | 25 ++ pkg/ipfs/registry.go | 307 ------------------------- pkg/ipfs/registry_ipfs.go | 330 +++++++++++++++++++++++++++ pkg/ipfs/registry_noipfs.go | 27 +++ pkg/referenceutil/cid_ipfs.go | 29 +++ pkg/referenceutil/cid_noipfs.go | 29 +++ pkg/referenceutil/referenceutil.go | 5 +- 12 files changed, 518 insertions(+), 311 deletions(-) create mode 100644 BUILDING.md rename pkg/ipfs/{image.go => image_ipfs.go} (99%) create mode 100644 pkg/ipfs/image_noipfs.go create mode 100644 pkg/ipfs/noipfs.go create mode 100644 pkg/ipfs/registry_ipfs.go create mode 100644 pkg/ipfs/registry_noipfs.go create mode 100644 pkg/referenceutil/cid_ipfs.go create mode 100644 pkg/referenceutil/cid_noipfs.go diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index d20822d68d3..169e95112ef 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -99,3 +99,12 @@ jobs: build linux s390x [ ! "$failure" ] || exit 1 + + - if: ${{ env.GO_VERSION != '' }} + name: "Run: make binaries with custom BUILDTAGS" + run: | + set -eux + # no_ipfs: make sure it does not incur any IPFS-related dependency + go mod vendor + rm -rf vendor/github.com/ipfs vendor/github.com/multiformats + BUILDTAGS=no_ipfs make binaries diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 00000000000..775cc2977aa --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,22 @@ +# Building nerdctl + +To build nerdctl, use `make`: + +```bash +make +sudo make install +``` + +Alternatively, nerdctl can be also built with `go build ./cmd/nerdctl`. +However, this is not recommended as it does not populate the version string (`nerdctl -v`). + +## Customization + +To specify build tags, set the `BUILDTAGS` variable as follows: + +```bash +BUILDTAGS=no_ipfs make +``` + +The following build tags are supported: +* `no_ipfs` (since v2.1.3): Disable IPFS diff --git a/Makefile b/Makefile index 7e0638d448b..1dd5fbd0720 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,9 @@ LINT_COMMIT_RANGE ?= main..HEAD GO_BUILD_LDFLAGS ?= -s -w GO_BUILD_FLAGS ?= +BUILDTAGS ?= +GO_TAGS=$(if $(BUILDTAGS),-tags "$(strip $(BUILDTAGS))",) + ########################## # Helpers ########################## @@ -54,7 +57,7 @@ ifdef VERBOSE VERBOSE_FLAG_LONG := --verbose endif -export GO_BUILD=CGO_ENABLED=0 GOOS=$(GOOS) $(GO) -C $(MAKEFILE_DIR) build -ldflags "$(GO_BUILD_LDFLAGS) $(VERBOSE_FLAG) -X $(PACKAGE)/pkg/version.Version=$(VERSION) -X $(PACKAGE)/pkg/version.Revision=$(REVISION)" +export GO_BUILD=CGO_ENABLED=0 GOOS=$(GOOS) $(GO) -C $(MAKEFILE_DIR) build $(GO_TAGS) -ldflags "$(GO_BUILD_LDFLAGS) $(VERBOSE_FLAG) -X $(PACKAGE)/pkg/version.Version=$(VERSION) -X $(PACKAGE)/pkg/version.Revision=$(REVISION)" ifndef NO_COLOR NC := \033[0m diff --git a/pkg/ipfs/image.go b/pkg/ipfs/image_ipfs.go similarity index 99% rename from pkg/ipfs/image.go rename to pkg/ipfs/image_ipfs.go index 84d73bda32e..6e8e49105e5 100644 --- a/pkg/ipfs/image.go +++ b/pkg/ipfs/image_ipfs.go @@ -1,3 +1,5 @@ +//go:build !no_ipfs + /* Copyright The containerd Authors. diff --git a/pkg/ipfs/image_noipfs.go b/pkg/ipfs/image_noipfs.go new file mode 100644 index 00000000000..43210f6e9df --- /dev/null +++ b/pkg/ipfs/image_noipfs.go @@ -0,0 +1,39 @@ +//go:build no_ipfs + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "context" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images/converter" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/imgutil" +) + +// EnsureImage pull the specified image from IPFS. +func EnsureImage(ctx context.Context, client *containerd.Client, scheme, ref, ipfsPath string, options types.ImagePullOptions) (*imgutil.EnsuredImage, error) { + return nil, ErrNotImplemented +} + +// Push pushes the specified image to IPFS. +func Push(ctx context.Context, client *containerd.Client, rawRef string, layerConvert converter.ConvertFunc, allPlatforms bool, platform []string, ensureImage bool, ipfsPath string) (string, error) { + return "", ErrNotImplemented +} diff --git a/pkg/ipfs/noipfs.go b/pkg/ipfs/noipfs.go new file mode 100644 index 00000000000..4a1d29b0d3c --- /dev/null +++ b/pkg/ipfs/noipfs.go @@ -0,0 +1,25 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "fmt" + + "github.com/containerd/errdefs" +) + +var ErrNotImplemented = fmt.Errorf("%w: ipfs is disabled by the distributor of this build", errdefs.ErrNotImplemented) diff --git a/pkg/ipfs/registry.go b/pkg/ipfs/registry.go index 038b44f70ce..0e620bd2bfd 100644 --- a/pkg/ipfs/registry.go +++ b/pkg/ipfs/registry.go @@ -17,25 +17,7 @@ package ipfs import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "regexp" - "strconv" - "strings" "time" - - "github.com/opencontainers/go-digest" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - - "github.com/containerd/containerd/v2/core/content" - "github.com/containerd/containerd/v2/core/images" - "github.com/containerd/log" - ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client" ) // RegistryOptions represents options to configure the registry. @@ -50,292 +32,3 @@ type RegistryOptions struct { // IpfsPath is the IPFS_PATH value to be used for ipfs command. IpfsPath string } - -func NewRegistry(options RegistryOptions) (http.Handler, error) { - // HTTP is only supported as of now. We can add https support here if needed (e.g. for connecting to it via proxy, etc) - iurl, err := ipfsclient.GetIPFSAPIAddress(lookupIPFSPath(options.IpfsPath), "http") - if err != nil { - return nil, err - } - return &server{options, ipfsclient.New(iurl)}, nil -} - -// server is a read-only registry which converts OCI Distribution Spec's pull-related API to IPFS -// https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#pull -type server struct { - config RegistryOptions - ipfsclient *ipfsclient.Client -} - -var manifestRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/manifests/(.*)`) -var blobsRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/blobs/(.*)`) - -func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - cid, content, mediaType, size, err := s.serve(r) - if err != nil { - log.L.WithError(err).Warnf("failed to serve %q %q", r.Method, r.URL.Path) - // TODO: support response body following OCI Distribution Spec's error response format spec: - // https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#error-codes - http.Error(w, "", http.StatusNotFound) - return - } - if content == nil { - log.L.Debugf("returning without contents") - w.WriteHeader(200) - return - } - w.Header().Set("Content-Type", mediaType) - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - if r.Method == "GET" { - http.ServeContent(w, r, "", time.Now(), content) - log.L.WithField("CID", cid).Debugf("served file") - } -} - -func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, error) { - if r.Method != "GET" && r.Method != "HEAD" { - return "", nil, "", 0, fmt.Errorf("unsupported method") - } - - if r.URL.Path == "/v2/" { - log.L.Debugf("requested /v2/") - return "", nil, "", 0, nil - } - - if matches := manifestRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 { - cidStr, ref := matches[1], matches[2] - if _, dgstErr := digest.Parse(ref); dgstErr == nil { - resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), cidStr, ref) - if !images.IsManifestType(mediaType) && !images.IsIndexType(mediaType) { - return "", nil, "", 0, fmt.Errorf("cannot serve non-manifest from manifest API: %q", mediaType) - } - log.L.WithField("root CID", cidStr).WithField("digest", ref).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by digest") - return resolvedCID, content, mediaType, size, err - } - if ref != "latest" { - return "", nil, "", 0, fmt.Errorf("tag of %q must be latest but got %q", cidStr, ref) - } - resolvedCID, content, mediaType, size, err := s.serveContentByCID(r.Context(), cidStr) - if err != nil { - return "", nil, "", 0, err - } - log.L.WithField("root CID", cidStr).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by cid") - return resolvedCID, content, mediaType, size, nil - } - - if matches := blobsRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 { - rootCIDStr, dgstStr := matches[1], matches[2] - resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), rootCIDStr, dgstStr) - if err != nil { - return "", nil, "", 0, err - } - log.L.WithField("root CID", rootCIDStr).WithField("digest", dgstStr).WithField("resolved CID", resolvedCID).Debugf("resolved blob by digest") - return resolvedCID, content, mediaType, size, nil - } - - return "", nil, "", 0, fmt.Errorf("unsupported path") -} - -func (s *server) serveContentByCID(ctx context.Context, targetCID string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) { - // TODO: make sure cidStr is a vaild CID? - c, desc, err := s.resolveCIDOfRootBlob(ctx, targetCID) - if err != nil { - return "", nil, "", 0, err - } - rc, err := s.getReadSeeker(ctx, c) - if err != nil { - return "", nil, "", 0, err - } - return c, rc, getMediaType(desc), desc.Size, nil -} - -func (s *server) serveContentByDigest(ctx context.Context, rootCID, digestStr string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) { - dgst, err := digest.Parse(digestStr) - if err != nil { - return "", nil, "", 0, err - } - _, rootDesc, err := s.resolveCIDOfRootBlob(ctx, rootCID) - if err != nil { - return "", nil, "", 0, err - } - targetCID, targetDesc, err := s.resolveCIDOfDigest(ctx, dgst, rootDesc) - if err != nil { - return "", nil, "", 0, err - } - rc, err := s.getReadSeeker(ctx, targetCID) - if err != nil { - return "", nil, "", 0, err - } - return targetCID, rc, getMediaType(targetDesc), targetDesc.Size, nil -} - -func (s *server) getReadSeeker(ctx context.Context, c string) (io.ReadSeeker, error) { - sr, err := s.getFile(ctx, c) - if err != nil { - return nil, err - } - return newBufReadSeeker(sr), nil -} - -func (s *server) getFile(ctx context.Context, c string) (*io.SectionReader, error) { - st, err := s.ipfsclient.StatCID(c) - if err != nil { - return nil, err - } - ra := &retryReaderAt{ - ctx: ctx, - readAtFunc: func(ctx context.Context, p []byte, off int64) (int, error) { - ofst, size := int(off), len(p) - r, err := s.ipfsclient.Get("/ipfs/"+c, &ofst, &size) - if err != nil { - return 0, err - } - return io.ReadFull(r, p) - }, - timeout: s.config.ReadTimeout, - retry: s.config.ReadRetryNum, - } - return io.NewSectionReader(ra, 0, int64(st.Size)), nil -} - -func (s *server) resolveCIDOfRootBlob(ctx context.Context, c string) (string, ocispec.Descriptor, error) { - rc, err := s.getReadSeeker(ctx, c) - if err != nil { - return "", ocispec.Descriptor{}, err - } - var desc ocispec.Descriptor - if err := json.NewDecoder(rc).Decode(&desc); err != nil { - return "", ocispec.Descriptor{}, err - } - c, err = getIPFSCID(desc) - if err != nil { - return "", ocispec.Descriptor{}, err - } - return c, desc, nil -} - -func (s *server) resolveCIDOfDigest(ctx context.Context, dgst digest.Digest, desc ocispec.Descriptor) (string, ocispec.Descriptor, error) { - c, err := getIPFSCID(desc) - if err != nil { - return "", ocispec.Descriptor{}, err - } - if desc.Digest == dgst { - return c, desc, nil // hit - } - if !images.IsManifestType(desc.MediaType) && !images.IsIndexType(desc.MediaType) { - // This is not the target blob and have no child. Early return here and avoid querying this blob. - return "", ocispec.Descriptor{}, fmt.Errorf("blob doesn't match") - } - sr, err := s.getFile(ctx, c) - if err != nil { - return "", ocispec.Descriptor{}, err - } - descs, err := images.Children(ctx, &readerProvider{desc, sr}, desc) - if err != nil { - return "", ocispec.Descriptor{}, err - } - var errs []error - for _, desc := range descs { - gotCID, gotDesc, err := s.resolveCIDOfDigest(ctx, dgst, desc) - if err != nil { - errs = append(errs, err) - continue - } - return gotCID, gotDesc, nil - } - allErr := errors.Join(errs...) - if allErr == nil { - return "", ocispec.Descriptor{}, fmt.Errorf("not found") - } - return "", ocispec.Descriptor{}, allErr -} - -func getIPFSCID(desc ocispec.Descriptor) (string, error) { - for _, u := range desc.URLs { - if strings.HasPrefix(u, "ipfs://") { - // support only content addressable URL (ipfs://) - return u[7:], nil - } - } - return "", fmt.Errorf("no CID is recorded in %s", desc.Digest) -} - -func getMediaType(desc ocispec.Descriptor) string { - if images.IsManifestType(desc.MediaType) || images.IsIndexType(desc.MediaType) || images.IsConfigType(desc.MediaType) { - return desc.MediaType - } - return "application/octet-stream" -} - -type retryReaderAt struct { - ctx context.Context - readAtFunc func(ctx context.Context, p []byte, off int64) (int, error) - timeout time.Duration - retry int -} - -func (r *retryReaderAt) ReadAt(p []byte, off int64) (int, error) { - if r.retry < 0 { - r.retry = 0 - } - for i := 0; i <= r.retry; i++ { - ctx := r.ctx - if r.timeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.timeout) - defer cancel() - } - n, err := r.readAtFunc(ctx, p, off) - if err == nil { - return n, nil - } else if !errors.Is(err, context.DeadlineExceeded) { - return 0, err - } - // deadline exceeded. retry. - } - return 0, context.DeadlineExceeded -} - -func newBufReadSeeker(rs io.ReadSeeker) io.ReadSeeker { - rsc := &bufReadSeeker{ - rs: rs, - } - rsc.curR = bufio.NewReaderSize(rsc.rs, 512*1024) - return rsc -} - -type bufReadSeeker struct { - rs io.ReadSeeker - curR *bufio.Reader -} - -func (r *bufReadSeeker) Read(p []byte) (int, error) { - return r.curR.Read(p) -} - -func (r *bufReadSeeker) Seek(offset int64, whence int) (int64, error) { - n, err := r.rs.Seek(offset, whence) - if err != nil { - return 0, err - } - r.curR.Reset(r.rs) - return n, nil -} - -type readerProvider struct { - desc ocispec.Descriptor - r *io.SectionReader -} - -func (p *readerProvider) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) { - if desc.Digest != p.desc.Digest || desc.Size != p.desc.Size { - return nil, fmt.Errorf("unexpected content") - } - return &contentReaderAt{p.r}, nil -} - -type contentReaderAt struct { - *io.SectionReader -} - -func (r *contentReaderAt) Close() error { return nil } diff --git a/pkg/ipfs/registry_ipfs.go b/pkg/ipfs/registry_ipfs.go new file mode 100644 index 00000000000..915947310a2 --- /dev/null +++ b/pkg/ipfs/registry_ipfs.go @@ -0,0 +1,330 @@ +//go:build !no_ipfs + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/log" + ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client" +) + +func NewRegistry(options RegistryOptions) (http.Handler, error) { + // HTTP is only supported as of now. We can add https support here if needed (e.g. for connecting to it via proxy, etc) + iurl, err := ipfsclient.GetIPFSAPIAddress(lookupIPFSPath(options.IpfsPath), "http") + if err != nil { + return nil, err + } + return &server{options, ipfsclient.New(iurl)}, nil +} + +// server is a read-only registry which converts OCI Distribution Spec's pull-related API to IPFS +// https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#pull +type server struct { + config RegistryOptions + ipfsclient *ipfsclient.Client +} + +var manifestRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/manifests/(.*)`) +var blobsRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/blobs/(.*)`) + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cid, content, mediaType, size, err := s.serve(r) + if err != nil { + log.L.WithError(err).Warnf("failed to serve %q %q", r.Method, r.URL.Path) + // TODO: support response body following OCI Distribution Spec's error response format spec: + // https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#error-codes + http.Error(w, "", http.StatusNotFound) + return + } + if content == nil { + log.L.Debugf("returning without contents") + w.WriteHeader(200) + return + } + w.Header().Set("Content-Type", mediaType) + w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + if r.Method == "GET" { + http.ServeContent(w, r, "", time.Now(), content) + log.L.WithField("CID", cid).Debugf("served file") + } +} + +func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, error) { + if r.Method != "GET" && r.Method != "HEAD" { + return "", nil, "", 0, fmt.Errorf("unsupported method") + } + + if r.URL.Path == "/v2/" { + log.L.Debugf("requested /v2/") + return "", nil, "", 0, nil + } + + if matches := manifestRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 { + cidStr, ref := matches[1], matches[2] + if _, dgstErr := digest.Parse(ref); dgstErr == nil { + resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), cidStr, ref) + if !images.IsManifestType(mediaType) && !images.IsIndexType(mediaType) { + return "", nil, "", 0, fmt.Errorf("cannot serve non-manifest from manifest API: %q", mediaType) + } + log.L.WithField("root CID", cidStr).WithField("digest", ref).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by digest") + return resolvedCID, content, mediaType, size, err + } + if ref != "latest" { + return "", nil, "", 0, fmt.Errorf("tag of %q must be latest but got %q", cidStr, ref) + } + resolvedCID, content, mediaType, size, err := s.serveContentByCID(r.Context(), cidStr) + if err != nil { + return "", nil, "", 0, err + } + log.L.WithField("root CID", cidStr).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by cid") + return resolvedCID, content, mediaType, size, nil + } + + if matches := blobsRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 { + rootCIDStr, dgstStr := matches[1], matches[2] + resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), rootCIDStr, dgstStr) + if err != nil { + return "", nil, "", 0, err + } + log.L.WithField("root CID", rootCIDStr).WithField("digest", dgstStr).WithField("resolved CID", resolvedCID).Debugf("resolved blob by digest") + return resolvedCID, content, mediaType, size, nil + } + + return "", nil, "", 0, fmt.Errorf("unsupported path") +} + +func (s *server) serveContentByCID(ctx context.Context, targetCID string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) { + // TODO: make sure cidStr is a vaild CID? + c, desc, err := s.resolveCIDOfRootBlob(ctx, targetCID) + if err != nil { + return "", nil, "", 0, err + } + rc, err := s.getReadSeeker(ctx, c) + if err != nil { + return "", nil, "", 0, err + } + return c, rc, getMediaType(desc), desc.Size, nil +} + +func (s *server) serveContentByDigest(ctx context.Context, rootCID, digestStr string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) { + dgst, err := digest.Parse(digestStr) + if err != nil { + return "", nil, "", 0, err + } + _, rootDesc, err := s.resolveCIDOfRootBlob(ctx, rootCID) + if err != nil { + return "", nil, "", 0, err + } + targetCID, targetDesc, err := s.resolveCIDOfDigest(ctx, dgst, rootDesc) + if err != nil { + return "", nil, "", 0, err + } + rc, err := s.getReadSeeker(ctx, targetCID) + if err != nil { + return "", nil, "", 0, err + } + return targetCID, rc, getMediaType(targetDesc), targetDesc.Size, nil +} + +func (s *server) getReadSeeker(ctx context.Context, c string) (io.ReadSeeker, error) { + sr, err := s.getFile(ctx, c) + if err != nil { + return nil, err + } + return newBufReadSeeker(sr), nil +} + +func (s *server) getFile(ctx context.Context, c string) (*io.SectionReader, error) { + st, err := s.ipfsclient.StatCID(c) + if err != nil { + return nil, err + } + ra := &retryReaderAt{ + ctx: ctx, + readAtFunc: func(ctx context.Context, p []byte, off int64) (int, error) { + ofst, size := int(off), len(p) + r, err := s.ipfsclient.Get("/ipfs/"+c, &ofst, &size) + if err != nil { + return 0, err + } + return io.ReadFull(r, p) + }, + timeout: s.config.ReadTimeout, + retry: s.config.ReadRetryNum, + } + return io.NewSectionReader(ra, 0, int64(st.Size)), nil +} + +func (s *server) resolveCIDOfRootBlob(ctx context.Context, c string) (string, ocispec.Descriptor, error) { + rc, err := s.getReadSeeker(ctx, c) + if err != nil { + return "", ocispec.Descriptor{}, err + } + var desc ocispec.Descriptor + if err := json.NewDecoder(rc).Decode(&desc); err != nil { + return "", ocispec.Descriptor{}, err + } + c, err = getIPFSCID(desc) + if err != nil { + return "", ocispec.Descriptor{}, err + } + return c, desc, nil +} + +func (s *server) resolveCIDOfDigest(ctx context.Context, dgst digest.Digest, desc ocispec.Descriptor) (string, ocispec.Descriptor, error) { + c, err := getIPFSCID(desc) + if err != nil { + return "", ocispec.Descriptor{}, err + } + if desc.Digest == dgst { + return c, desc, nil // hit + } + if !images.IsManifestType(desc.MediaType) && !images.IsIndexType(desc.MediaType) { + // This is not the target blob and have no child. Early return here and avoid querying this blob. + return "", ocispec.Descriptor{}, fmt.Errorf("blob doesn't match") + } + sr, err := s.getFile(ctx, c) + if err != nil { + return "", ocispec.Descriptor{}, err + } + descs, err := images.Children(ctx, &readerProvider{desc, sr}, desc) + if err != nil { + return "", ocispec.Descriptor{}, err + } + var errs []error + for _, desc := range descs { + gotCID, gotDesc, err := s.resolveCIDOfDigest(ctx, dgst, desc) + if err != nil { + errs = append(errs, err) + continue + } + return gotCID, gotDesc, nil + } + allErr := errors.Join(errs...) + if allErr == nil { + return "", ocispec.Descriptor{}, fmt.Errorf("not found") + } + return "", ocispec.Descriptor{}, allErr +} + +func getIPFSCID(desc ocispec.Descriptor) (string, error) { + for _, u := range desc.URLs { + if strings.HasPrefix(u, "ipfs://") { + // support only content addressable URL (ipfs://) + return u[7:], nil + } + } + return "", fmt.Errorf("no CID is recorded in %s", desc.Digest) +} + +func getMediaType(desc ocispec.Descriptor) string { + if images.IsManifestType(desc.MediaType) || images.IsIndexType(desc.MediaType) || images.IsConfigType(desc.MediaType) { + return desc.MediaType + } + return "application/octet-stream" +} + +type retryReaderAt struct { + ctx context.Context + readAtFunc func(ctx context.Context, p []byte, off int64) (int, error) + timeout time.Duration + retry int +} + +func (r *retryReaderAt) ReadAt(p []byte, off int64) (int, error) { + if r.retry < 0 { + r.retry = 0 + } + for i := 0; i <= r.retry; i++ { + ctx := r.ctx + if r.timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, r.timeout) + defer cancel() + } + n, err := r.readAtFunc(ctx, p, off) + if err == nil { + return n, nil + } else if !errors.Is(err, context.DeadlineExceeded) { + return 0, err + } + // deadline exceeded. retry. + } + return 0, context.DeadlineExceeded +} + +func newBufReadSeeker(rs io.ReadSeeker) io.ReadSeeker { + rsc := &bufReadSeeker{ + rs: rs, + } + rsc.curR = bufio.NewReaderSize(rsc.rs, 512*1024) + return rsc +} + +type bufReadSeeker struct { + rs io.ReadSeeker + curR *bufio.Reader +} + +func (r *bufReadSeeker) Read(p []byte) (int, error) { + return r.curR.Read(p) +} + +func (r *bufReadSeeker) Seek(offset int64, whence int) (int64, error) { + n, err := r.rs.Seek(offset, whence) + if err != nil { + return 0, err + } + r.curR.Reset(r.rs) + return n, nil +} + +type readerProvider struct { + desc ocispec.Descriptor + r *io.SectionReader +} + +func (p *readerProvider) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) { + if desc.Digest != p.desc.Digest || desc.Size != p.desc.Size { + return nil, fmt.Errorf("unexpected content") + } + return &contentReaderAt{p.r}, nil +} + +type contentReaderAt struct { + *io.SectionReader +} + +func (r *contentReaderAt) Close() error { return nil } diff --git a/pkg/ipfs/registry_noipfs.go b/pkg/ipfs/registry_noipfs.go new file mode 100644 index 00000000000..f93c114b9d6 --- /dev/null +++ b/pkg/ipfs/registry_noipfs.go @@ -0,0 +1,27 @@ +//go:build no_ipfs + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "net/http" +) + +func NewRegistry(options RegistryOptions) (http.Handler, error) { + return nil, ErrNotImplemented +} diff --git a/pkg/referenceutil/cid_ipfs.go b/pkg/referenceutil/cid_ipfs.go new file mode 100644 index 00000000000..24b6974d7bf --- /dev/null +++ b/pkg/referenceutil/cid_ipfs.go @@ -0,0 +1,29 @@ +//go:build !no_ipfs + +/* + 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 referenceutil + +import "github.com/ipfs/go-cid" + +func decodeCid(v string) (string, error) { + c, err := cid.Decode(v) + if err != nil { + return "", err + } + return c.String(), nil +} diff --git a/pkg/referenceutil/cid_noipfs.go b/pkg/referenceutil/cid_noipfs.go new file mode 100644 index 00000000000..d7a8228925f --- /dev/null +++ b/pkg/referenceutil/cid_noipfs.go @@ -0,0 +1,29 @@ +//go:build no_ipfs + +/* + 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 referenceutil + +import ( + "fmt" + + "github.com/containerd/errdefs" +) + +func decodeCid(v string) (string, error) { + return "", fmt.Errorf("%w: ipfs is disabled by the distributor of this build", errdefs.ErrNotImplemented) +} diff --git a/pkg/referenceutil/referenceutil.go b/pkg/referenceutil/referenceutil.go index b46bb240d83..54e8f79e273 100644 --- a/pkg/referenceutil/referenceutil.go +++ b/pkg/referenceutil/referenceutil.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/distribution/reference" - "github.com/ipfs/go-cid" "github.com/opencontainers/go-digest" ) @@ -108,9 +107,9 @@ func Parse(rawRef string) (*ImageReference, error) { // before parsing the image reference specified in its OCI image manifest. return nil, ErrLoadOCIArchiveRequired } - if decodedCID, err := cid.Decode(rawRef); err == nil { + if decodedCID, err := decodeCid(rawRef); err == nil { ir.Protocol = IPFSProtocol - rawRef = decodedCID.String() + rawRef = decodedCID ir.Path = rawRef return ir, nil } From 42b1c2eb9de976e782705c3b41f853a607804a0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 01:19:12 +0000 Subject: [PATCH 116/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.6.5 to 2.7.1. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.6.5...v2.7.1) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.7.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 968288fe37b..c4da4b00894 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.6.5 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.7.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index b0262d3a5e0..db94e94cc4c 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.6.5 h1:H7xP5OMKdkN2p0brx01slxIU6dE/q6ybbG+jozPtIqk= -github.com/compose-spec/compose-go/v2 v2.6.5/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= +github.com/compose-spec/compose-go/v2 v2.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs= +github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From 671b7fe99a56b4922a9608036fa4f20500001b34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 02:14:28 +0000 Subject: [PATCH 117/868] build(deps): bump go.yaml.in/yaml/v3 from 3.0.3 to 3.0.4 Bumps [go.yaml.in/yaml/v3](https://github.com/yaml/go-yaml) from 3.0.3 to 3.0.4. - [Commits](https://github.com/yaml/go-yaml/compare/v3.0.3...v3.0.4) --- updated-dependencies: - dependency-name: go.yaml.in/yaml/v3 dependency-version: 3.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 968288fe37b..3a83d8798ba 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.5.2 - go.yaml.in/yaml/v3 v3.0.3 + go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.39.0 golang.org/x/net v0.41.0 golang.org/x/sync v0.15.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index b0262d3a5e0..13526858bdd 100644 --- a/go.sum +++ b/go.sum @@ -353,8 +353,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= From 2baeb05d7c2963e2cae613e59295ba062e01556f Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 30 Jun 2025 14:15:04 +0800 Subject: [PATCH 118/868] commit: support zstdchunked conversion with writable layer in container commit support zstdchunked conversion with writable layer in container commit Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_commit.go | 26 ++++++++++++++ docs/command-reference.md | 4 +++ pkg/api/types/container_types.go | 2 ++ pkg/cmd/container/commit.go | 17 ++++----- pkg/imgutil/commit/commit.go | 44 +++++++++++++++++++++++ 5 files changed, 85 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/container/container_commit.go b/cmd/nerdctl/container/container_commit.go index 4c22cff25aa..14db2be2a0b 100644 --- a/cmd/nerdctl/container/container_commit.go +++ b/cmd/nerdctl/container/container_commit.go @@ -48,6 +48,9 @@ func CommitCommand() *cobra.Command { cmd.Flags().Int("estargz-compression-level", 9, "eStargz compression level (1-9)") cmd.Flags().Int("estargz-chunk-size", 0, "eStargz chunk size") cmd.Flags().Int("estargz-min-chunk-size", 0, "The minimal number of bytes of data must be written in one gzip stream") + cmd.Flags().Bool("zstdchunked", false, "Convert the committed layer to zstd:chunked for lazy pulling") + cmd.Flags().Int("zstdchunked-compression-level", 3, "zstd:chunked compression level") + cmd.Flags().Int("zstdchunked-chunk-size", 0, "zstd:chunked chunk size") return cmd } @@ -107,6 +110,24 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { return types.ContainerCommitOptions{}, err } + zstdchunked, err := cmd.Flags().GetBool("zstdchunked") + if err != nil { + return types.ContainerCommitOptions{}, err + } + zstdchunkedCompressionLevel, err := cmd.Flags().GetInt("zstdchunked-compression-level") + if err != nil { + return types.ContainerCommitOptions{}, err + } + zstdchunkedChunkSize, err := cmd.Flags().GetInt("zstdchunked-chunk-size") + if err != nil { + return types.ContainerCommitOptions{}, err + } + + // estargz and zstdchunked are mutually exclusive + if estargz && zstdchunked { + return types.ContainerCommitOptions{}, errors.New("options --estargz and --zstdchunked lead to conflict, only one of them can be used") + } + return types.ContainerCommitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, @@ -122,6 +143,11 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { EstargzChunkSize: estargzChunkSize, EstargzMinChunkSize: estargzMinChunkSize, }, + ZstdChunkedOptions: types.ZstdChunkedOptions{ + ZstdChunked: zstdchunked, + ZstdChunkedCompressionLevel: zstdchunkedCompressionLevel, + ZstdChunkedChunkSize: zstdchunkedChunkSize, + }, }, nil } diff --git a/docs/command-reference.md b/docs/command-reference.md index 5e17ad03a6d..09bbef2fb89 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -782,6 +782,10 @@ Flags: - :nerd_face: `--estargz-compression-level`: eStargz compression level (1-9) (default: 9) - :nerd_face: `--estargz-chunk-size`: eStargz chunk size - :nerd_face: `--estargz-min-chunk-size`: The minimal number of bytes of data must be written in one gzip stream +- :nerd_face: `--zstdchunked`: Convert the committed layer to zstd:chunked for lazy pulling +support zstdchunked convert +- :nerd_face: `--zstdchunked-compression-level`: zstd:chunked compression level (default: 3) +- :nerd_face: `--zstdchunked-chunk-size`: zstd:chunked chunk size ## Image management diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index b90034abd01..4583e44d733 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -398,6 +398,8 @@ type ContainerCommitOptions struct { Format ImageFormat // Embed EstargzOptions for eStargz conversion options EstargzOptions + // Embed ZstdChunkedOptions for zstd:chunked conversion options + ZstdChunkedOptions } type CompressionType string diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go index fdafe3ae776..4b447004247 100644 --- a/pkg/cmd/container/commit.go +++ b/pkg/cmd/container/commit.go @@ -44,14 +44,15 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s } opts := &commit.Opts{ - Author: options.Author, - Message: options.Message, - Ref: parsedReference.String(), - Pause: options.Pause, - Changes: changes, - Compression: options.Compression, - Format: options.Format, - EstargzOptions: options.EstargzOptions, + Author: options.Author, + Message: options.Message, + Ref: parsedReference.String(), + Pause: options.Pause, + Changes: changes, + Compression: options.Compression, + Format: options.Format, + EstargzOptions: options.EstargzOptions, + ZstdChunkedOptions: options.ZstdChunkedOptions, } walker := &containerwalker.ContainerWalker{ diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index 1f3292178e8..f283a4dd290 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -27,6 +27,7 @@ import ( "strings" "time" + "github.com/klauspost/compress/zstd" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go" @@ -45,6 +46,7 @@ import ( "github.com/containerd/platforms" "github.com/containerd/stargz-snapshotter/estargz" estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" + zstdchunkedconvert "github.com/containerd/stargz-snapshotter/nativeconverter/zstdchunked" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -67,6 +69,7 @@ type Opts struct { Compression types.CompressionType Format types.ImageFormat types.EstargzOptions + types.ZstdChunkedOptions } var ( @@ -181,6 +184,9 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd // Sync filesystem to make sure that all the data writes in container could be persisted to disk. Sync() + if opts.ZstdChunked { + opts.Compression = types.Zstd + } diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ, opts.Compression, opts) if err != nil { return emptyDigest, fmt.Errorf("failed to export layer: %w", err) @@ -478,6 +484,44 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c } } + // Convert to zstd:chunked if requested + if opts.ZstdChunked { + log.G(ctx).Infof("Converting diff layer to zstd:chunked format") + + esgzOpts := []estargz.Option{ + estargz.WithChunkSize(opts.ZstdChunkedChunkSize), + } + + convertFunc := zstdchunkedconvert.LayerConvertFuncWithCompressionLevel(zstd.EncoderLevelFromZstd(opts.ZstdChunkedCompressionLevel), esgzOpts...) + + zstdchunkedDesc, err := convertFunc(ctx, cs, newDesc) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("failed to convert diff layer to zstd:chunked: %w", err) + } else if zstdchunkedDesc != nil { + zstdchunkedDesc.MediaType = mediaType + zstdchunkedInfo, err := cs.Info(ctx, zstdchunkedDesc.Digest) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), err + } + + zstdchunkedDiffIDStr, ok := zstdchunkedInfo.Labels["containerd.io/uncompressed"] + if !ok { + return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("invalid differ response with no diffID") + } + + zstdchunkedDiffID, err := digest.Parse(zstdchunkedDiffIDStr) + if err != nil { + return ocispec.Descriptor{}, digest.Digest(""), err + } + return ocispec.Descriptor{ + MediaType: zstdchunkedDesc.MediaType, + Digest: zstdchunkedDesc.Digest, + Size: zstdchunkedDesc.Size, + Annotations: zstdchunkedDesc.Annotations, + }, zstdchunkedDiffID, nil + } + } + return ocispec.Descriptor{ MediaType: mediaType, Digest: newDesc.Digest, From a59d0ac61aecae0fc7a4d7e2d6d5592b6922e84d Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Tue, 1 Jul 2025 17:41:54 +0000 Subject: [PATCH 119/868] exec: wait for I/O completion before return Signed-off-by: Swagat Bora --- pkg/cmd/container/exec.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/container/exec.go b/pkg/cmd/container/exec.go index 0c087e63782..c874a4d1087 100644 --- a/pkg/cmd/container/exec.go +++ b/pkg/cmd/container/exec.go @@ -134,6 +134,10 @@ func execActionWithContainer(ctx context.Context, client *containerd.Client, con return nil } status := <-statusC + + process.IO().Wait() + process.IO().Close() + code, _, err := status.Result() if err != nil { return err From 71a1d7bcaa14b74d3dc7ed7c30b0e431aa6e452e Mon Sep 17 00:00:00 2001 From: Mahapatra Date: Tue, 1 Jul 2025 11:32:32 -0700 Subject: [PATCH 120/868] fix: allow soci v1 pulls for existing tests Signed-off-by: Shubhranshu Mahapatra --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4acda707b80..a7d66145915 100644 --- a/Dockerfile +++ b/Dockerfile @@ -329,7 +329,10 @@ COPY --from=ghcr.io/sigstore/cosign/cosign:v2.2.3@sha256:8fc9cad121611e8479f65f7 ARG SOCI_SNAPSHOTTER_VERSION RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ - tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc + tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc && \ + mkdir -p /etc/soci-snapshotter-grpc && \ + touch /etc/soci-snapshotter-grpc/config.toml && \ + echo "\n[pull_modes]\n [pull_modes.soci_v1]\n enable = true" >> /etc/soci-snapshotter-grpc/config.toml # enable offline ipfs for integration test COPY --from=build-kubo /out/${TARGETARCH:-amd64}/* /usr/local/bin/ COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml From 824af527536b41a2ea6b249c924d53072e2fe696 Mon Sep 17 00:00:00 2001 From: Shubhranshu Mahapatra Date: Wed, 2 Jul 2025 00:33:18 -0700 Subject: [PATCH 121/868] update soci base version to latest Signed-off-by: Shubhranshu Mahapatra --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a7d66145915..4c85670bc93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=0d9599e513d70e5792bb9334869f82f6e8b53d4d ARG NYDUS_VERSION=v2.3.1 -ARG SOCI_SNAPSHOTTER_VERSION=0.9.0 +ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.34.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx From 445fb7b1403bb20a773775b9ab0d749ce6cf7bb7 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 23 May 2025 05:11:51 +0000 Subject: [PATCH 122/868] add soci convert feature Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/image/image_convert.go | 28 ++++++ cmd/nerdctl/image/image_convert_linux_test.go | 18 ++++ docs/command-reference.md | 5 ++ docs/soci.md | 15 ++++ pkg/api/types/image_types.go | 14 ++- pkg/cmd/image/convert.go | 19 +++- pkg/cmd/image/push.go | 2 +- pkg/snapshotterutil/sociutil.go | 88 ++++++++++++++----- pkg/testutil/nerdtest/requirements.go | 47 ++++++++++ 9 files changed, 210 insertions(+), 26 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index ff7caade58d..49496f09ee7 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -89,6 +89,12 @@ func convertCommand() *cobra.Command { cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd") // #endregion + // #region soci flags + cmd.Flags().Bool("soci", false, "Convert image to SOCI Index V2 format.") + cmd.Flags().Int64("soci-min-layer-size", -1, "The minimum size of layers that will be converted to SOCI Index V2 format") + cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans") + // #endregion + // #region generic flags cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers") cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types") @@ -213,6 +219,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { } // #endregion + // #region soci flags + soci, err := cmd.Flags().GetBool("soci") + if err != nil { + return types.ImageConvertOptions{}, err + } + sociMinLayerSize, err := cmd.Flags().GetInt64("soci-min-layer-size") + if err != nil { + return types.ImageConvertOptions{}, err + } + sociSpanSize, err := cmd.Flags().GetInt64("soci-span-size") + if err != nil { + return types.ImageConvertOptions{}, err + } + // #endregion + // #region generic flags uncompress, err := cmd.Flags().GetBool("uncompress") if err != nil { @@ -277,6 +298,13 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { OverlayFsType: overlaybdFsType, OverlaydbDBStr: overlaybdDbstr, }, + SociConvertOptions: types.SociConvertOptions{ + Soci: soci, + SociOptions: types.SociOptions{ + SpanSize: sociSpanSize, + MinLayerSize: sociMinLayerSize, + }, + }, Stdout: cmd.OutOrStdout(), }, nil } diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index e514300aede..90e7a556dc3 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -89,6 +89,24 @@ func TestImageConvert(t *testing.T) { }, Expected: test.Expects(0, nil, nil), }, + { + Description: "soci", + Require: require.All( + require.Not(nerdtest.Docker), + nerdtest.Soci, + nerdtest.SociVersion("0.10.0"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--soci", + "--soci-span-size", "2097152", + "--soci-min-layer-size", "20971520", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, }, } diff --git a/docs/command-reference.md b/docs/command-reference.md index 09bbef2fb89..3019be9d541 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -979,6 +979,11 @@ Flags: - `--oci` : convert Docker media types to OCI media types - `--platform=` : convert content for a specific platform - `--all-platforms` : convert content for all platforms (default: false) +- `--soci` : generate SOCI v2 Indices to oci images. +*[**Note**: content is converted for all platforms by default when using this flag, use the `--platorm` flag to limit this behavior]* +- `--soci-span-size` : Span size in bytes that soci index uses to segment layer data. Default is 4 MiB. +- `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. + ### :nerd_face: nerdctl image encrypt diff --git a/docs/soci.md b/docs/soci.md index 67fbe92f584..d2dc84645df 100644 --- a/docs/soci.md +++ b/docs/soci.md @@ -45,3 +45,18 @@ For images that already have SOCI indices, see https://gallery.ecr.aws/soci-work nerdctl push --snapshotter=soci --soci-span-size=2097152 --soci-min-layer-size=20971520 public.ecr.aws/my-registry/my-repo:latest ``` --soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details. + + +## Enable SOCI for `nerdctl image convert` + +| :zap: Requirement | nerdctl >= 2.2.0 | +| ----------------- | ---------------- | + +| :zap: Requirement | soci-snapshotter >= 0.10.0 | +| ----------------- | ---------------- | + +- Convert an image to generate SOCI Index artifacts v2. Running the `nerdctl image convert` with the `--soci` flag and a `srcImg` and `dstImg`, `nerdctl` will create the SOCI v2 indices and the new image will be present in the `dstImg` address. +```console +nerdctl image convert --soci --soci-span-size=2097152 --soci-min-layer-size=20971520 public.ecr.aws/my-registry/my-repo:latest public.ecr.aws/my-registry/my-repo:soci +``` +--soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details. \ No newline at end of file diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index ddc08facf68..5ff507ccc7c 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -19,7 +19,7 @@ package types import ( "io" - "github.com/opencontainers/image-spec/specs-go/v1" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ImageListOptions specifies options for `nerdctl image list`. @@ -73,6 +73,7 @@ type ImageConvertOptions struct { ZstdChunkedOptions NydusOptions OverlaybdOptions + SociConvertOptions } // EstargzOptions contains eStargz conversion options @@ -135,6 +136,15 @@ type OverlaybdOptions struct { OverlayFsType string // OverlaydbDBStr database config string for overlaybd OverlaydbDBStr string + // #endregion +} + +type SociConvertOptions struct { + // Soci convert image to SOCI format. + Soci bool + // SociOptions contains SOCI-specific options + SociOptions SociOptions + // #endregion } // ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`. @@ -211,7 +221,7 @@ type ImagePullOptions struct { // If nil, it will unpack automatically if only 1 platform is specified. Unpack *bool // Content for specific platforms. Empty if `--all-platforms` is true - OCISpecPlatform []v1.Platform + OCISpecPlatform []ocispec.Platform // Pull mode Mode string // Suppress verbose output diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index b7963ea2702..12a2040d598 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -47,6 +47,7 @@ import ( converterutil "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" ) func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRawRef string, options types.ImageConvertOptions) error { @@ -86,8 +87,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa zstdchunked := options.ZstdChunked overlaybd := options.Overlaybd nydus := options.Nydus + soci := options.Soci var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error) - if estargz || zstd || zstdchunked || overlaybd || nydus { + if estargz || zstd || zstdchunked || overlaybd || nydus || soci { convertCount := 0 if estargz { convertCount++ @@ -104,9 +106,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa if nydus { convertCount++ } + if soci { + convertCount++ + } if convertCount > 1 { - return errors.New("options --estargz, --zstdchunked, --overlaybd and --nydus lead to conflict, only one of them can be used") + return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus and --soci lead to conflict, only one of them can be used") } var convertFunc converter.ConvertFunc @@ -164,6 +169,16 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa )), ) convertType = "nydus" + case soci: + // Convert image to SOCI format + convertedRef, err := snapshotterutil.ConvertSociIndexV2(ctx, client, srcRef, targetRef, options.GOptions, options.Platforms, options.SociOptions) + if err != nil { + return fmt.Errorf("failed to convert image to SOCI format: %w", err) + } + res := converterutil.ConvertedImageInfo{ + Image: convertedRef, + } + return printConvertedImage(options.Stdout, options, res) } if convertType != "overlaybd" { diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go index 5c4b9d1272e..8731b0cfc94 100644 --- a/pkg/cmd/image/push.go +++ b/pkg/cmd/image/push.go @@ -210,7 +210,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options return err } if options.GOptions.Snapshotter == "soci" { - if err = snapshotterutil.CreateSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil { + if err = snapshotterutil.CreateSociIndexV1(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil { return err } if err = snapshotterutil.PushSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms); err != nil { diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index a2148de027c..54ef3b5bdd7 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -18,23 +18,26 @@ package snapshotterutil import ( "bufio" + "context" + "fmt" "os" "os/exec" "strconv" "strings" + "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" ) -// CreateSoci creates a SOCI index(`rawRef`) -func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string, sOpts types.SociOptions) error { +// setupSociCommand creates and sets up a SOCI command with common configuration +func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) { sociExecutable, err := exec.LookPath("soci") if err != nil { log.L.WithError(err).Error("soci executable not found in path $PATH") log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md") - return err + return nil, err } sociCmd := exec.Command(sociExecutable) @@ -47,7 +50,64 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo if gOpts.Namespace != "" { sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace) } - // #endregion + + return sociCmd, nil +} + +// ConvertSociIndexV2 converts an image to SOCI format and returns the converted image reference with digest +func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) { + sociCmd, err := setupSociCommand(gOpts) + if err != nil { + return "", err + } + + sociCmd.Args = append(sociCmd.Args, "convert") + + if len(platforms) > 0 { + // multiple values need to be passed as separate, repeating flags in soci as it uses urfave + // https://github.com/urfave/cli/blob/main/docs/v2/examples/flags.md#multiple-values-per-single-flag + for _, p := range platforms { + sociCmd.Args = append(sociCmd.Args, "--platform", p) + } + } + + if sOpts.SpanSize != -1 { + sociCmd.Args = append(sociCmd.Args, "--span-size", strconv.FormatInt(sOpts.SpanSize, 10)) + } + + if sOpts.MinLayerSize != -1 { + sociCmd.Args = append(sociCmd.Args, "--min-layer-size", strconv.FormatInt(sOpts.MinLayerSize, 10)) + } + + sociCmd.Args = append(sociCmd.Args, srcRef, destRef) + + log.L.Infof("Converting image from %s to %s using SOCI format", srcRef, destRef) + + err = processSociIO(sociCmd) + if err != nil { + return "", err + } + err = sociCmd.Wait() + if err != nil { + return "", err + } + + // Get the converted image's digest + img, err := client.GetImage(ctx, destRef) + if err != nil { + return "", fmt.Errorf("failed to get converted image: %w", err) + } + + // Return the full reference with digest + return fmt.Sprintf("%s@%s", destRef, img.Target().Digest), nil +} + +// CreateSociIndexV1 creates a SOCI index(`rawRef`) +func CreateSociIndexV1(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string, sOpts types.SociOptions) error { + sociCmd, err := setupSociCommand(gOpts) + if err != nil { + return err + } // Global flags have to be put before subcommand before soci upgrades to urfave v3. // https://github.com/urfave/cli/issues/1113 @@ -73,7 +133,7 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo // --timeout, --debug, --content-store sociCmd.Args = append(sociCmd.Args, rawRef) - log.L.Debugf("running %s %v", sociExecutable, sociCmd.Args) + log.L.Debugf("running soci %v", sociCmd.Args) err = processSociIO(sociCmd) if err != nil { @@ -88,25 +148,11 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string) error { log.L.Debugf("pushing SOCI index: %s", rawRef) - sociExecutable, err := exec.LookPath("soci") + sociCmd, err := setupSociCommand(gOpts) if err != nil { - log.L.WithError(err).Error("soci executable not found in path $PATH") - log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md") return err } - sociCmd := exec.Command(sociExecutable) - sociCmd.Env = os.Environ() - - // #region for global flags. - if gOpts.Address != "" { - sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address) - } - if gOpts.Namespace != "" { - sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace) - } - // #endregion - // Global flags have to be put before subcommand before soci upgrades to urfave v3. // https://github.com/urfave/cli/issues/1113 sociCmd.Args = append(sociCmd.Args, "push") @@ -131,7 +177,7 @@ func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, } sociCmd.Args = append(sociCmd.Args, rawRef) - log.L.Debugf("running %s %v", sociExecutable, sociCmd.Args) + log.L.Debugf("running soci %v", sociCmd.Args) err = processSociIO(sociCmd) if err != nil { diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 3cc9390996a..06a11aa3d4f 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -411,6 +411,53 @@ var RemapIDs = &test.Requirement{ }, } +// SociVersion returns a requirement that checks if the installed SOCI version +// meets the minimum required version +func SociVersion(minVersion string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + sociExecutable, err := exec.LookPath("soci") + if err != nil { + return false, fmt.Sprintf("soci executable not found in path $PATH: %v", err) + } + + cmd := exec.Command(sociExecutable, "--version") + output, err := cmd.Output() + if err != nil { + return false, fmt.Sprintf("failed to get soci version: %v", err) + } + + // Parse version from output + // Example output format: "soci version v0.9.0 737f61a3db40c386f997c1f126344158aa3ad43c" + versionStr := strings.TrimSpace(string(output)) + parts := strings.Fields(versionStr) + if len(parts) < 3 { + return false, fmt.Sprintf("unexpected soci version output format: %s", versionStr) + } + + // Extract version number without 'v' prefix + installedVersion := strings.TrimPrefix(parts[2], "v") + + // Compare versions + v1, err := semver.NewVersion(installedVersion) + if err != nil { + return false, fmt.Sprintf("failed to parse installed version %s: %v", installedVersion, err) + } + + v2, err := semver.NewVersion(minVersion) + if err != nil { + return false, fmt.Sprintf("failed to parse minimum required version %s: %v", minVersion, err) + } + + if v1.LessThan(v2) { + return false, fmt.Sprintf("installed soci version %s is older than required version %s", installedVersion, minVersion) + } + + return true, fmt.Sprintf("soci version %s meets minimum requirement %s", installedVersion, minVersion) + }, + } +} + func ContainerdVersion(v string) *test.Requirement { return &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { From b4bcc305ed27f489f9169831d3491934259f247c Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 1 Jul 2025 03:11:51 +0000 Subject: [PATCH 123/868] check soci version Signed-off-by: Arjun Raja Yogidas --- pkg/snapshotterutil/sociutil.go | 53 +++++++++++++++++++++++++++ pkg/testutil/nerdtest/requirements.go | 41 +++------------------ 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 54ef3b5bdd7..2321b54fdf8 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -22,15 +22,63 @@ import ( "fmt" "os" "os/exec" + "regexp" "strconv" "strings" + "github.com/Masterminds/semver/v3" "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" ) +// CheckSociVersion checks if the SOCI binary version is at least the required version +// This function can be used by both production code and tests +func CheckSociVersion(requiredVersion string) error { + sociExecutable, err := exec.LookPath("soci") + if err != nil { + log.L.WithError(err).Error("soci executable not found in path $PATH") + log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md") + return err + } + + cmd := exec.Command(sociExecutable, "--version") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to get SOCI version: %w", err) + } + + // Parse the version string + versionStr := string(output) + // Handle format like "soci version v0.10.0 8bbfe951bbb411798ee85dbd908544df4a1619a8.m" + re := regexp.MustCompile(`v?(\d+\.\d+\.\d+)`) + matches := re.FindStringSubmatch(versionStr) + if len(matches) < 2 { + return fmt.Errorf("failed to parse SOCI version from output: %s", versionStr) + } + + // Extract version number + installedVersion := matches[1] + + // Compare versions using semver + v1, err := semver.NewVersion(installedVersion) + if err != nil { + return fmt.Errorf("failed to parse installed version %s: %v", installedVersion, err) + } + + v2, err := semver.NewVersion(requiredVersion) + if err != nil { + return fmt.Errorf("failed to parse minimum required version %s: %v", requiredVersion, err) + } + + if v1.LessThan(v2) { + return fmt.Errorf("SOCI version %s is lower than the required version %s for the convert operation", installedVersion, requiredVersion) + } + + return nil +} + // setupSociCommand creates and sets up a SOCI command with common configuration func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) { sociExecutable, err := exec.LookPath("soci") @@ -56,6 +104,11 @@ func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) { // ConvertSociIndexV2 converts an image to SOCI format and returns the converted image reference with digest func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) { + // Check if SOCI version is at least 0.10.0 which is required for the convert operation + if err := CheckSociVersion("0.10.0"); err != nil { + return "", err + } + sociCmd, err := setupSociCommand(gOpts) if err != nil { return "", err diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 06a11aa3d4f..46bbeee675d 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -35,6 +35,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" ) @@ -416,44 +417,12 @@ var RemapIDs = &test.Requirement{ func SociVersion(minVersion string) *test.Requirement { return &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (bool, string) { - sociExecutable, err := exec.LookPath("soci") + // Use the common CheckSociVersion function from snapshotterutil + err := snapshotterutil.CheckSociVersion(minVersion) if err != nil { - return false, fmt.Sprintf("soci executable not found in path $PATH: %v", err) - } - - cmd := exec.Command(sociExecutable, "--version") - output, err := cmd.Output() - if err != nil { - return false, fmt.Sprintf("failed to get soci version: %v", err) - } - - // Parse version from output - // Example output format: "soci version v0.9.0 737f61a3db40c386f997c1f126344158aa3ad43c" - versionStr := strings.TrimSpace(string(output)) - parts := strings.Fields(versionStr) - if len(parts) < 3 { - return false, fmt.Sprintf("unexpected soci version output format: %s", versionStr) - } - - // Extract version number without 'v' prefix - installedVersion := strings.TrimPrefix(parts[2], "v") - - // Compare versions - v1, err := semver.NewVersion(installedVersion) - if err != nil { - return false, fmt.Sprintf("failed to parse installed version %s: %v", installedVersion, err) - } - - v2, err := semver.NewVersion(minVersion) - if err != nil { - return false, fmt.Sprintf("failed to parse minimum required version %s: %v", minVersion, err) - } - - if v1.LessThan(v2) { - return false, fmt.Sprintf("installed soci version %s is older than required version %s", installedVersion, minVersion) + return false, err.Error() } - - return true, fmt.Sprintf("soci version %s meets minimum requirement %s", installedVersion, minVersion) + return true, fmt.Sprintf("soci version meets minimum requirement %s", minVersion) }, } } From 1ac8bb470873b32d79d633aaee8bd8ab401cd57b Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Wed, 25 Jun 2025 20:34:58 +0000 Subject: [PATCH 124/868] Enable setting DNS options through global nerdctl config Signed-off-by: Swagat Bora --- cmd/nerdctl/container/container_create.go | 2 +- cmd/nerdctl/container/container_run.go | 2 +- .../container/container_run_network.go | 57 +++++++++---- .../container_run_network_linux_test.go | 84 +++++++++++++++++++ cmd/nerdctl/helpers/cobra.go | 7 ++ cmd/nerdctl/helpers/flagutil.go | 15 ++++ cmd/nerdctl/main.go | 3 + docs/config.md | 9 +- pkg/config/config.go | 11 ++- 9 files changed, 167 insertions(+), 23 deletions(-) diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index 83f377eb245..fc5cbdd97c8 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -539,7 +539,7 @@ func createAction(cmd *cobra.Command, args []string) error { } defer cancel() - netFlags, err := loadNetworkFlags(cmd) + netFlags, err := loadNetworkFlags(cmd, createOpt.GOptions) if err != nil { return fmt.Errorf("failed to load networking flags: %w", err) } diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 39f1004aeda..f498f2df6d5 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -376,7 +376,7 @@ func runAction(cmd *cobra.Command, args []string) error { return errors.New("flags -d and -a cannot be specified together") } - netFlags, err := loadNetworkFlags(cmd) + netFlags, err := loadNetworkFlags(cmd, createOpt.GOptions) if err != nil { return fmt.Errorf("failed to load networking flags: %w", err) } diff --git a/cmd/nerdctl/container/container_run_network.go b/cmd/nerdctl/container/container_run_network.go index 1efddf25434..d208a6caf82 100644 --- a/cmd/nerdctl/container/container_run_network.go +++ b/cmd/nerdctl/container/container_run_network.go @@ -28,7 +28,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/strutil" ) -func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) { +func loadNetworkFlags(cmd *cobra.Command, globalOpts types.GlobalCommandOptions) (types.NetworkOptions, error) { netOpts := types.NetworkOptions{} // --net/--network= ... @@ -101,33 +101,58 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) { netOpts.Domainname = domainname // --dns= ... - dnsSlice, err := cmd.Flags().GetStringSlice("dns") - if err != nil { - return netOpts, err + // Use command flags if set, otherwise use global config is set + var dnsSlice []string + if cmd.Flags().Changed("dns") { + var err error + dnsSlice, err = cmd.Flags().GetStringSlice("dns") + if err != nil { + return netOpts, err + } + } else { + dnsSlice = globalOpts.DNS } netOpts.DNSServers = strutil.DedupeStrSlice(dnsSlice) // --dns-search= ... - dnsSearchSlice, err := cmd.Flags().GetStringSlice("dns-search") - if err != nil { - return netOpts, err + // Use command flags if set, otherwise use global config is set + var dnsSearchSlice []string + if cmd.Flags().Changed("dns-search") { + var err error + dnsSearchSlice, err = cmd.Flags().GetStringSlice("dns-search") + if err != nil { + return netOpts, err + } + } else { + dnsSearchSlice = globalOpts.DNSSearch } netOpts.DNSSearchDomains = strutil.DedupeStrSlice(dnsSearchSlice) // --dns-opt/--dns-option= ... + // Use command flags if set, otherwise use global config if set dnsOptions := []string{} - dnsOptFlags, err := cmd.Flags().GetStringSlice("dns-opt") - if err != nil { - return netOpts, err - } - dnsOptions = append(dnsOptions, dnsOptFlags...) + // Check if either dns-opt or dns-option flags were set + dnsOptChanged := cmd.Flags().Changed("dns-opt") + dnsOptionChanged := cmd.Flags().Changed("dns-option") - dnsOptionFlags, err := cmd.Flags().GetStringSlice("dns-option") - if err != nil { - return netOpts, err + if dnsOptChanged || dnsOptionChanged { + // Use command flags + dnsOptFlags, err := cmd.Flags().GetStringSlice("dns-opt") + if err != nil { + return netOpts, err + } + dnsOptions = append(dnsOptions, dnsOptFlags...) + + dnsOptionFlags, err := cmd.Flags().GetStringSlice("dns-option") + if err != nil { + return netOpts, err + } + dnsOptions = append(dnsOptions, dnsOptionFlags...) + } else { + // Use global config defaults + dnsOptions = append(dnsOptions, globalOpts.DNSOpts...) } - dnsOptions = append(dnsOptions, dnsOptionFlags...) netOpts.DNSResolvConfOptions = strutil.DedupeStrSlice(dnsOptions) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index d9de442eec3..02f01677cee 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -996,3 +996,87 @@ func TestHostNetworkDnsConfigs(t *testing.T) { } testCase.Run(t) } + +func TestDNSWithGlobalConfig(t *testing.T) { + var configContent test.ConfigValue = `debug = false +debug_full = false +dns = ["10.10.10.10", "20.20.20.20"] +dns_opts = ["ndots:2", "timeout:5"] +dns_search = ["example.com", "test.local"]` + + nerdtest.Setup() + + testCase := &test.Case{ + Config: test.WithConfig(nerdtest.NerdctlToml, configContent), + // NERDCTL_TOML not supported in Docker + Require: require.Not(nerdtest.Docker), + SubTests: []*test.Case{ + { + Description: "Global DNS settings are used when command line options are not provided", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) + helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) + cmd := helpers.Command("run", "--rm", testutil.CommonImage, "cat", "/etc/resolv.conf") + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("nameserver 10.10.10.10"), + expect.Contains("nameserver 20.20.20.20"), + expect.Contains("search example.com test.local"), + expect.Contains("options ndots:2 timeout:5"), + )), + }, + { + Description: "Command line DNS options override global config", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) + helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) + cmd := helpers.Command("run", "--rm", + "--dns", "9.9.9.9", + "--dns-search", "override.com", + "--dns-opt", "ndots:3", + testutil.CommonImage, "cat", "/etc/resolv.conf") + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("nameserver 9.9.9.9"), + expect.Contains("search override.com"), + expect.Contains("options ndots:3"), + )), + }, + { + Description: "Global DNS settings should also apply when using host network", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) + helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) + cmd := helpers.Command("run", "--rm", "--network", "host", + testutil.CommonImage, "cat", "/etc/resolv.conf") + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("nameserver 10.10.10.10"), + expect.Contains("nameserver 20.20.20.20"), + expect.Contains("search example.com test.local"), + expect.Contains("options ndots:2 timeout:5"), + )), + }, + { + Description: "Global DNS settings should also apply when using none network", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdctlTomlContent := string(helpers.Read(nerdtest.NerdctlToml)) + helpers.T().Log("NERDCTL_TOML file content:\n%s", nerdctlTomlContent) + cmd := helpers.Command("run", "--rm", "--network", "none", + testutil.CommonImage, "cat", "/etc/resolv.conf") + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("nameserver 10.10.10.10"), + expect.Contains("nameserver 20.20.20.20"), + expect.Contains("search example.com test.local"), + expect.Contains("options ndots:2 timeout:5"), + )), + }, + }, + } + testCase.Run(t) +} diff --git a/cmd/nerdctl/helpers/cobra.go b/cmd/nerdctl/helpers/cobra.go index d35030ea8cf..58eb9ac5f51 100644 --- a/cmd/nerdctl/helpers/cobra.go +++ b/cmd/nerdctl/helpers/cobra.go @@ -283,3 +283,10 @@ func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersiste } } } + +// HiddenPersistentStringArrayFlag creates a persistent string slice flag and hides it. +// Used mainly to pass global config values to individual commands. +func HiddenPersistentStringArrayFlag(cmd *cobra.Command, name string, value []string, usage string) { + cmd.PersistentFlags().StringSlice(name, value, usage) + cmd.PersistentFlags().MarkHidden(name) +} diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 7200ae459a3..e4c09e49cc7 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -145,6 +145,18 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) if err != nil { return types.GlobalCommandOptions{}, err } + dns, err := cmd.Flags().GetStringSlice("global-dns") + if err != nil { + return types.GlobalCommandOptions{}, err + } + dnsOpts, err := cmd.Flags().GetStringSlice("global-dns-opts") + if err != nil { + return types.GlobalCommandOptions{}, err + } + dnsSearch, err := cmd.Flags().GetStringSlice("global-dns-search") + if err != nil { + return types.GlobalCommandOptions{}, err + } // Point to dataRoot for filesystem-helpers implementing rollback / backups. err = pkg.InitFS(dataRoot) @@ -169,6 +181,9 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) BridgeIP: bridgeIP, KubeHideDupe: kubeHideDupe, CDISpecDirs: cdiSpecDirs, + DNS: dns, + DNSOpts: dnsOpts, + DNSSearch: dnsSearch, }, nil } diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index d04f54bd08a..55cc12c9bd6 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -188,6 +188,9 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io") rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi") rootCmd.PersistentFlags().String("userns-remap", cfg.UsernsRemap, "Support idmapping for creating and running containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively") + helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns", cfg.DNS, "Global DNS servers for containers") + helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns-opts", cfg.DNSOpts, "Global DNS options for containers") + helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns-search", cfg.DNSSearch, "Global DNS search domains for containers") return aliasToBeInherited, nil } diff --git a/docs/config.md b/docs/config.md index 9d9369e2ebe..4ed70965948 100644 --- a/docs/config.md +++ b/docs/config.md @@ -27,11 +27,14 @@ cgroup_manager = "cgroupfs" hosts_dir = ["/etc/containerd/certs.d", "/etc/docker/certs.d"] experimental = true userns_remap = "" +dns = ["8.8.8.8", "1.1.1.1"] +dns_opts = ["ndots:1", "timeout:2"] +dns_search = ["example.com", "example.org"] ``` ## Properties -| TOML property | CLI flag | Env var | Description | Availability \*1 | +| TOML property | CLI flag | Env var | Description | Availability | |---------------------|------------------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| | `debug` | `--debug` | | Debug mode | Since 0.16.0 | | `debug_full` | `--debug-full` | | Debug mode (with full output) | Since 0.16.0 | @@ -50,6 +53,9 @@ userns_remap = "" | `kube_hide_dupe` | `--kube-hide-dupe` | | Deduplicate images for Kubernetes with namespace k8s.io, no more redundant ones are displayed | Since 2.0.3 | | `cdi_spec_dirs` | `--cdi-spec-dirs` | | The folders to use when searching for CDI ([container-device-interface](https://github.com/cncf-tags/container-device-interface)) specifications. | Since 2.1.0 | | `userns_remap` | `--userns-remap` | | Support idmapping of containers. This options is only supported on rootful linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively. | Since 2.1.0 | +| `dns` | | | Set global DNS servers for containers | Since 2.1.3 | +| `dns_opts` | | | Set global DNS options for containers | Since 2.1.3 | +| `dns_search` | | | Set global DNS search domains for containers | Since 2.1.3 | The properties are parsed in the following precedence: 1. CLI flag @@ -57,7 +63,6 @@ The properties are parsed in the following precedence: 3. TOML property 4. Built-in default value (Run `nerdctl --help` to see the default values) -\*1: Availability of the TOML properties ## See also - [`registry.md`](registry.md) diff --git a/pkg/config/config.go b/pkg/config/config.go index ce118de5edf..a2eae17764a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -41,9 +41,11 @@ type Config struct { HostGatewayIP string `toml:"host_gateway_ip"` BridgeIP string `toml:"bridge_ip, omitempty"` KubeHideDupe bool `toml:"kube_hide_dupe"` - // CDISpecDirs is a list of directories in which CDI specifications can be found. - CDISpecDirs []string `toml:"cdi_spec_dirs,omitempty"` - UsernsRemap string `toml:"userns_remap, omitempty"` + CDISpecDirs []string `toml:"cdi_spec_dirs,omitempty"` // CDISpecDirs is a list of directories in which CDI specifications can be found. + UsernsRemap string `toml:"userns_remap, omitempty"` + DNS []string `toml:"dns,omitempty"` + DNSOpts []string `toml:"dns_opts,omitempty"` + DNSSearch []string `toml:"dns_search,omitempty"` } // New creates a default Config object statically, @@ -66,5 +68,8 @@ func New() *Config { KubeHideDupe: false, CDISpecDirs: ncdefaults.CDISpecDirs(), UsernsRemap: "", + DNS: []string{}, + DNSOpts: []string{}, + DNSSearch: []string{}, } } From 337f5a134e7b6822048f59778698f941eb5fbf72 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 1 Jul 2025 04:43:06 +0000 Subject: [PATCH 125/868] fix golangci-lint Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/image/image_convert_linux_test.go | 2 +- docs/command-reference.md | 4 +- docs/soci.md | 23 ++++++- pkg/snapshotterutil/sociutil.go | 65 ++++++++++--------- 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 90e7a556dc3..be24918fb31 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -102,7 +102,7 @@ func TestImageConvert(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--soci", "--soci-span-size", "2097152", - "--soci-min-layer-size", "20971520", + "--soci-min-layer-size", "0", testutil.CommonImage, data.Identifier("converted-image")) }, Expected: test.Expects(0, nil, nil), diff --git a/docs/command-reference.md b/docs/command-reference.md index 3019be9d541..d2e82e8a0ea 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -979,8 +979,8 @@ Flags: - `--oci` : convert Docker media types to OCI media types - `--platform=` : convert content for a specific platform - `--all-platforms` : convert content for all platforms (default: false) -- `--soci` : generate SOCI v2 Indices to oci images. -*[**Note**: content is converted for all platforms by default when using this flag, use the `--platorm` flag to limit this behavior]* +- `--soci` : convert content to SOCI image manifest v2 +*[**Note**: soci convert uses the default platform if nothing is specified. --platform flag can be used to specify a platform]* - `--soci-span-size` : Span size in bytes that soci index uses to segment layer data. Default is 4 MiB. - `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. diff --git a/docs/soci.md b/docs/soci.md index d2dc84645df..0e91bea2443 100644 --- a/docs/soci.md +++ b/docs/soci.md @@ -4,6 +4,22 @@ SOCI Snapshotter is a containerd snapshotter plugin. It enables standard OCI ima See https://github.com/awslabs/soci-snapshotter to learn further information. +## SOCI Index Manifest Versions + +SOCI supports two index manifest versions: + +- **v1**: Original format using OCI Referrers API (disabled by default in SOCI v0.10.0+) +- **v2**: New format that packages SOCI index with the image (default in SOCI v0.10.0+) + +To enable v1 indices in SOCI v0.10.0+, add to `/etc/soci-snapshotter-grpc/config.toml`: +```toml +[pull_modes] + [pull_modes.soci_v1] + enable = true +``` + +For detailed information about the differences between v1 and v2, see the [SOCI Index Manifest v2 documentation](https://github.com/awslabs/soci-snapshotter/blob/main/docs/soci-index-manifest-v2.md). + ## Prerequisites - Install containerd remote snapshotter plugin (`soci-snapshotter-grpc`) from https://github.com/awslabs/soci-snapshotter/blob/main/docs/getting-started.md @@ -46,10 +62,11 @@ nerdctl push --snapshotter=soci --soci-span-size=2097152 --soci-min-layer-size=2 ``` --soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details. +> **Note**: With SOCI v0.10.0+, When using `nerdctl push --snapshotter=soci`, it creates and pushes v1 indices. When pushing a converted image (created with `nerdctl image convert --soci`), it will push v2 indices. ## Enable SOCI for `nerdctl image convert` -| :zap: Requirement | nerdctl >= 2.2.0 | +| :zap: Requirement | nerdctl >= 2.1.3 | | ----------------- | ---------------- | | :zap: Requirement | soci-snapshotter >= 0.10.0 | @@ -59,4 +76,6 @@ nerdctl push --snapshotter=soci --soci-span-size=2097152 --soci-min-layer-size=2 ```console nerdctl image convert --soci --soci-span-size=2097152 --soci-min-layer-size=20971520 public.ecr.aws/my-registry/my-repo:latest public.ecr.aws/my-registry/my-repo:soci ``` ---soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details. \ No newline at end of file +--soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details. + +The `image convert` command with `--soci` flag creates SOCI-enabled images using SOCI Index Manifest v2, which combines the SOCI index and the original image into a single artifact. diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 2321b54fdf8..240ef54737e 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -27,14 +27,37 @@ import ( "strings" "github.com/Masterminds/semver/v3" + "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" ) +// setupSociCommand creates and sets up a SOCI command with common configuration +func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) { + sociExecutable, err := exec.LookPath("soci") + if err != nil { + log.L.WithError(err).Error("soci executable not found in path $PATH") + log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md") + return nil, err + } + + sociCmd := exec.Command(sociExecutable) + sociCmd.Env = os.Environ() + + // #region for global flags. + if gOpts.Address != "" { + sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address) + } + if gOpts.Namespace != "" { + sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace) + } + + return sociCmd, nil +} + // CheckSociVersion checks if the SOCI binary version is at least the required version -// This function can be used by both production code and tests func CheckSociVersion(requiredVersion string) error { sociExecutable, err := exec.LookPath("soci") if err != nil { @@ -59,49 +82,27 @@ func CheckSociVersion(requiredVersion string) error { } // Extract version number - installedVersion := matches[1] + installedVersionStr := matches[1] - // Compare versions using semver - v1, err := semver.NewVersion(installedVersion) + // Parse versions using semver package + installedVersion, err := semver.NewVersion(installedVersionStr) if err != nil { - return fmt.Errorf("failed to parse installed version %s: %v", installedVersion, err) + return fmt.Errorf("failed to parse installed SOCI version: %w", err) } - v2, err := semver.NewVersion(requiredVersion) + reqVersion, err := semver.NewVersion(requiredVersion) if err != nil { - return fmt.Errorf("failed to parse minimum required version %s: %v", requiredVersion, err) + return fmt.Errorf("failed to parse required SOCI version: %w", err) } - if v1.LessThan(v2) { - return fmt.Errorf("SOCI version %s is lower than the required version %s for the convert operation", installedVersion, requiredVersion) + // Compare versions + if installedVersion.LessThan(reqVersion) { + return fmt.Errorf("SOCI version %s is lower than the required version %s for the convert operation", installedVersion.String(), reqVersion.String()) } return nil } -// setupSociCommand creates and sets up a SOCI command with common configuration -func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) { - sociExecutable, err := exec.LookPath("soci") - if err != nil { - log.L.WithError(err).Error("soci executable not found in path $PATH") - log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md") - return nil, err - } - - sociCmd := exec.Command(sociExecutable) - sociCmd.Env = os.Environ() - - // #region for global flags. - if gOpts.Address != "" { - sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address) - } - if gOpts.Namespace != "" { - sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace) - } - - return sociCmd, nil -} - // ConvertSociIndexV2 converts an image to SOCI format and returns the converted image reference with digest func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) { // Check if SOCI version is at least 0.10.0 which is required for the convert operation From 0b068a559df812bf18521b1f73c63967288743d7 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 3 Jul 2025 10:04:45 +0800 Subject: [PATCH 126/868] cmd/image: update save command usage string Show proper syntax with flags and image arguments in help text. Fixes: #4397 Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/image/image_save.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 4c9f9ef9191..79c0c9cfd0a 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -32,7 +32,7 @@ import ( func SaveCommand() *cobra.Command { var cmd = &cobra.Command{ - Use: "save", + Use: "save [flags] IMAGE [IMAGE...]", Args: cobra.MinimumNArgs(1), Short: "Save one or more images to a tar archive (streamed to STDOUT by default)", Long: "The archive implements both Docker Image Spec v1.2 and OCI Image Spec v1.0.", From 2eb82b6c6104b68240b3ee373f2cb86d770fc197 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 3 Jul 2025 15:23:27 +0800 Subject: [PATCH 127/868] rename flag variables to descriptive boolean names Improves code readability by replacing short flag names (flagA, flagI, flagT, flagD) with descriptive boolean names (isAttach, isInteractive, isTerminal, isDetach). Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_exec.go | 20 ++++++++++---------- pkg/containerutil/containerutil.go | 16 ++++++++-------- pkg/taskutil/taskutil.go | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cmd/nerdctl/container/container_exec.go b/cmd/nerdctl/container/container_exec.go index e9684a5435e..b39d04eacdb 100644 --- a/cmd/nerdctl/container/container_exec.go +++ b/cmd/nerdctl/container/container_exec.go @@ -62,27 +62,27 @@ func execOptions(cmd *cobra.Command) (types.ContainerExecOptions, error) { return types.ContainerExecOptions{}, err } - flagI, err := cmd.Flags().GetBool("interactive") + isInteractive, err := cmd.Flags().GetBool("interactive") if err != nil { return types.ContainerExecOptions{}, err } - flagT, err := cmd.Flags().GetBool("tty") + isTerminal, err := cmd.Flags().GetBool("tty") if err != nil { return types.ContainerExecOptions{}, err } - flagD, err := cmd.Flags().GetBool("detach") + isDetach, err := cmd.Flags().GetBool("detach") if err != nil { return types.ContainerExecOptions{}, err } - if flagI { - if flagD { + if isInteractive { + if isDetach { return types.ContainerExecOptions{}, errors.New("currently flag -i and -d cannot be specified together (FIXME)") } } - if flagT { - if flagD { + if isTerminal { + if isDetach { return types.ContainerExecOptions{}, errors.New("currently flag -t and -d cannot be specified together (FIXME)") } } @@ -111,9 +111,9 @@ func execOptions(cmd *cobra.Command) (types.ContainerExecOptions, error) { return types.ContainerExecOptions{ GOptions: globalOptions, - TTY: flagT, - Interactive: flagI, - Detach: flagD, + TTY: isTerminal, + Interactive: isInteractive, + Detach: isDetach, Workdir: workdir, Env: env, EnvFile: envFile, diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 1559e203196..64352f75e7b 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -204,7 +204,7 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) } // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. -func Start(ctx context.Context, container containerd.Container, flagA bool, flagI bool, client *containerd.Client, detachKeys string) (err error) { +func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string) (err error) { // defer the storage of start error in the dedicated label defer func() { if err != nil { @@ -232,9 +232,9 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, flag if err != nil { return err } - flagT := process.Process.Terminal + isTerminal := process.Process.Terminal var con console.Console - if (flagI || flagA) && flagT { + if (isInteractive || isAttach) && isTerminal { con, err = consoleutil.Current() if err != nil { return err @@ -270,12 +270,12 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, flag } detachC := make(chan struct{}) attachStreamOpt := []string{} - if flagA { - // In start, flagA attaches only STDOUT/STDERR + if isAttach { + // In start, isAttach attaches only STDOUT/STDERR // source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start attachStreamOpt = []string{"STDOUT", "STDERR"} } - task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, flagI, flagT, true, con, logURI, detachKeys, namespace, detachC) + task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC) if err != nil { return err } @@ -283,10 +283,10 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, flag if err := task.Start(ctx); err != nil { return err } - if !flagA { + if !isAttach { return nil } - if flagA && flagT { + if isAttach && isTerminal { if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil { log.G(ctx).WithError(err).Error("console resize") } diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index 6e978aa6a5a..67962ac9065 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -43,7 +43,7 @@ import ( // NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108 func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, - attachStreamOpt []string, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) { + attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) { var t containerd.Task closer := func() { @@ -67,9 +67,9 @@ func NewTask(ctx context.Context, client *containerd.Client, container container if len(attachStreamOpt) != 0 { log.G(ctx).Debug("attaching output instead of using the log-uri") // when attaching a TTY we use writee for stdio and binary for log persistence - if flagT { + if isTerminal { var in io.Reader - if flagI { + if isInteractive { // FIXME: check IsTerminal on Windows too if runtime.GOOS != "windows" && !term.IsTerminal(0) { return nil, errors.New("the input device is not a TTY") @@ -86,7 +86,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container ioCreator = cioutil.NewContainerIO(namespace, logURI, false, streams.stdIn, streams.stdOut, streams.stdErr) } - } else if flagT && flagD { + } else if isTerminal && isDetach { u, err := url.Parse(logURI) if err != nil { return nil, err @@ -113,12 +113,12 @@ func NewTask(ctx context.Context, client *containerd.Client, container container ioCreator = cio.TerminalBinaryIO(parsedPath, map[string]string{ args[0]: args[1], }) - } else if flagT && !flagD { + } else if isTerminal && !isDetach { if con == nil { - return nil, errors.New("got nil con with flagT=true") + return nil, errors.New("got nil con with isTerminal=true") } var in io.Reader - if flagI { + if isInteractive { // FIXME: check IsTerminal on Windows too if runtime.GOOS != "windows" && !term.IsTerminal(0) { return nil, errors.New("the input device is not a TTY") @@ -130,7 +130,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } } ioCreator = cioutil.NewContainerIO(namespace, logURI, true, in, os.Stdout, os.Stderr) - } else if flagD && logURI != "" && logURI != "none" { + } else if isDetach && logURI != "" && logURI != "none" { u, err := url.Parse(logURI) if err != nil { return nil, err @@ -138,7 +138,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container ioCreator = cio.LogURI(u) } else { var in io.Reader - if flagI { + if isInteractive { if sv, err := infoutil.ServerSemVer(ctx, client); err != nil { log.G(ctx).Warn(err) } else if sv.LessThan(semver.MustParse("1.6.0-0")) { From a7b01be2727808ad65822150dd070e07cd1cc9da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:36:39 +0000 Subject: [PATCH 128/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.3.0+incompatible to 28.3.1+incompatible - [Commits](https://github.com/docker/cli/compare/v28.3.0...v28.3.1) Updates `github.com/docker/docker` from 28.3.0+incompatible to 28.3.1+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.0...v28.3.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.3.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.3.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index fb857204047..e67e3b4b702 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.3.0+incompatible //gomodjail:unconfined - github.com/docker/docker v28.3.0+incompatible //gomodjail:unconfined + github.com/docker/cli v28.3.1+incompatible //gomodjail:unconfined + github.com/docker/docker v28.3.1+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 9b8880be01e..459a7c147b5 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.3.0+incompatible h1:s+ttruVLhB5ayeuf2BciwDVxYdKi+RoUlxmwNHV3Vfo= -github.com/docker/cli v28.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ= -github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.3.1+incompatible h1:ZUdwOLDEBoE3TE5rdC9IXGY5HPHksJK3M+hJEWhh2mc= +github.com/docker/cli v28.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY= +github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= From d9d2f0b1b4dc2476423e7251b0cacfbba6566fa9 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Fri, 4 Jul 2025 01:37:51 +0530 Subject: [PATCH 129/868] feat: Add completions for 'nerdctl network create -o' Signed-off-by: Kanishk Pachauri fix: remove unimplemented networks Signed-off-by: Kanishk Pachauri fix: remove unimplemented networks Signed-off-by: Kanishk Pachauri --- cmd/nerdctl/completion/completion_unix.go | 43 ++++++++++++++++++++ cmd/nerdctl/completion/completion_windows.go | 22 ++++++++++ cmd/nerdctl/network/network_create.go | 1 + 3 files changed, 66 insertions(+) diff --git a/cmd/nerdctl/completion/completion_unix.go b/cmd/nerdctl/completion/completion_unix.go index af0b8698ce2..64438047fa2 100644 --- a/cmd/nerdctl/completion/completion_unix.go +++ b/cmd/nerdctl/completion/completion_unix.go @@ -38,6 +38,49 @@ func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string return []string{"default", "host-local", "dhcp"}, cobra.ShellCompDirectiveNoFileComp } +func NetworkOptions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + driver, _ := cmd.Flags().GetString("driver") + if driver == "" { + driver = "bridge" + } + + var candidates []string + switch driver { + case "bridge": + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + "ip-masq=", + "com.docker.network.bridge.enable_ip_masquerade=", + } + case "macvlan": + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + "mode=bridge", + "macvlan_mode=bridge", + "parent=", + } + case "ipvlan": + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + "mode=l2", + "mode=l3", + "ipvlan_mode=l2", + "ipvlan_mode=l3", + "parent=", + } + default: + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + "parent=", + } + } + return candidates, cobra.ShellCompDirectiveNoSpace +} + func NamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { diff --git a/cmd/nerdctl/completion/completion_windows.go b/cmd/nerdctl/completion/completion_windows.go index 020e0594926..b46d4c3fb5d 100644 --- a/cmd/nerdctl/completion/completion_windows.go +++ b/cmd/nerdctl/completion/completion_windows.go @@ -38,3 +38,25 @@ func NetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]str func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"default"}, cobra.ShellCompDirectiveNoFileComp } + +func NetworkOptions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + driver, _ := cmd.Flags().GetString("driver") + if driver == "" { + driver = "nat" + } + + var candidates []string + switch driver { + case "nat": + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + } + default: + candidates = []string{ + "mtu=", + "com.docker.network.driver.mtu=", + } + } + return candidates, cobra.ShellCompDirectiveNoSpace +} diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index f1ba2de2473..ca8b414654f 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -42,6 +42,7 @@ func createCommand() *cobra.Command { cmd.Flags().StringP("driver", "d", DefaultNetworkDriver, "Driver to manage the Network") cmd.RegisterFlagCompletionFunc("driver", completion.NetworkDrivers) cmd.Flags().StringArrayP("opt", "o", nil, "Set driver specific options") + cmd.RegisterFlagCompletionFunc("opt", completion.NetworkOptions) cmd.Flags().String("ipam-driver", "default", "IP Address helpers.Management Driver") cmd.RegisterFlagCompletionFunc("ipam-driver", completion.IPAMDrivers) cmd.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options") From f450c33b75281303d03b44ae006e213bf416edb7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:15:59 +0900 Subject: [PATCH 130/868] update containerd (2.1.3) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index b11d4578ed7..de5799d5786 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -141,9 +141,9 @@ jobs: go-version: 1.24 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.1.1 + containerd-version: 2.1.3 # Note: these as for amd64 - containerd-sha: 918e88fd393c28c89424e6535df0546ca36c1dfa7d8a5d685dee70b449380a9b + containerd-sha: 436cc160c33b37ec25b89fb5c72fc879ab2b3416df5d7af240c3e9c2f4065d3c containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 diff --git a/Dockerfile b/Dockerfile index 4c85670bc93..855f9e0948d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.1.1@cb1076646aa3740577fafbf3d914198b7fe8e3f7 +ARG CONTAINERD_VERSION=v2.1.3@c787fb98911740dd3ff2d0e45ce88cdf01410486 ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY From 27b707f1522f5fb6b14a9cde5c3939a971c90f5e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:17:52 +0900 Subject: [PATCH 131/868] update BuildKit (0.23.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 diff --git a/Dockerfile b/Dockerfile index 855f9e0948d..fbd4336b8d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.21.1@BINARY +ARG BUILDKIT_VERSION=v0.23.2@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 deleted file mode 100644 index 853b7c35172..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 +++ /dev/null @@ -1,2 +0,0 @@ -e0d83a631a48f13232fcee71cbd913e6b11dbde0a45985fa1b99af27ab97086e buildkit-v0.21.1.linux-amd64.tar.gz -7652a05f2961c386ea6e65c4701daa0e5a899a20c77596cd5f0eca02851dc1f6 buildkit-v0.21.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 new file mode 100644 index 00000000000..e74581e9551 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 @@ -0,0 +1,2 @@ +2771c3403e3a1f75a83cde387a05365794d3b900c355e864772a36c3ce541f82 buildkit-v0.23.2.linux-amd64.tar.gz +6385ff70b2fb4134b50ac3183eea3a0b06c6f6129173940d73178ae0477368f1 buildkit-v0.23.2.linux-arm64.tar.gz From 2894a94c1248a2ab7827fba200c77a85b4460399 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:19:03 +0900 Subject: [PATCH 132/868] update slirp4netns (1.3.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 | 7 ------- Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 | 7 +++++++ 3 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 diff --git a/Dockerfile b/Dockerfile index fbd4336b8d6..7e2e2fb80a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3@BINARY ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless ARG ROOTLESSKIT_VERSION=v2.3.5@BINARY -ARG SLIRP4NETNS_VERSION=v1.3.2@BINARY +ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 deleted file mode 100644 index db7c5ae07df..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 +++ /dev/null @@ -1,7 +0,0 @@ -b4162d27bbbd3683ca8ee57b51a1b270c0054b3a15fcc1830a5d7c10b77ad045 SOURCE_DATE_EPOCH -c55117faa5e18345a3ee1515267f056822ff0c1897999ae5422b0114ee48df85 slirp4netns-aarch64 -f55a6c9e3ec8280e9c3cec083f07dc124e2846ce8139a9281c35013e968d7e95 slirp4netns-armv7l -7b388a9cacbd89821f7f7a6457470fcae8f51aa846162521589feb4634ec7586 slirp4netns-ppc64le -041f9fe507510de1fbb802933a6add093ff19f941185965295c81f2ba4fc9cec slirp4netns-riscv64 -aa39cf14414ae53dbff6b79dfdfa55b5ff8ac5250e2261804863cd365b33a818 slirp4netns-s390x -4d55a3658ae259e3e74bb75cf058eb05d6e39ad6bbe170ca8e94c2462bea0eb1 slirp4netns-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 new file mode 100644 index 00000000000..a40e6aee074 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 @@ -0,0 +1,7 @@ +d0e6a13342efbedb8b7454629a0e9ce9b7a937c261034c85f46ed81af76307d8 SOURCE_DATE_EPOCH +1ca9d2f5f1fb4beb91f354653e5dad35b95c049afb264268d99a96ff2a10d903 slirp4netns-aarch64 +3e209d1c56fccbe627a038d311b233c15e8d914b30f9b981b5ed78b98e836859 slirp4netns-armv7l +4d1003a98103ee170c0fcd4aad8a5e0ba7aa2e70fbca883cbb6a39f40447c8da slirp4netns-ppc64le +06a13b398d88120097b20dace966d7dd5e2fbfd284b95a086347808df392200e slirp4netns-riscv64 +23d4a206edd6d3fc9c86f8b05c0881ff77a607b8d471f20964ad9f9c3f3176b1 slirp4netns-s390x +5618887b671a30a2f7548f2bdf7fba98a53981abc80cfd3183cd28b4dc8b2b97 slirp4netns-x86_64 From fb86ede0a2bf2abf6834ab2caafa843f0f43226f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:21:21 +0900 Subject: [PATCH 133/868] update gotestsum (1.12.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7e2e2fb80a2..bb521d1db92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GOMODJAIL_VERSION=v0.1.2@0a86b34442a491fa8f5e4565e9c846fce310239c ARG GO_VERSION=1.24 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 -ARG GOTESTSUM_VERSION=0d9599e513d70e5792bb9334869f82f6e8b53d4d +ARG GOTESTSUM_VERSION=v1.12.3 ARG NYDUS_VERSION=v2.3.1 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.34.1 From d3a009f1391de80fd31e30ed7518a18ccade8b18 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:21:39 +0900 Subject: [PATCH 134/868] update Nydus (2.3.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bb521d1db92..2dbd6d1b540 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG GO_VERSION=1.24 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 -ARG NYDUS_VERSION=v2.3.1 +ARG NYDUS_VERSION=v2.3.2 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.34.1 From 2da141b2832ca062955c3c255973b151bedb8031 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 7 Jul 2025 17:22:00 +0900 Subject: [PATCH 135/868] update Kubo (0.35.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2dbd6d1b540..5969b2d2c1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 ARG NYDUS_VERSION=v2.3.2 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 -ARG KUBO_VERSION=v0.34.1 +ARG KUBO_VERSION=v0.35.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx From 248d28a764f881df3aacd249bda5cdb56d3cafdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:08:24 +0000 Subject: [PATCH 136/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.3.1+incompatible to 28.3.2+incompatible - [Commits](https://github.com/docker/cli/compare/v28.3.1...v28.3.2) Updates `github.com/docker/docker` from 28.3.1+incompatible to 28.3.2+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.1...v28.3.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.3.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.3.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e67e3b4b702..e6abb3d5841 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.3.1+incompatible //gomodjail:unconfined - github.com/docker/docker v28.3.1+incompatible //gomodjail:unconfined + github.com/docker/cli v28.3.2+incompatible //gomodjail:unconfined + github.com/docker/docker v28.3.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 459a7c147b5..3cc40997b6d 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.3.1+incompatible h1:ZUdwOLDEBoE3TE5rdC9IXGY5HPHksJK3M+hJEWhh2mc= -github.com/docker/cli v28.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY= -github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= +github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= +github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= From 342eaca3ed8ce71f3c9a6a503a56b436719829b0 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Wed, 9 Jul 2025 14:43:58 +0800 Subject: [PATCH 137/868] fix:fifo leak Signed-off-by: ningmingxiao --- cmd/nerdctl/container/container_run_test.go | 45 +++++++++++++++++++++ pkg/cioutil/container_io.go | 4 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 0eaa72b0ac9..64692a3ead7 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -40,6 +40,7 @@ import ( "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" ) @@ -1058,3 +1059,47 @@ HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8 testCase.Run(t) } + +func countFIFOFiles(root string) (int, error) { + count := 0 + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Mode()&os.ModeNamedPipe != 0 { + count++ + } + return nil + }) + 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.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, + }) + oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + + cmd = helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") + cmd.WithPseudoTTY() + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + assert.Equal(t, oldNumFifos, newNumFifos) + } + testCase.Run(t) +} diff --git a/pkg/cioutil/container_io.go b/pkg/cioutil/container_io.go index c69bfda4888..22dd6b4a0a5 100644 --- a/pkg/cioutil/container_io.go +++ b/pkg/cioutil/container_io.go @@ -85,7 +85,9 @@ func (c *ncio) Close() error { select { case err := <-done: - return err + if err != nil { + lastErr = fmt.Errorf("faied to run cmd.wait: %w", err) + } case <-time.After(binaryIOProcTermTimeout): err := c.cmd.Process.Kill() From 6157151749faecbdb0f2b5f27a1fde228ebca26e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:04:45 +0000 Subject: [PATCH 138/868] build(deps): bump the golang-x group across 1 directory with 6 updates Bumps the golang-x group with 2 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/crypto` from 0.39.0 to 0.40.0 - [Commits](https://github.com/golang/crypto/compare/v0.39.0...v0.40.0) Updates `golang.org/x/net` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0) Updates `golang.org/x/sync` from 0.15.0 to 0.16.0 - [Commits](https://github.com/golang/sync/compare/v0.15.0...v0.16.0) Updates `golang.org/x/sys` from 0.33.0 to 0.34.0 - [Commits](https://github.com/golang/sys/compare/v0.33.0...v0.34.0) Updates `golang.org/x/term` from 0.32.0 to 0.33.0 - [Commits](https://github.com/golang/term/compare/v0.32.0...v0.33.0) Updates `golang.org/x/text` from 0.26.0 to 0.27.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.26.0...v0.27.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.27.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index e67e3b4b702..3856bdcf17b 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.5.2 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.39.0 - golang.org/x/net v0.41.0 - golang.org/x/sync v0.15.0 //gomodjail:unconfined - golang.org/x/sys v0.33.0 //gomodjail:unconfined - golang.org/x/term v0.32.0 //gomodjail:unconfined - golang.org/x/text v0.26.0 + golang.org/x/crypto v0.40.0 + golang.org/x/net v0.42.0 + golang.org/x/sync v0.16.0 //gomodjail:unconfined + golang.org/x/sys v0.34.0 //gomodjail:unconfined + golang.org/x/term v0.33.0 //gomodjail:unconfined + golang.org/x/text v0.27.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index 459a7c147b5..e146edb0d01 100644 --- a/go.sum +++ b/go.sum @@ -363,8 +363,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -396,8 +396,8 @@ 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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 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= @@ -410,8 +410,8 @@ 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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -436,8 +436,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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -447,8 +447,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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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= @@ -458,8 +458,8 @@ 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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 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= @@ -472,8 +472,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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= From e56e9aa84a52f92770fbd663f183c5b6c60edd38 Mon Sep 17 00:00:00 2001 From: haoyun Date: Fri, 11 Jul 2025 16:04:34 +0800 Subject: [PATCH 139/868] fix: call wait before start Signed-off-by: haoyun --- cmd/nerdctl/container/container_run.go | 10 ++++++---- pkg/containerutil/containerutil.go | 10 ++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index f498f2df6d5..67b797bdce9 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -435,6 +435,12 @@ func runAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + + statusC, err := task.Wait(ctx) + if err != nil { + return err + } + if err := task.Start(ctx); err != nil { return err } @@ -454,10 +460,6 @@ func runAction(cmd *cobra.Command, args []string) error { } } - statusC, err := task.Wait(ctx) - if err != nil { - return err - } select { // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit. // diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 64352f75e7b..60da6f11895 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -279,7 +279,10 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i if err != nil { return err } - + statusC, err := task.Wait(ctx) + if err != nil { + return err + } if err := task.Start(ctx); err != nil { return err } @@ -293,11 +296,6 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i } sigc := signalutil.ForwardAllSignals(ctx, task) defer signalutil.StopCatch(sigc) - - statusC, err := task.Wait(ctx) - if err != nil { - return err - } select { // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit. // From 88ed9529783499196d438d730331666bacf3055f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:46:19 +0000 Subject: [PATCH 140/868] build(deps): bump github.com/go-viper/mapstructure/v2 Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/go-viper/mapstructure/releases) - [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-viper/mapstructure/compare/v2.3.0...v2.4.0) --- updated-dependencies: - dependency-name: github.com/go-viper/mapstructure/v2 dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9c4a7d12414..27c8415b8d8 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/fatih/color v1.18.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.0 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined - github.com/go-viper/mapstructure/v2 v2.3.0 + github.com/go-viper/mapstructure/v2 v2.4.0 github.com/ipfs/go-cid v0.5.0 github.com/klauspost/compress v1.18.0 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 216e5f1a1e1..3b021ddddaa 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= From e45c6530f8be6af7a68ee33c7e4a830430b25101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:28:39 +0000 Subject: [PATCH 141/868] build(deps): bump github.com/spf13/pflag from 1.0.6 to 1.0.7 Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/spf13/pflag/releases) - [Commits](https://github.com/spf13/pflag/compare/v1.0.6...v1.0.7) --- updated-dependencies: - dependency-name: github.com/spf13/pflag dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 27c8415b8d8..061191e610c 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined github.com/spf13/cobra v1.9.1 //gomodjail:unconfined - github.com/spf13/pflag v1.0.6 //gomodjail:unconfined + github.com/spf13/pflag v1.0.7 //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 diff --git a/go.sum b/go.sum index 3b021ddddaa..857e8a74619 100644 --- a/go.sum +++ b/go.sum @@ -290,8 +290,9 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 7720234d11780a1387658b590da56cb8b98d3c42 Mon Sep 17 00:00:00 2001 From: Laitron Date: Wed, 23 Jul 2025 23:44:03 +0800 Subject: [PATCH 142/868] feat: validate IP address in --dns flag. Signed-off-by: Laitron --- .../container/container_run_network.go | 11 ++ pkg/dnsutil/dnsutil.go | 14 +++ pkg/dnsutil/dnsutil_test.go | 105 ++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 pkg/dnsutil/dnsutil_test.go diff --git a/cmd/nerdctl/container/container_run_network.go b/cmd/nerdctl/container/container_run_network.go index d208a6caf82..09f6a1b4b59 100644 --- a/cmd/nerdctl/container/container_run_network.go +++ b/cmd/nerdctl/container/container_run_network.go @@ -17,6 +17,8 @@ package container import ( + "errors" + "fmt" "net" "github.com/spf13/cobra" @@ -24,6 +26,7 @@ import ( "github.com/containerd/go-cni" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/dnsutil" "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/containerd/nerdctl/v2/pkg/strutil" ) @@ -109,6 +112,14 @@ func loadNetworkFlags(cmd *cobra.Command, globalOpts types.GlobalCommandOptions) if err != nil { return netOpts, err } + if len(dnsSlice) == 0 { + return netOpts, errors.New("--dns flag was specified but no DNS server was provided") + } + for _, dns := range dnsSlice { + if _, err := dnsutil.ValidateIPAddress(dns); err != nil { + return netOpts, fmt.Errorf("%w with --dns flag", err) + } + } } else { dnsSlice = globalOpts.DNS } diff --git a/pkg/dnsutil/dnsutil.go b/pkg/dnsutil/dnsutil.go index 433a19b324b..370ad23db24 100644 --- a/pkg/dnsutil/dnsutil.go +++ b/pkg/dnsutil/dnsutil.go @@ -18,6 +18,9 @@ package dnsutil import ( "context" + "fmt" + "net" + "strings" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) @@ -39,3 +42,14 @@ func GetSlirp4netnsDNS() ([]string, error) { } return dns, nil } + +// ValidateIPAddress validates if the given value is a correctly formatted +// IP address, and returns the value in normalized form. Leading and trailing +// whitespace is allowed, but it does not allow IPv6 addresses surrounded by +// square brackets ("[::1]"). Refer to [net.ParseIP] for accepted formats. +func ValidateIPAddress(val string) (string, error) { + if ip := net.ParseIP(strings.TrimSpace(val)); ip != nil { + return ip.String(), nil + } + return "", fmt.Errorf("ip address is not correctly formatted: %q", val) +} diff --git a/pkg/dnsutil/dnsutil_test.go b/pkg/dnsutil/dnsutil_test.go new file mode 100644 index 00000000000..17a8e8813e2 --- /dev/null +++ b/pkg/dnsutil/dnsutil_test.go @@ -0,0 +1,105 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dnsutil + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestValidateIPAddress(t *testing.T) { + tests := []struct { + name string + input string + expectedOut string + expectedErr string + }{ + { + name: "IPv4 loopback", + input: `127.0.0.1`, + expectedOut: `127.0.0.1`, + }, + { + name: "IPv4 loopback with whitespace", + input: ` 127.0.0.1 `, + expectedOut: `127.0.0.1`, + }, + { + name: "IPv6 loopback long form", + input: `0:0:0:0:0:0:0:1`, + expectedOut: `::1`, + }, + { + name: "IPv6 loopback", + input: `::1`, + expectedOut: `::1`, + }, + { + name: "IPv6 loopback with whitespace", + input: ` ::1 `, + expectedOut: `::1`, + }, + { + name: "IPv6 lowercase", + input: `2001:db8::68`, + expectedOut: `2001:db8::68`, + }, + { + name: "IPv6 uppercase", + input: `2001:DB8::68`, + expectedOut: `2001:db8::68`, + }, + { + name: "IPv6 with brackets", + input: `[::1]`, + expectedErr: `ip address is not correctly formatted: "[::1]"`, + }, + { + name: "IPv4 partial", + input: `127`, + expectedErr: `ip address is not correctly formatted: "127"`, + }, + { + name: "random invalid string", + input: `random invalid string`, + expectedErr: `ip address is not correctly formatted: "random invalid string"`, + }, + { + name: "empty string", + input: ``, + expectedErr: `ip address is not correctly formatted: ""`, + }, + { + name: "only whitespace", + input: ` `, + expectedErr: `ip address is not correctly formatted: " "`, + }, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + actualOut, actualErr := ValidateIPAddress(tc.input) + assert.Equal(t, tc.expectedOut, actualOut) + if tc.expectedErr == "" { + assert.Check(t, actualErr) + } else { + assert.Equal(t, tc.expectedErr, actualErr.Error()) + } + }) + } +} From 4cebcaf957e239d060e53d708f394bd7c8c9445b Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 23 Jul 2025 13:45:28 -0700 Subject: [PATCH 143/868] Pass github token to tasks querying the API Signed-off-by: apostasie --- .github/workflows/job-build.yml | 2 ++ .github/workflows/job-lint-go.yml | 2 ++ .github/workflows/job-test-in-host.yml | 2 ++ .github/workflows/job-test-unit.yml | 2 ++ .github/workflows/workflow-tigron.yml | 2 ++ 5 files changed, 10 insertions(+) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 169e95112ef..c1b2700ea31 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -41,6 +41,8 @@ jobs: - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | . ./hack/github/action-helpers.sh latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 8ca82c91506..b4eac0e4668 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -45,6 +45,8 @@ jobs: - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index f760c74780f..da6822bb5f3 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -77,6 +77,8 @@ jobs: - if: ${{ inputs.canary }} name: "Init (canary): retrieve latest go and containerd" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" latest_containerd="$(. ./hack/provisioning/version/fetch.sh; github::project::latest "containerd/containerd")" diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index c623b402b2b..c7af9804e75 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -53,6 +53,8 @@ jobs: # If canary is requested, check for the latest unstable release - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 4aa477a9790..e74b34a9082 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -37,6 +37,8 @@ jobs: fetch-depth: 100 - if: ${{ matrix.canary }} name: "Init (canary): retrieve GO_VERSION" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" From fe8ffa5d582569e1e29505efffde4031548c1458 Mon Sep 17 00:00:00 2001 From: Tushar Gupta Date: Thu, 5 Jun 2025 02:30:19 +0530 Subject: [PATCH 144/868] feat: add support for DockerfileInline As nerdctl currently uses "nerdctl build" to build the service images, write the inline file to a temporary file and use "-f" to specify the temporary dockerfile. Signed-off-by: Tushar Gupta --- .../compose/compose_build_linux_test.go | 37 ++++++++++++++++--- pkg/composer/build.go | 17 +++++++++ pkg/composer/serviceparser/build.go | 6 ++- pkg/composer/serviceparser/build_test.go | 20 ++++++++++ pkg/composer/serviceparser/serviceparser.go | 5 ++- 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index 79ef29a178b..cfa51e27400 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -39,6 +39,7 @@ func TestComposeBuild(t *testing.T) { // Make sure we shard the image name to something unique to the test to avoid conflicts with other tests imageSvc0 := data.Identifier("svc0") imageSvc1 := data.Identifier("svc1") + imageSvc2 := data.Identifier("svc2") // We are not going to run them, so, ports conflicts should not matter here dockerComposeYAML := fmt.Sprintf(` @@ -51,7 +52,13 @@ services: svc1: build: . image: %s -`, imageSvc0, imageSvc1) + svc2: + image: %s + build: + context: . + dockerfile_inline: | + FROM %s +`, imageSvc0, imageSvc1, imageSvc2, testutil.CommonImage) data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Temp().Save(dockerfile, "Dockerfile") @@ -59,6 +66,7 @@ services: data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) data.Labels().Set("imageSvc0", imageSvc0) data.Labels().Set("imageSvc1", imageSvc1) + data.Labels().Set("imageSvc2", imageSvc2) } testCase.SubTests = []*test.Case{ @@ -76,22 +84,41 @@ services: Output: expect.All( expect.Contains(data.Labels().Get("imageSvc0")), expect.DoesNotContain(data.Labels().Get("imageSvc1")), + expect.DoesNotContain(data.Labels().Get("imageSvc2")), + ), + } + }, + }, + { + Description: "build svc2", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc2") + }, + + Command: test.Command("images"), + + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("imageSvc2")), + expect.DoesNotContain(data.Labels().Get("imageSvc1")), ), } }, }, { - Description: "build svc0 and svc1", + Description: "build svc0, svc1, svc2", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1") + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1", "svc2") }, Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")), + Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")), } }, }, @@ -122,7 +149,7 @@ services: testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if data.Labels().Get("imageSvc0") != "" { - helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")) + helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1"), data.Labels().Get("imageSvc2")) } } diff --git a/pkg/composer/build.go b/pkg/composer/build.go index 17b3fd0d8cd..780c7d8c319 100644 --- a/pkg/composer/build.go +++ b/pkg/composer/build.go @@ -63,6 +63,23 @@ func (c *Composer) buildServiceImage(ctx context.Context, image string, b *servi if bo.Progress != "" { args = append(args, "--progress="+bo.Progress) } + + if b.DockerfileInline != "" { + // if DockerfileInline is specified, write it to a temporary file + // and use -f flag to use that docker file with project's ctxdir + tmpFile, err := os.CreateTemp("", "inline-dockerfile-*.Dockerfile") + if err != nil { + return fmt.Errorf("failed to create temp file for DockerfileInline: %w", err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + if _, err := tmpFile.Write([]byte(b.DockerfileInline)); err != nil { + return fmt.Errorf("failed to write DockerfileInline: %w", err) + } + b.BuildArgs = append(b.BuildArgs, "-f="+tmpFile.Name()) + } + args = append(args, b.BuildArgs...) cmd := c.createNerdctlCmd(ctx, append([]string{"build"}, args...)...) diff --git a/pkg/composer/serviceparser/build.go b/pkg/composer/serviceparser/build.go index d797d0690ba..98839a5c396 100644 --- a/pkg/composer/serviceparser/build.go +++ b/pkg/composer/serviceparser/build.go @@ -34,7 +34,7 @@ import ( func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName string) (*Build, error) { if unknown := reflectutil.UnknownNonEmptyFields(c, - "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", "AdditionalContexts", + "Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets", "DockerfileInline", "AdditionalContexts", ); len(unknown) > 0 { log.L.Warnf("Ignoring: build: %+v", unknown) } @@ -60,6 +60,10 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st } } + if c.DockerfileInline != "" { + b.DockerfileInline = c.DockerfileInline + } + for k, v := range c.Args { if v == nil { b.BuildArgs = append(b.BuildArgs, "--build-arg="+k) diff --git a/pkg/composer/serviceparser/build_test.go b/pkg/composer/serviceparser/build_test.go index 34af7143aec..e152296c242 100644 --- a/pkg/composer/serviceparser/build_test.go +++ b/pkg/composer/serviceparser/build_test.go @@ -18,6 +18,7 @@ package serviceparser import ( "runtime" + "strings" "testing" "gotest.tools/v3/assert" @@ -54,6 +55,12 @@ services: target: tgt_secret - simple_secret - absolute_secret + baz: + image: bazimg + build: + context: ./bazctx + dockerfile_inline: | + FROM random secrets: src_secret: file: test_secret1 @@ -95,4 +102,17 @@ secrets: assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=tgt_secret,src="+secretPath+"/test_secret1")) assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=simple_secret,src="+secretPath+"/test_secret2")) assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=absolute_secret,src=/tmp/absolute_secret")) + + bazSvc, err := project.GetService("baz") + assert.NilError(t, err) + + baz, err := Parse(project, bazSvc) + assert.NilError(t, err) + + t.Logf("baz: %+v", baz) + t.Logf("baz.Build.BuildArgs: %+v", baz.Build.BuildArgs) + t.Logf("baz.Build.DockerfileInline: %q", baz.Build.DockerfileInline) + assert.Assert(t, func() bool { + return strings.TrimSpace(baz.Build.DockerfileInline) == "FROM random" + }()) } diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 971e4d8041b..804250f80ec 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -195,8 +195,9 @@ type Container struct { } type Build struct { - Force bool // force build even if already present - BuildArgs []string // {"-t", "example.com/foo", "--target", "foo", "/path/to/ctx"} + Force bool // force build even if already present + BuildArgs []string // {"-t", "example.com/foo", "--target", "foo", "/path/to/ctx"} + DockerfileInline string // store contents of dockerfile_inline field is specified // TODO: call BuildKit API directly without executing `nerdctl build` } From 137be2b534b47a8b4e945c9ddffccf9e160792bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 22:07:02 +0000 Subject: [PATCH 145/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.7.1 to 2.8.1. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.7.1...v2.8.1) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.8.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 061191e610c..210d1460f74 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.7.1 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.8.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 857e8a74619..4d2e3caf2c3 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs= -github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= +github.com/compose-spec/compose-go/v2 v2.8.1 h1:27O4dzyhiS/UEUKp1zHOHCBWD1WbxGsYGMNNaSejTk4= +github.com/compose-spec/compose-go/v2 v2.8.1/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From 4e9ec4292d40471f9ccf362a429f829d6c138c93 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 14 Jul 2025 17:51:33 +0800 Subject: [PATCH 146/868] cmd: support nerdctl manifeset inspect - Add nerdctl manifest inspect to display image manifest details, with optional --verbose output. - Implement manifest parsing, formatting, and related type definitions. - Integrate the new command into the CLI. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/main.go | 4 + cmd/nerdctl/manifest/manifest.go | 40 +++ cmd/nerdctl/manifest/manifest_inspect.go | 95 +++++++ pkg/api/types/manifest_types.go | 29 ++ pkg/cmd/manifest/inspect.go | 322 +++++++++++++++++++++++ pkg/manifesttypes/manifesttypes.go | 52 ++++ 6 files changed, 542 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest.go create mode 100644 cmd/nerdctl/manifest/manifest_inspect.go create mode 100644 pkg/api/types/manifest_types.go create mode 100644 pkg/cmd/manifest/inspect.go create mode 100644 pkg/manifesttypes/manifesttypes.go diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 55cc12c9bd6..5ada639d7ff 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -40,6 +40,7 @@ import ( "github.com/containerd/nerdctl/v2/cmd/nerdctl/internal" "github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/login" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest" "github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace" "github.com/containerd/nerdctl/v2/cmd/nerdctl/network" "github.com/containerd/nerdctl/v2/cmd/nerdctl/system" @@ -344,6 +345,9 @@ Config file ($NERDCTL_TOML): %s // IPFS ipfs.NewIPFSCommand(), + + // Manifest + manifest.Command(), ) addApparmorCommand(rootCmd) container.AddCpCommand(rootCmd) diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go new file mode 100644 index 00000000000..39c63dfa57c --- /dev/null +++ b/cmd/nerdctl/manifest/manifest.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 manifest + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Annotations: map[string]string{helpers.Category: helpers.Management}, + Use: "manifest", + Short: "Manage image manifests.", + RunE: helpers.UnknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + + cmd.AddCommand( + InspectCommand(), + ) + + return cmd +} diff --git a/cmd/nerdctl/manifest/manifest_inspect.go b/cmd/nerdctl/manifest/manifest_inspect.go new file mode 100644 index 00000000000..2572883a4c6 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_inspect.go @@ -0,0 +1,95 @@ +/* + 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 manifest + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" + "github.com/containerd/nerdctl/v2/pkg/formatter" +) + +func InspectCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "inspect MANIFEST", + Short: "Display the contents of a manifest or image index/manifest list", + Args: cobra.MinimumNArgs(1), + RunE: inspectAction, + ValidArgsFunction: inspectShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().Bool("verbose", false, "Verbose output additional info including layers and platform") + cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") + return cmd +} + +func processInspectFlags(cmd *cobra.Command) (types.ManifestInspectOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.ManifestInspectOptions{}, err + } + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return types.ManifestInspectOptions{}, err + } + insecure, err := cmd.Flags().GetBool("insecure") + if err != nil { + return types.ManifestInspectOptions{}, err + } + return types.ManifestInspectOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Verbose: verbose, + Insecure: insecure, + }, nil +} + +func inspectAction(cmd *cobra.Command, args []string) error { + inspectOptions, err := processInspectFlags(cmd) + if err != nil { + return err + } + rawRef := args[0] + res, err := manifest.Inspect(cmd.Context(), rawRef, inspectOptions) + if err != nil { + return err + } + + // Output format: single object for single result, array for multiple results + if len(res) == 1 { + jsonStr, err := formatter.ToJSON(res[0], "", " ") + if err != nil { + return err + } + fmt.Fprint(inspectOptions.Stdout, jsonStr) + } else { + if formatErr := formatter.FormatSlice("", inspectOptions.Stdout, res); formatErr != nil { + return formatErr + } + } + return nil +} + +func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/manifest_types.go b/pkg/api/types/manifest_types.go new file mode 100644 index 00000000000..eacd84e4fdc --- /dev/null +++ b/pkg/api/types/manifest_types.go @@ -0,0 +1,29 @@ +/* + 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 types + +import "io" + +// ManifestInspectOptions specifies options for `nerdctl manifest inspect`. +type ManifestInspectOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Verbose output additional info including layers and platform + Verbose bool + // Allow communication with an insecure registry + Insecure bool +} diff --git a/pkg/cmd/manifest/inspect.go b/pkg/cmd/manifest/inspect.go new file mode 100644 index 00000000000..8f676f805be --- /dev/null +++ b/pkg/cmd/manifest/inspect.go @@ -0,0 +1,322 @@ +/* + 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 manifest + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/remotes" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" + "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +// manifestParser defines a function type for parsing manifest data +type manifestParser func([]byte) (interface{}, error) + +// manifestParsers maps media types to their parsing functions +var manifestParsers = map[string]manifestParser{ + ocispec.MediaTypeImageManifest: parseOCIManifest, + images.MediaTypeDockerSchema2Manifest: parseDockerManifest, + images.MediaTypeDockerSchema2ManifestList: parseDockerManifestList, + ocispec.MediaTypeImageIndex: parseOCIIndex, +} + +// getManifestFieldName returns the appropriate field name based on media type +func getManifestFieldName(mediaType string) string { + switch mediaType { + case images.MediaTypeDockerSchema2Manifest: + return "SchemaV2Manifest" + case ocispec.MediaTypeImageManifest: + return "OCIManifest" + default: + return "ManifestStruct" + } +} + +func Inspect(ctx context.Context, rawRef string, options types.ManifestInspectOptions) ([]interface{}, error) { + manifest, desc, rawData, err := getManifest(ctx, rawRef, options) + if err != nil { + return nil, err + } + + if options.Verbose { + return formatVerboseOutput(ctx, rawRef, manifest, desc, rawData, options.Insecure) + } + + // Return manifest wrapped in array for formatting compatibility + return []interface{}{manifest}, nil +} + +// formatVerboseOutput formats manifest data in Docker-compatible verbose format +func formatVerboseOutput(ctx context.Context, rawRef string, manifest interface{}, desc ocispec.Descriptor, rawData []byte, insecure bool) ([]interface{}, error) { + switch desc.MediaType { + case ocispec.MediaTypeImageIndex: + index, ok := manifest.(manifesttypes.OCIIndexStruct) + if !ok { + return nil, fmt.Errorf("expected ocispec.Index for OCI index") + } + return verboseEntriesForManifests(ctx, rawRef, index.Manifests, insecure) + + case images.MediaTypeDockerSchema2ManifestList: + di, ok := manifest.(manifesttypes.DockerManifestListStruct) + if !ok { + return nil, fmt.Errorf("expected DockerManifestListStruct for Docker manifest list") + } + return verboseEntriesForManifests(ctx, rawRef, di.Manifests, insecure) + + default: + // Single manifest + entry, err := createManifestEntry(rawRef, desc, rawData) + if err != nil { + return nil, err + } + return []interface{}{entry}, nil + } +} + +// createManifestEntry creates a DockerManifestEntry with proper ManifestStruct +func createManifestEntry(rawRef string, desc ocispec.Descriptor, rawData []byte) (manifesttypes.DockerManifestEntry, error) { + parsedRef, err := referenceutil.Parse(rawRef) + if err != nil { + return manifesttypes.DockerManifestEntry{}, fmt.Errorf("failed to parse reference: %w", err) + } + + var ref string + if parsedRef.Digest != "" { + ref = parsedRef.String() + } else { + ref = fmt.Sprintf("%s@%s", parsedRef.String(), desc.Digest.String()) + } + + entry := manifesttypes.DockerManifestEntry{ + Ref: ref, + Descriptor: desc, + Raw: base64.StdEncoding.EncodeToString(rawData), + } + + // Parse manifest data based on media type + parser, exists := manifestParsers[desc.MediaType] + if !exists { + return manifesttypes.DockerManifestEntry{}, fmt.Errorf("unsupported media type: %s", desc.MediaType) + } + + manifest, err := parser(rawData) + if err != nil { + return manifesttypes.DockerManifestEntry{}, fmt.Errorf("failed to parse manifest: %w", err) + } + + // Set the appropriate manifest field based on media type + fieldName := getManifestFieldName(desc.MediaType) + switch fieldName { + case "SchemaV2Manifest": + entry.SchemaV2Manifest = manifest + case "OCIManifest": + entry.OCIManifest = manifest + } + + // Special handling for OCI manifests to match Docker output + if desc.MediaType == ocispec.MediaTypeImageManifest { + entry.Descriptor.Annotations = nil + } + + return entry, nil +} + +// verboseEntriesForManifests fetches and formats verbose entries for a list of descriptors +func verboseEntriesForManifests(ctx context.Context, rawRef string, manifests []ocispec.Descriptor, insecure bool) ([]interface{}, error) { + parsedRef, err := referenceutil.Parse(rawRef) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + + resolver, err := createResolver(ctx, parsedRef.Domain, types.GlobalCommandOptions{}, insecure) + if err != nil { + return nil, fmt.Errorf("failed to create resolver: %w", err) + } + + fetcher, err := resolver.Fetcher(ctx, parsedRef.String()) + if err != nil { + return nil, fmt.Errorf("failed to create fetcher: %w", err) + } + + return fetchAndCreateEntries(ctx, fetcher, rawRef, manifests) +} + +// fetchAndCreateEntries fetches multiple manifests and creates DockerManifestEntry objects +func fetchAndCreateEntries(ctx context.Context, fetcher remotes.Fetcher, rawRef string, manifests []ocispec.Descriptor) ([]interface{}, error) { + entries := make([]interface{}, 0, len(manifests)) + + for _, mdesc := range manifests { + entry, err := fetchAndCreateEntry(ctx, fetcher, rawRef, mdesc) + if err != nil { + return nil, err + } + entries = append(entries, entry) + } + + return entries, nil +} + +// fetchAndCreateEntry fetches a single manifest and creates a DockerManifestEntry +func fetchAndCreateEntry(ctx context.Context, fetcher remotes.Fetcher, rawRef string, desc ocispec.Descriptor) (manifesttypes.DockerManifestEntry, error) { + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return manifesttypes.DockerManifestEntry{}, err + } + defer rc.Close() + + data, err := io.ReadAll(rc) + if err != nil { + return manifesttypes.DockerManifestEntry{}, err + } + + entry, err := createManifestEntry(rawRef, desc, data) + if err != nil { + return manifesttypes.DockerManifestEntry{}, err + } + + return entry, nil +} + +// createResolver creates a resolver for registry operations +func createResolver(ctx context.Context, domain string, globalOptions types.GlobalCommandOptions, insecure bool) (remotes.Resolver, error) { + dOpts := buildResolverOptions(globalOptions, insecure) + + resolver, err := dockerconfigresolver.New(ctx, domain, dOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create resolver: %w", err) + } + + return resolver, nil +} + +// buildResolverOptions builds resolver options based on global options and security settings +func buildResolverOptions(globalOptions types.GlobalCommandOptions, insecure bool) []dockerconfigresolver.Opt { + var dOpts []dockerconfigresolver.Opt + + if insecure { + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir)) + + return dOpts +} + +// getManifest returns manifest, descriptor, and raw data in one call +func getManifest(ctx context.Context, rawRef string, options types.ManifestInspectOptions) (interface{}, ocispec.Descriptor, []byte, error) { + parsedRef, err := referenceutil.Parse(rawRef) + if err != nil { + return nil, ocispec.Descriptor{}, nil, fmt.Errorf("failed to parse reference: %w", err) + } + + resolver, err := createResolver(ctx, parsedRef.Domain, options.GOptions, options.Insecure) + if err != nil { + return nil, ocispec.Descriptor{}, nil, fmt.Errorf("failed to create resolver: %w", err) + } + + desc, data, err := fetchManifestData(ctx, resolver, parsedRef.String()) + if err != nil { + return nil, ocispec.Descriptor{}, nil, err + } + + manifest, err := parseManifest(desc.MediaType, data) + if err != nil { + return nil, ocispec.Descriptor{}, nil, err + } + + return manifest, desc, data, nil +} + +// fetchManifestData fetches manifest descriptor and data from the registry +func fetchManifestData(ctx context.Context, resolver remotes.Resolver, ref string) (ocispec.Descriptor, []byte, error) { + _, desc, err := resolver.Resolve(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to resolve %s: %w", ref, err) + } + + fetcher, err := resolver.Fetcher(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to create fetcher: %w", err) + } + + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to fetch manifest: %w", err) + } + defer rc.Close() + + data, err := io.ReadAll(rc) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read manifest data: %w", err) + } + + return desc, data, nil +} + +// parseManifest parses manifest data based on media type +func parseManifest(mediaType string, data []byte) (interface{}, error) { + if parser, exists := manifestParsers[mediaType]; exists { + return parser(data) + } + return nil, fmt.Errorf("unsupported media type: %s", mediaType) +} + +// parseOCIManifest parses OCI manifest data +func parseOCIManifest(data []byte) (interface{}, error) { + var ociManifest manifesttypes.OCIManifestStruct + if err := json.Unmarshal(data, &ociManifest); err != nil { + return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) + } + return ociManifest, nil +} + +// parseDockerManifest parses Docker manifest data +func parseDockerManifest(data []byte) (interface{}, error) { + var dockerManifest manifesttypes.DockerManifestStruct + if err := json.Unmarshal(data, &dockerManifest); err != nil { + return nil, fmt.Errorf("failed to unmarshal docker manifest: %w", err) + } + return dockerManifest, nil +} + +// parseDockerManifestList parses Docker manifest list data +func parseDockerManifestList(data []byte) (interface{}, error) { + var manifestList manifesttypes.DockerManifestListStruct + if err := json.Unmarshal(data, &manifestList); err != nil { + return nil, fmt.Errorf("failed to unmarshal docker index: %w", err) + } + return manifestList, nil +} + +// parseOCIIndex parses OCI index data +func parseOCIIndex(data []byte) (interface{}, error) { + var index manifesttypes.OCIIndexStruct + if err := json.Unmarshal(data, &index); err != nil { + return nil, fmt.Errorf("failed to unmarshal index: %w", err) + } + return index, nil +} diff --git a/pkg/manifesttypes/manifesttypes.go b/pkg/manifesttypes/manifesttypes.go new file mode 100644 index 00000000000..129c234a13f --- /dev/null +++ b/pkg/manifesttypes/manifesttypes.go @@ -0,0 +1,52 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifesttypes + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type ( + + // DockerManifestEntry represents a single manifest entry in Docker's verbose format + DockerManifestEntry struct { + Ref string `json:"Ref"` + Descriptor ocispec.Descriptor `json:"Descriptor"` + Raw string `json:"Raw"` + SchemaV2Manifest interface{} `json:"SchemaV2Manifest,omitempty"` + OCIManifest interface{} `json:"OCIManifest,omitempty"` + } + ManifestStruct struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Config ocispec.Descriptor `json:"config"` + Layers []ocispec.Descriptor `json:"layers"` + Annotations map[string]string `json:"annotations,omitempty"` + } + + DockerManifestStruct ManifestStruct + + DockerManifestListStruct struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Manifests []ocispec.Descriptor `json:"manifests"` + } + + OCIIndexStruct ocispec.Index + + OCIManifestStruct ManifestStruct +) From 3c86f2b4e796678a96499d6399507c249ccd465e Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 16 Jul 2025 11:32:32 +0800 Subject: [PATCH 147/868] manifest: Add unit tests for manifest inspect command Add tests for tag/digest references and verbose modes Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_inspect_linux_test.go | 149 ++++++++++++++++++ cmd/nerdctl/manifest/manifest_test.go | 27 ++++ pkg/testutil/images.yaml | 8 + pkg/testutil/images_linux.go | 88 +++++++++-- pkg/testutil/testutil_linux.go | 28 ++-- 5 files changed, 276 insertions(+), 24 deletions(-) create mode 100644 cmd/nerdctl/manifest/manifest_inspect_linux_test.go create mode 100644 cmd/nerdctl/manifest/manifest_test.go diff --git a/cmd/nerdctl/manifest/manifest_inspect_linux_test.go b/cmd/nerdctl/manifest/manifest_inspect_linux_test.go new file mode 100644 index 00000000000..a9ca1914013 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_inspect_linux_test.go @@ -0,0 +1,149 @@ +/* + 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 manifest + +import ( + "encoding/json" + "testing" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +const ( + testImageName = "alpine" + testPlatform = "linux/amd64" +) + +type testData struct { + imageName string + platform string + imageRef string + manifestDigest string + configDigest string + rawData string +} + +func newTestData(imageName, platform string) *testData { + return &testData{ + imageName: imageName, + platform: platform, + imageRef: testutil.GetTestImage(imageName), + manifestDigest: testutil.GetTestImageManifestDigest(imageName, platform), + configDigest: testutil.GetTestImageConfigDigest(imageName, platform), + rawData: testutil.GetTestImageRaw(imageName, platform), + } +} + +func (td *testData) imageWithDigest() string { + return testutil.GetTestImageWithoutTag(td.imageName) + "@" + td.manifestDigest +} + +func (td *testData) isAmd64Platform(platform *ocispec.Platform) bool { + return platform != nil && + platform.Architecture == "amd64" && + platform.OS == "linux" +} + +func TestManifestInspect(t *testing.T) { + testCase := nerdtest.Setup() + td := newTestData(testImageName, testPlatform) + + testCase.SubTests = []*test.Case{ + { + Description: "tag-non-verbose", + Command: test.Command("manifest", "inspect", td.imageRef), + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var manifest manifesttypes.DockerManifestListStruct + assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest)) + + assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName)) + assert.Equal(t, manifest.MediaType, testutil.GetTestImageMediaType(td.imageName)) + assert.Assert(t, len(manifest.Manifests) > 0) + + var foundManifest *ocispec.Descriptor + for _, m := range manifest.Manifests { + if td.isAmd64Platform(m.Platform) { + foundManifest = &m + break + } + } + assert.Assert(t, foundManifest != nil, "should find amd64 platform manifest") + assert.Equal(t, foundManifest.Digest.String(), td.manifestDigest) + assert.Equal(t, foundManifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) + }), + }, + { + Description: "tag-verbose", + Command: test.Command("manifest", "inspect", td.imageRef, "--verbose"), + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var entries []manifesttypes.DockerManifestEntry + assert.NilError(t, json.Unmarshal([]byte(stdout), &entries)) + assert.Assert(t, len(entries) > 0) + + var foundEntry *manifesttypes.DockerManifestEntry + for _, e := range entries { + if td.isAmd64Platform(e.Descriptor.Platform) { + foundEntry = &e + break + } + } + assert.Assert(t, foundEntry != nil, "should find amd64 platform entry") + + expectedRef := td.imageRef + "@" + td.manifestDigest + assert.Equal(t, foundEntry.Ref, expectedRef) + assert.Equal(t, foundEntry.Descriptor.Digest.String(), td.manifestDigest) + assert.Equal(t, foundEntry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) + assert.Equal(t, foundEntry.Raw, td.rawData) + }), + }, + { + Description: "digest-non-verbose", + Command: test.Command("manifest", "inspect", td.imageWithDigest()), + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var manifest manifesttypes.DockerManifestStruct + assert.NilError(t, json.Unmarshal([]byte(stdout), &manifest)) + + assert.Equal(t, manifest.SchemaVersion, testutil.GetTestImageSchemaVersion(td.imageName)) + assert.Equal(t, manifest.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) + assert.Equal(t, manifest.Config.Digest.String(), td.configDigest) + }), + }, + { + Description: "digest-verbose", + Command: test.Command("manifest", "inspect", td.imageWithDigest(), "--verbose"), + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var entry manifesttypes.DockerManifestEntry + assert.NilError(t, json.Unmarshal([]byte(stdout), &entry)) + + assert.Equal(t, entry.Ref, td.imageWithDigest()) + assert.Equal(t, entry.Descriptor.Digest.String(), td.manifestDigest) + assert.Equal(t, entry.Descriptor.MediaType, testutil.GetTestImagePlatformMediaType(td.imageName, td.platform)) + assert.Equal(t, entry.Raw, td.rawData) + }), + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/manifest/manifest_test.go b/cmd/nerdctl/manifest/manifest_test.go new file mode 100644 index 00000000000..d4ec523683b --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_test.go @@ -0,0 +1,27 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifest + +import ( + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +func TestMain(m *testing.M) { + testutil.M(m) +} diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 5ca8bed656f..405ba8fd3f3 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -5,8 +5,16 @@ alpine: ref: "ghcr.io/stargz-containers/alpine" tag: "3.13-org" + schemaversion: 2 + mediatype: "application/vnd.docker.distribution.manifest.list.v2+json" digest: "sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a" variants: ["linux/amd64", "linux/arm64"] + manifests: + linux/amd64: + mediatype: "application/vnd.docker.distribution.manifest.v2+json" + manifest: "sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310" + config: "sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c" + raw: "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6NDlmMzU2ZmE0NTEzNjc2YzVlMjJlM2E4NDA0YWFkNmM3MjYyY2M3YWFlZDE1MzQxNDU4MjY1MzIwNzg2YzU4YyIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI4MTE5NDcsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OmNhM2NkNDJhN2M5NTI1ZjZjZTNkNjRjMWE3MDk4MjYxM2E4MjM1ZjBjYzA1N2VjOTI0NDA1MjkyMTg1M2VmMTUiCiAgICAgIH0KICAgXQp9" busybox: ref: "ghcr.io/containerd/busybox" diff --git a/pkg/testutil/images_linux.go b/pkg/testutil/images_linux.go index 641141ca066..c566e6274e5 100644 --- a/pkg/testutil/images_linux.go +++ b/pkg/testutil/images_linux.go @@ -29,30 +29,98 @@ var rawImagesList string var testImagesOnce sync.Once +type manifestInfo struct { + Config string `yaml:"config,omitempty"` + Manifest string `yaml:"manifest,omitempty"` + MediaType string `yaml:"mediatype,omitempty"` + Raw string `yaml:"raw,omitempty"` +} + type TestImage struct { - Ref string `yaml:"ref"` - Tag string `yaml:"tag,omitempty"` - Digest string `yaml:"digest,omitempty"` - Variants []string `yaml:"variants,omitempty"` + Ref string `yaml:"ref"` + Tag string `yaml:"tag,omitempty"` + SchemaVersion int `yaml:"schemaversion,omitempty"` + MediaType string `yaml:"mediatype,omitempty"` + Digest string `yaml:"digest,omitempty"` + Variants []string `yaml:"variants,omitempty"` + Manifests map[string]manifestInfo `yaml:"manifests,omitempty"` } var testImages map[string]TestImage -func getImage(key string) string { +// internal helper to lookup TestImage by key, panics if not found +func lookup(key string) TestImage { testImagesOnce.Do(func() { if err := yaml.Unmarshal([]byte(rawImagesList), &testImages); err != nil { fmt.Printf("Error unmarshaling test images YAML file: %v\n", err) panic("testing is broken") } }) - - var im TestImage - var ok bool - - if im, ok = testImages[key]; !ok { + im, ok := testImages[key] + if !ok { fmt.Printf("Image %s was not found in images list\n", key) panic("testing is broken") } + return im +} +func GetTestImage(key string) string { + im := lookup(key) return im.Ref + ":" + im.Tag } + +func GetTestImageWithoutTag(key string) string { + im := lookup(key) + return im.Ref +} + +func GetTestImageConfigDigest(key, platform string) string { + im := lookup(key) + pd, ok := im.Manifests[platform] + if !ok { + panic(fmt.Sprintf("platform %s not found for image %s", platform, key)) + } + return pd.Config +} + +func GetTestImageManifestDigest(key, platform string) string { + im := lookup(key) + pd, ok := im.Manifests[platform] + if !ok { + panic(fmt.Sprintf("platform %s not found for image %s", platform, key)) + } + return pd.Manifest +} + +func GetTestImageDigest(key string) string { + im := lookup(key) + return im.Digest +} + +func GetTestImageMediaType(key string) string { + im := lookup(key) + return im.MediaType +} + +func GetTestImageSchemaVersion(key string) int { + im := lookup(key) + return im.SchemaVersion +} + +func GetTestImagePlatformMediaType(key, platform string) string { + im := lookup(key) + pd, ok := im.Manifests[platform] + if !ok { + panic(fmt.Sprintf("platform %s not found for image %s", platform, key)) + } + return pd.MediaType +} + +func GetTestImageRaw(key, platform string) string { + im := lookup(key) + pd, ok := im.Manifests[platform] + if !ok { + panic(fmt.Sprintf("platform %s not found for image %s", platform, key)) + } + return pd.Raw +} diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index de6e68a58e2..d50d6e30489 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -17,23 +17,23 @@ package testutil var ( - AlpineImage = getImage("alpine") - BusyboxImage = getImage("busybox") - DockerAuthImage = getImage("docker_auth") - FluentdImage = getImage("fluentd") - GolangImage = getImage("golang") - KuboImage = getImage("kubo") - MariaDBImage = getImage("mariadb") - NginxAlpineImage = getImage("nginx") - RegistryImageStable = getImage("registry") - SystemdImage = getImage("stargz") - WordpressImage = getImage("wordpress") + AlpineImage = GetTestImage("alpine") + BusyboxImage = GetTestImage("busybox") + DockerAuthImage = GetTestImage("docker_auth") + FluentdImage = GetTestImage("fluentd") + GolangImage = GetTestImage("golang") + KuboImage = GetTestImage("kubo") + MariaDBImage = GetTestImage("mariadb") + NginxAlpineImage = GetTestImage("nginx") + RegistryImageStable = GetTestImage("registry") + SystemdImage = GetTestImage("stargz") + WordpressImage = GetTestImage("wordpress") CommonImage = AlpineImage - FedoraESGZImage = getImage("fedora_esgz") // eStargz - FfmpegSociImage = getImage("ffmpeg_soci") // SOCI - UbuntuImage = getImage("ubuntu") // Large enough for testing soci index creation + FedoraESGZImage = GetTestImage("fedora_esgz") // eStargz + FfmpegSociImage = GetTestImage("ffmpeg_soci") // SOCI + UbuntuImage = GetTestImage("ubuntu") // Large enough for testing soci index creation ) const ( From ca2ad1b50b0232a89822246ff002b21d8497b04a Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 16 Jul 2025 15:10:15 +0800 Subject: [PATCH 148/868] docs: add manifest inspect command reference Documented the nerdctl manifest inspect command with usage, flags, and examples. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index d2e82e8a0ea..cca699c3f2a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -51,6 +51,8 @@ It does not necessarily mean that the corresponding features are missing in cont - [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert) - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) +- [Manifest management](#manifest-management) + - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) - [:whale: nerdctl logout](#whale-nerdctl-logout) @@ -1035,6 +1037,31 @@ Flags: - `--platform=` : Convert content for a specific platform - `--all-platforms` : Convert content for all platforms (default: false) +## Manifest management + +### :whale: nerdctl manifest inspect + +Display the contents of a manifest list or manifest. + +Usage: `nerdctl manifest inspect [OPTIONS] MANIFEST` + +#### Input formats + +You can specify the manifest to inspect using one of the following formats: +- **Image name with tag**: `alpine:3.22.1` +- **Image name with digest**: `alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f` + +Flags: + +- `--verbose` : Verbose output, show additional info including layers and platform +- `--insecure`: Allow communication with an insecure registry +Example: + +```bash +nerdctl manifest inspect alpine:3.22.1 +nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f +``` + ## Registry ### :whale: nerdctl login From 78b71815c16271687f25b0b393ab562ae336e14e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:11:42 +0000 Subject: [PATCH 149/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.3.2+incompatible to 28.3.3+incompatible - [Commits](https://github.com/docker/cli/compare/v28.3.2...v28.3.3) Updates `github.com/docker/docker` from 28.3.2+incompatible to 28.3.3+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.2...v28.3.3) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.3.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.3.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 061191e610c..8a2af35201d 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.3.2+incompatible //gomodjail:unconfined - github.com/docker/docker v28.3.2+incompatible //gomodjail:unconfined + github.com/docker/cli v28.3.3+incompatible //gomodjail:unconfined + github.com/docker/docker v28.3.3+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 857e8a74619..ec8393da5d7 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= -github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= -github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo= +github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= From 1756355d9db02167d64416cc2dfd573bf3576629 Mon Sep 17 00:00:00 2001 From: Ruihua Wen Date: Wed, 30 Jul 2025 16:08:25 +0900 Subject: [PATCH 150/868] fix: add shell completion for namespace commands Signed-off-by: Ruihua Wen --- cmd/nerdctl/namespace/namespace_inspect.go | 19 +++++++++++++------ cmd/nerdctl/namespace/namespace_remove.go | 21 ++++++++++++++------- cmd/nerdctl/namespace/namespace_update.go | 19 +++++++++++++------ 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/namespace/namespace_inspect.go b/cmd/nerdctl/namespace/namespace_inspect.go index 8afe47c21cd..b79868dbb48 100644 --- a/cmd/nerdctl/namespace/namespace_inspect.go +++ b/cmd/nerdctl/namespace/namespace_inspect.go @@ -19,6 +19,7 @@ package namespace import ( "github.com/spf13/cobra" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -27,12 +28,13 @@ import ( func inspectCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "inspect NAMESPACE", - Short: "Display detailed information on one or more namespaces.", - RunE: inspectAction, - Args: cobra.MinimumNArgs(1), - SilenceUsage: true, - SilenceErrors: true, + Use: "inspect NAMESPACE", + Short: "Display detailed information on one or more namespaces.", + RunE: inspectAction, + ValidArgsFunction: namespaceInspectShellComplete, + Args: cobra.MinimumNArgs(1), + SilenceUsage: true, + SilenceErrors: true, } cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -71,3 +73,8 @@ func inspectAction(cmd *cobra.Command, args []string) error { return namespace.Inspect(ctx, client, args, options) } + +func namespaceInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show namespace names + return completion.NamespaceNames(cmd, args, toComplete) +} diff --git a/cmd/nerdctl/namespace/namespace_remove.go b/cmd/nerdctl/namespace/namespace_remove.go index 5624e2d9d80..3ce29a5741f 100644 --- a/cmd/nerdctl/namespace/namespace_remove.go +++ b/cmd/nerdctl/namespace/namespace_remove.go @@ -19,6 +19,7 @@ package namespace import ( "github.com/spf13/cobra" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -27,13 +28,14 @@ import ( func removeCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "remove [flags] NAMESPACE [NAMESPACE...]", - Aliases: []string{"rm"}, - Args: cobra.MinimumNArgs(1), - Short: "Remove one or more namespaces", - RunE: removeAction, - SilenceUsage: true, - SilenceErrors: true, + Use: "remove [flags] NAMESPACE [NAMESPACE...]", + Aliases: []string{"rm"}, + Args: cobra.MinimumNArgs(1), + Short: "Remove one or more namespaces", + RunE: removeAction, + ValidArgsFunction: namespaceRemoveShellComplete, + SilenceUsage: true, + SilenceErrors: true, } cmd.Flags().BoolP("cgroup", "c", false, "delete the namespace's cgroup") return cmd @@ -69,3 +71,8 @@ func removeAction(cmd *cobra.Command, args []string) error { return namespace.Remove(ctx, client, args, options) } + +func namespaceRemoveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show namespace names + return completion.NamespaceNames(cmd, args, toComplete) +} diff --git a/cmd/nerdctl/namespace/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go index 1909d90e701..a15a865bde1 100644 --- a/cmd/nerdctl/namespace/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -19,6 +19,7 @@ package namespace import ( "github.com/spf13/cobra" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -27,12 +28,13 @@ import ( func updateCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "update [flags] NAMESPACE", - Short: "Update labels for a namespace", - RunE: updateAction, - Args: cobra.MinimumNArgs(1), - SilenceUsage: true, - SilenceErrors: true, + Use: "update [flags] NAMESPACE", + Short: "Update labels for a namespace", + RunE: updateAction, + ValidArgsFunction: namespaceUpdateShellComplete, + Args: cobra.MinimumNArgs(1), + SilenceUsage: true, + SilenceErrors: true, } cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace") return cmd @@ -67,3 +69,8 @@ func updateAction(cmd *cobra.Command, args []string) error { return namespace.Update(ctx, client, args[0], options) } + +func namespaceUpdateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show namespace names + return completion.NamespaceNames(cmd, args, toComplete) +} From ff908fdc24929ed9f1c8619c7d2d3d6120a775bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:12:20 +0000 Subject: [PATCH 151/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.3 to 2.1.4. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.1.3...v2.1.4) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.1.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 061191e610c..58ddc8e63d0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.3 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.1.4 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 857e8a74619..817a1b84e5b 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.3 h1:eMD2SLcIQPdMlnlNF6fatlrlRLAeDaiGPGwmRKLZKNs= -github.com/containerd/containerd/v2 v2.1.3/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ= +github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From 8c1110e57e42e81aabbf63e8b444e183d904597e Mon Sep 17 00:00:00 2001 From: Swapnanil-Gupta Date: Thu, 31 Jul 2025 17:00:38 +0000 Subject: [PATCH 152/868] Provenance flag fixes: - Skip passing provenance flag to buildctl when '--provenance=disabled' - Skip listing attestation manifest on `image ls` Signed-off-by: Swapnanil-Gupta --- pkg/cmd/builder/build.go | 9 +++++++++ pkg/cmd/image/list.go | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index 30a5d2aebd2..835b2ece8f1 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -404,6 +404,15 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option for _, s := range strutil.DedupeStrSlice(options.Attest) { optAttestType, optAttestAttrs, _ := strings.Cut(s, ",") if strings.HasPrefix(optAttestType, "type=") { + if strings.HasPrefix(optAttestAttrs, "disabled=") { + disabled, err := strconv.ParseBool(strings.TrimPrefix(optAttestAttrs, "disabled=")) + if err != nil { + return "", nil, false, "", nil, nil, fmt.Errorf("invalid value for attribute \"disabled\"") + } + if disabled { + continue + } + } optAttestType := strings.TrimPrefix(optAttestType, "type=") buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=attest:%s=%s", optAttestType, optAttestAttrs)) } else { diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go index c7440b459fb..0476b6b55fb 100644 --- a/pkg/cmd/image/list.go +++ b/pkg/cmd/image/list.go @@ -312,6 +312,10 @@ func readIndex(ctx context.Context, provider content.Provider, snapshotter snaps // Iterate over manifest descriptors and read them all for _, manifestDescriptor := range index.Manifests { + if isAttestationManifestDescriptor(manifestDescriptor) { + continue + } + manifest, err := readManifest(ctx, provider, snapshotter, manifestDescriptor) if err != nil { continue @@ -419,3 +423,9 @@ func (x *imagePrinter) printImageSinglePlatform(desc ocispec.Descriptor, img ima } return nil } + +func isAttestationManifestDescriptor(desc ocispec.Descriptor) bool { + const manifestReferenceType = "vnd.docker.reference.type" + const attestationManifest = "attestation-manifest" + return desc.Annotations[manifestReferenceType] == attestationManifest +} From bd6b61afc81c998819e8440216a55ce30f1aa75c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 2 Aug 2025 04:46:21 +0900 Subject: [PATCH 153/868] mv pkg/fs.go pkg/fs/fs.go Signed-off-by: Akihiro Suda --- cmd/nerdctl/helpers/flagutil.go | 4 ++-- pkg/{ => fs}/fs.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename pkg/{ => fs}/fs.go (98%) diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index e4c09e49cc7..32217ae95c6 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -21,8 +21,8 @@ import ( "github.com/spf13/cobra" - "github.com/containerd/nerdctl/v2/pkg" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/fs" ) func VerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions, err error) { @@ -159,7 +159,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) } // Point to dataRoot for filesystem-helpers implementing rollback / backups. - err = pkg.InitFS(dataRoot) + err = fs.InitFS(dataRoot) if err != nil { return types.GlobalCommandOptions{}, err } diff --git a/pkg/fs.go b/pkg/fs/fs.go similarity index 98% rename from pkg/fs.go rename to pkg/fs/fs.go index 4b535867b44..b9313806172 100644 --- a/pkg/fs.go +++ b/pkg/fs/fs.go @@ -14,7 +14,7 @@ limitations under the License. */ -package pkg +package fs import "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" From d0ee88bd5bb381a39c337a65e921d40fe815cffa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:06:31 +0000 Subject: [PATCH 154/868] build(deps): bump docker/metadata-action from 5.7.0 to 5.8.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/902fa8ec7d6ecbf8d84d538b9b233a880e428804...c1e51972afc2121e065aed6d45c65596fe445f3f) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 5.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 9ede0bdfd05..14fe15e225a 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -54,7 +54,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From 9a7c3e07925524b00b44bf5e65dafecae31cdd31 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 1 Aug 2025 17:17:09 +0800 Subject: [PATCH 155/868] manifest: support nerdctl manifest create command Support `nerdctl manifest create` command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest.go | 1 + cmd/nerdctl/manifest/manifest_create.go | 87 ++++++++ pkg/api/types/manifest_types.go | 10 + pkg/cmd/manifest/create.go | 86 ++++++++ pkg/cmd/manifest/inspect.go | 258 +++-------------------- pkg/manifeststore/manifeststore.go | 109 ++++++++++ pkg/manifestutil/manifestutils.go | 262 ++++++++++++++++++++++++ 7 files changed, 580 insertions(+), 233 deletions(-) create mode 100644 cmd/nerdctl/manifest/manifest_create.go create mode 100644 pkg/cmd/manifest/create.go create mode 100644 pkg/manifeststore/manifeststore.go create mode 100644 pkg/manifestutil/manifestutils.go diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go index 39c63dfa57c..201b72b14db 100644 --- a/cmd/nerdctl/manifest/manifest.go +++ b/cmd/nerdctl/manifest/manifest.go @@ -34,6 +34,7 @@ func Command() *cobra.Command { cmd.AddCommand( InspectCommand(), + CreateCommand(), ) return cmd diff --git a/cmd/nerdctl/manifest/manifest_create.go b/cmd/nerdctl/manifest/manifest_create.go new file mode 100644 index 00000000000..962a406a09e --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_create.go @@ -0,0 +1,87 @@ +/* + 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 manifest + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" +) + +func CreateCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "create INDEX/MANIFESTLIST MANIFEST [MANIFEST...]", + Short: "Create a local index/manifest list for annotating and pushing to a registry", + Args: cobra.MinimumNArgs(2), + RunE: createAction, + ValidArgsFunction: createShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().Bool("amend", false, "Amend the existing index/manifest list") + cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") + return cmd +} + +func processCreateFlags(cmd *cobra.Command) (types.ManifestCreateOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.ManifestCreateOptions{}, err + } + amend, err := cmd.Flags().GetBool("amend") + if err != nil { + return types.ManifestCreateOptions{}, err + } + insecure, err := cmd.Flags().GetBool("insecure") + if err != nil { + return types.ManifestCreateOptions{}, err + } + return types.ManifestCreateOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Amend: amend, + Insecure: insecure, + }, nil +} + +func createAction(cmd *cobra.Command, args []string) error { + createOptions, err := processCreateFlags(cmd) + if err != nil { + return err + } + + listRef := args[0] + manifestRefs := args[1:] + + listRef, err = manifest.Create(cmd.Context(), listRef, manifestRefs, createOptions) + if err != nil { + return err + } + + fmt.Fprintln(createOptions.Stdout, "Created manifest list", listRef) + + return nil +} + +func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/manifest_types.go b/pkg/api/types/manifest_types.go index eacd84e4fdc..9dd6bfa1cf8 100644 --- a/pkg/api/types/manifest_types.go +++ b/pkg/api/types/manifest_types.go @@ -18,6 +18,16 @@ package types import "io" +// ManifestCreateOptions specifies options for `nerdctl manifest create`. +type ManifestCreateOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Amend an existing manifest list + Amend bool + // Allow communication with an insecure registry + Insecure bool +} + // ManifestInspectOptions specifies options for `nerdctl manifest inspect`. type ManifestInspectOptions struct { Stdout io.Writer diff --git a/pkg/cmd/manifest/create.go b/pkg/cmd/manifest/create.go new file mode 100644 index 00000000000..3fc2e168651 --- /dev/null +++ b/pkg/cmd/manifest/create.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 manifest + +import ( + "context" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/images" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/manifeststore" + "github.com/containerd/nerdctl/v2/pkg/manifestutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +// Create creates a local manifest list/index +func Create(ctx context.Context, listRef string, manifestRefs []string, options types.ManifestCreateOptions) (string, error) { + parsedListRef, err := referenceutil.Parse(listRef) + if err != nil { + return "", fmt.Errorf("failed to parse list reference: %w", err) + } + + manifestStore, err := manifeststore.NewStore(options.GOptions.DataRoot) + if err != nil { + return "", fmt.Errorf("failed to create manifest store: %w", err) + } + + existingManifests, err := manifestStore.GetList(parsedListRef) + if err == nil && len(existingManifests) > 0 && !options.Amend { + return "", fmt.Errorf("refusing to amend an existing manifest list with no --amend flag") + } + + for _, manifestRef := range manifestRefs { + parsedRef, err := referenceutil.Parse(manifestRef) + if err != nil { + return "", fmt.Errorf("failed to parse manifest reference %s: %w", manifestRef, err) + } + + manifest, desc, rawData, err := manifestutil.GetManifest(ctx, parsedRef, options.GOptions, options.Insecure) + if err != nil { + return "", fmt.Errorf("failed to fetch manifest %s: %w", manifestRef, err) + } + + // Check if the manifest is manifest list + if desc.MediaType == images.MediaTypeDockerSchema2ManifestList || desc.MediaType == ocispec.MediaTypeImageIndex { + return "", fmt.Errorf("%s is a manifest list", manifestRef) + } + + imageManifest, err := manifestutil.CreateManifestEntry(parsedRef, desc, rawData) + if err != nil { + return "", fmt.Errorf("failed to create manifest entry for %s: %w", manifestRef, err) + } + + // Get platform information from config + if desc.MediaType == ocispec.MediaTypeImageManifest || desc.MediaType == images.MediaTypeDockerSchema2Manifest { + platform, err := manifestutil.GetPlatform(ctx, parsedRef.Domain, options.GOptions, options.Insecure, manifestRef, manifest) + if err != nil { + return "", fmt.Errorf("failed to extract platform for %s: %w", manifestRef, err) + } + imageManifest.Descriptor.Platform = platform + } + + if err := manifestStore.Save(parsedListRef, parsedRef, &imageManifest); err != nil { + return "", fmt.Errorf("failed to store manifest %s: %w", manifestRef, err) + } + } + + return listRef, nil +} diff --git a/pkg/cmd/manifest/inspect.go b/pkg/cmd/manifest/inspect.go index 8f676f805be..10e59dc72f0 100644 --- a/pkg/cmd/manifest/inspect.go +++ b/pkg/cmd/manifest/inspect.go @@ -18,53 +18,32 @@ package manifest import ( "context" - "encoding/base64" - "encoding/json" "fmt" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/containerd/containerd/v2/core/images" - "github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/nerdctl/v2/pkg/api/types" - "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/manifestutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) -// manifestParser defines a function type for parsing manifest data -type manifestParser func([]byte) (interface{}, error) - -// manifestParsers maps media types to their parsing functions -var manifestParsers = map[string]manifestParser{ - ocispec.MediaTypeImageManifest: parseOCIManifest, - images.MediaTypeDockerSchema2Manifest: parseDockerManifest, - images.MediaTypeDockerSchema2ManifestList: parseDockerManifestList, - ocispec.MediaTypeImageIndex: parseOCIIndex, -} - -// getManifestFieldName returns the appropriate field name based on media type -func getManifestFieldName(mediaType string) string { - switch mediaType { - case images.MediaTypeDockerSchema2Manifest: - return "SchemaV2Manifest" - case ocispec.MediaTypeImageManifest: - return "OCIManifest" - default: - return "ManifestStruct" +func Inspect(ctx context.Context, rawRef string, options types.ManifestInspectOptions) ([]interface{}, error) { + parsedRef, err := referenceutil.Parse(rawRef) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) } -} -func Inspect(ctx context.Context, rawRef string, options types.ManifestInspectOptions) ([]interface{}, error) { - manifest, desc, rawData, err := getManifest(ctx, rawRef, options) + manifest, desc, rawData, err := manifestutil.GetManifest(ctx, parsedRef, options.GOptions, options.Insecure) if err != nil { return nil, err } if options.Verbose { - return formatVerboseOutput(ctx, rawRef, manifest, desc, rawData, options.Insecure) + return formatVerboseOutput(ctx, parsedRef, manifest, desc, rawData, options.Insecure) } // Return manifest wrapped in array for formatting compatibility @@ -72,25 +51,24 @@ func Inspect(ctx context.Context, rawRef string, options types.ManifestInspectOp } // formatVerboseOutput formats manifest data in Docker-compatible verbose format -func formatVerboseOutput(ctx context.Context, rawRef string, manifest interface{}, desc ocispec.Descriptor, rawData []byte, insecure bool) ([]interface{}, error) { +func formatVerboseOutput(ctx context.Context, parsedRef *referenceutil.ImageReference, manifest interface{}, desc ocispec.Descriptor, rawData []byte, insecure bool) ([]interface{}, error) { switch desc.MediaType { case ocispec.MediaTypeImageIndex: index, ok := manifest.(manifesttypes.OCIIndexStruct) if !ok { return nil, fmt.Errorf("expected ocispec.Index for OCI index") } - return verboseEntriesForManifests(ctx, rawRef, index.Manifests, insecure) + return verboseEntriesForManifests(ctx, parsedRef, index.Manifests, insecure) case images.MediaTypeDockerSchema2ManifestList: di, ok := manifest.(manifesttypes.DockerManifestListStruct) if !ok { return nil, fmt.Errorf("expected DockerManifestListStruct for Docker manifest list") } - return verboseEntriesForManifests(ctx, rawRef, di.Manifests, insecure) + return verboseEntriesForManifests(ctx, parsedRef, di.Manifests, insecure) default: - // Single manifest - entry, err := createManifestEntry(rawRef, desc, rawData) + entry, err := manifestutil.CreateManifestEntry(parsedRef, desc, rawData) if err != nil { return nil, err } @@ -98,62 +76,10 @@ func formatVerboseOutput(ctx context.Context, rawRef string, manifest interface{ } } -// createManifestEntry creates a DockerManifestEntry with proper ManifestStruct -func createManifestEntry(rawRef string, desc ocispec.Descriptor, rawData []byte) (manifesttypes.DockerManifestEntry, error) { - parsedRef, err := referenceutil.Parse(rawRef) - if err != nil { - return manifesttypes.DockerManifestEntry{}, fmt.Errorf("failed to parse reference: %w", err) - } - - var ref string - if parsedRef.Digest != "" { - ref = parsedRef.String() - } else { - ref = fmt.Sprintf("%s@%s", parsedRef.String(), desc.Digest.String()) - } - - entry := manifesttypes.DockerManifestEntry{ - Ref: ref, - Descriptor: desc, - Raw: base64.StdEncoding.EncodeToString(rawData), - } - - // Parse manifest data based on media type - parser, exists := manifestParsers[desc.MediaType] - if !exists { - return manifesttypes.DockerManifestEntry{}, fmt.Errorf("unsupported media type: %s", desc.MediaType) - } - - manifest, err := parser(rawData) - if err != nil { - return manifesttypes.DockerManifestEntry{}, fmt.Errorf("failed to parse manifest: %w", err) - } - - // Set the appropriate manifest field based on media type - fieldName := getManifestFieldName(desc.MediaType) - switch fieldName { - case "SchemaV2Manifest": - entry.SchemaV2Manifest = manifest - case "OCIManifest": - entry.OCIManifest = manifest - } - - // Special handling for OCI manifests to match Docker output - if desc.MediaType == ocispec.MediaTypeImageManifest { - entry.Descriptor.Annotations = nil - } - - return entry, nil -} - // verboseEntriesForManifests fetches and formats verbose entries for a list of descriptors -func verboseEntriesForManifests(ctx context.Context, rawRef string, manifests []ocispec.Descriptor, insecure bool) ([]interface{}, error) { - parsedRef, err := referenceutil.Parse(rawRef) - if err != nil { - return nil, fmt.Errorf("failed to parse reference: %w", err) - } +func verboseEntriesForManifests(ctx context.Context, parsedRef *referenceutil.ImageReference, manifests []ocispec.Descriptor, insecure bool) ([]interface{}, error) { - resolver, err := createResolver(ctx, parsedRef.Domain, types.GlobalCommandOptions{}, insecure) + resolver, err := manifestutil.CreateResolver(ctx, parsedRef.Domain, types.GlobalCommandOptions{}, insecure) if err != nil { return nil, fmt.Errorf("failed to create resolver: %w", err) } @@ -163,160 +89,26 @@ func verboseEntriesForManifests(ctx context.Context, rawRef string, manifests [] return nil, fmt.Errorf("failed to create fetcher: %w", err) } - return fetchAndCreateEntries(ctx, fetcher, rawRef, manifests) -} - -// fetchAndCreateEntries fetches multiple manifests and creates DockerManifestEntry objects -func fetchAndCreateEntries(ctx context.Context, fetcher remotes.Fetcher, rawRef string, manifests []ocispec.Descriptor) ([]interface{}, error) { entries := make([]interface{}, 0, len(manifests)) for _, mdesc := range manifests { - entry, err := fetchAndCreateEntry(ctx, fetcher, rawRef, mdesc) + rc, err := fetcher.Fetch(ctx, mdesc) if err != nil { return nil, err } - entries = append(entries, entry) - } - - return entries, nil -} - -// fetchAndCreateEntry fetches a single manifest and creates a DockerManifestEntry -func fetchAndCreateEntry(ctx context.Context, fetcher remotes.Fetcher, rawRef string, desc ocispec.Descriptor) (manifesttypes.DockerManifestEntry, error) { - rc, err := fetcher.Fetch(ctx, desc) - if err != nil { - return manifesttypes.DockerManifestEntry{}, err - } - defer rc.Close() - - data, err := io.ReadAll(rc) - if err != nil { - return manifesttypes.DockerManifestEntry{}, err - } - - entry, err := createManifestEntry(rawRef, desc, data) - if err != nil { - return manifesttypes.DockerManifestEntry{}, err - } - - return entry, nil -} - -// createResolver creates a resolver for registry operations -func createResolver(ctx context.Context, domain string, globalOptions types.GlobalCommandOptions, insecure bool) (remotes.Resolver, error) { - dOpts := buildResolverOptions(globalOptions, insecure) - - resolver, err := dockerconfigresolver.New(ctx, domain, dOpts...) - if err != nil { - return nil, fmt.Errorf("failed to create resolver: %w", err) - } - - return resolver, nil -} - -// buildResolverOptions builds resolver options based on global options and security settings -func buildResolverOptions(globalOptions types.GlobalCommandOptions, insecure bool) []dockerconfigresolver.Opt { - var dOpts []dockerconfigresolver.Opt - - if insecure { - dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) - } - dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir)) - - return dOpts -} - -// getManifest returns manifest, descriptor, and raw data in one call -func getManifest(ctx context.Context, rawRef string, options types.ManifestInspectOptions) (interface{}, ocispec.Descriptor, []byte, error) { - parsedRef, err := referenceutil.Parse(rawRef) - if err != nil { - return nil, ocispec.Descriptor{}, nil, fmt.Errorf("failed to parse reference: %w", err) - } - - resolver, err := createResolver(ctx, parsedRef.Domain, options.GOptions, options.Insecure) - if err != nil { - return nil, ocispec.Descriptor{}, nil, fmt.Errorf("failed to create resolver: %w", err) - } - - desc, data, err := fetchManifestData(ctx, resolver, parsedRef.String()) - if err != nil { - return nil, ocispec.Descriptor{}, nil, err - } - - manifest, err := parseManifest(desc.MediaType, data) - if err != nil { - return nil, ocispec.Descriptor{}, nil, err - } + defer rc.Close() - return manifest, desc, data, nil -} - -// fetchManifestData fetches manifest descriptor and data from the registry -func fetchManifestData(ctx context.Context, resolver remotes.Resolver, ref string) (ocispec.Descriptor, []byte, error) { - _, desc, err := resolver.Resolve(ctx, ref) - if err != nil { - return ocispec.Descriptor{}, nil, fmt.Errorf("failed to resolve %s: %w", ref, err) - } - - fetcher, err := resolver.Fetcher(ctx, ref) - if err != nil { - return ocispec.Descriptor{}, nil, fmt.Errorf("failed to create fetcher: %w", err) - } - - rc, err := fetcher.Fetch(ctx, desc) - if err != nil { - return ocispec.Descriptor{}, nil, fmt.Errorf("failed to fetch manifest: %w", err) - } - defer rc.Close() - - data, err := io.ReadAll(rc) - if err != nil { - return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read manifest data: %w", err) - } - - return desc, data, nil -} - -// parseManifest parses manifest data based on media type -func parseManifest(mediaType string, data []byte) (interface{}, error) { - if parser, exists := manifestParsers[mediaType]; exists { - return parser(data) - } - return nil, fmt.Errorf("unsupported media type: %s", mediaType) -} - -// parseOCIManifest parses OCI manifest data -func parseOCIManifest(data []byte) (interface{}, error) { - var ociManifest manifesttypes.OCIManifestStruct - if err := json.Unmarshal(data, &ociManifest); err != nil { - return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) - } - return ociManifest, nil -} - -// parseDockerManifest parses Docker manifest data -func parseDockerManifest(data []byte) (interface{}, error) { - var dockerManifest manifesttypes.DockerManifestStruct - if err := json.Unmarshal(data, &dockerManifest); err != nil { - return nil, fmt.Errorf("failed to unmarshal docker manifest: %w", err) - } - return dockerManifest, nil -} + data, err := io.ReadAll(rc) + if err != nil { + return nil, err + } -// parseDockerManifestList parses Docker manifest list data -func parseDockerManifestList(data []byte) (interface{}, error) { - var manifestList manifesttypes.DockerManifestListStruct - if err := json.Unmarshal(data, &manifestList); err != nil { - return nil, fmt.Errorf("failed to unmarshal docker index: %w", err) + entry, err := manifestutil.CreateManifestEntry(parsedRef, mdesc, data) + if err != nil { + return nil, err + } + entries = append(entries, entry) } - return manifestList, nil -} -// parseOCIIndex parses OCI index data -func parseOCIIndex(data []byte) (interface{}, error) { - var index manifesttypes.OCIIndexStruct - if err := json.Unmarshal(data, &index); err != nil { - return nil, fmt.Errorf("failed to unmarshal index: %w", err) - } - return index, nil + return entries, nil } diff --git a/pkg/manifeststore/manifeststore.go b/pkg/manifeststore/manifeststore.go new file mode 100644 index 00000000000..6681caedc5d --- /dev/null +++ b/pkg/manifeststore/manifeststore.go @@ -0,0 +1,109 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifeststore + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/store" +) + +type Store interface { + // GetList returns all the local manifests for a index or manifest list + GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error) + // Save saves a manifest as part of a index or local manifest list + Save(listRef, manifestRef *referenceutil.ImageReference, manifest *manifesttypes.DockerManifestEntry) error +} + +type manifestStore struct { + store store.Store +} + +func NewStore(dataRoot string) (Store, error) { + manifestRoot := filepath.Join(dataRoot, "manifests") + st, err := store.New(manifestRoot, 0o755, 0o644) + if err != nil { + return nil, fmt.Errorf("failed to create manifest store: %w", err) + } + return &manifestStore{store: st}, nil +} + +func (s *manifestStore) GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error) { + listPath := makeFilesafeName(listRef.String()) + + if err := s.store.Lock(); err != nil { + return nil, err + } + defer s.store.Release() + + manifestPaths, err := s.store.List(listPath) + if err != nil { + return nil, err + } + + var manifests []*manifesttypes.DockerManifestEntry + for _, manifestPath := range manifestPaths { + manifest, err := s.getManifestFromPath(listPath, manifestPath) + if err != nil { + return nil, err + } + manifests = append(manifests, manifest) + } + + return manifests, nil +} + +func (s *manifestStore) Save(listRef, manifestRef *referenceutil.ImageReference, manifest *manifesttypes.DockerManifestEntry) error { + return s.store.WithLock(func() error { + listPath := makeFilesafeName(listRef.String()) + if err := s.store.GroupEnsure(listPath); err != nil { + return err + } + + manifestPath := makeFilesafeName(manifestRef.String()) + data, err := json.Marshal(manifest) + if err != nil { + return err + } + + return s.store.Set(data, listPath, manifestPath) + }) +} + +func (s *manifestStore) getManifestFromPath(listPath, manifestPath string) (*manifesttypes.DockerManifestEntry, error) { + data, err := s.store.Get(listPath, manifestPath) + if err != nil { + return nil, err + } + + var manifest manifesttypes.DockerManifestEntry + if err := json.Unmarshal(data, &manifest); err != nil { + return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) + } + + return &manifest, nil +} + +func makeFilesafeName(ref string) string { + fileName := strings.ReplaceAll(ref, ":", "-") + return strings.ReplaceAll(fileName, "/", "_") +} diff --git a/pkg/manifestutil/manifestutils.go b/pkg/manifestutil/manifestutils.go new file mode 100644 index 00000000000..978084f6793 --- /dev/null +++ b/pkg/manifestutil/manifestutils.go @@ -0,0 +1,262 @@ +/* + 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 manifestutil + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/remotes" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" + "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +// manifestParser defines a function type for parsing manifest data +type manifestParser func([]byte) (interface{}, error) + +// manifestParsers maps media types to their parsing functions +var manifestParsers = map[string]manifestParser{ + ocispec.MediaTypeImageManifest: parseOCIManifest, + images.MediaTypeDockerSchema2Manifest: parseDockerManifest, + images.MediaTypeDockerSchema2ManifestList: parseDockerManifestList, + ocispec.MediaTypeImageIndex: parseOCIIndex, +} + +// ParseManifest parses manifest data based on media type +func ParseManifest(mediaType string, data []byte) (interface{}, error) { + if parser, exists := manifestParsers[mediaType]; exists { + return parser(data) + } + return nil, fmt.Errorf("unsupported media type: %s", mediaType) +} + +// parseOCIManifest parses OCI manifest data +func parseOCIManifest(data []byte) (interface{}, error) { + var ociManifest manifesttypes.OCIManifestStruct + if err := json.Unmarshal(data, &ociManifest); err != nil { + return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) + } + return ociManifest, nil +} + +// parseDockerManifest parses Docker manifest data +func parseDockerManifest(data []byte) (interface{}, error) { + var dockerManifest manifesttypes.DockerManifestStruct + if err := json.Unmarshal(data, &dockerManifest); err != nil { + return nil, fmt.Errorf("failed to unmarshal docker manifest: %w", err) + } + return dockerManifest, nil +} + +// parseDockerManifestList parses Docker manifest list data +func parseDockerManifestList(data []byte) (interface{}, error) { + var manifestList manifesttypes.DockerManifestListStruct + if err := json.Unmarshal(data, &manifestList); err != nil { + return nil, fmt.Errorf("failed to unmarshal docker index: %w", err) + } + return manifestList, nil +} + +// parseOCIIndex parses OCI index data +func parseOCIIndex(data []byte) (interface{}, error) { + var index manifesttypes.OCIIndexStruct + if err := json.Unmarshal(data, &index); err != nil { + return nil, fmt.Errorf("failed to unmarshal index: %w", err) + } + return index, nil +} + +// CreateResolver creates a resolver for registry operations +func CreateResolver(ctx context.Context, domain string, globalOptions types.GlobalCommandOptions, insecure bool) (remotes.Resolver, error) { + dOpts := buildResolverOptions(globalOptions, insecure) + + resolver, err := dockerconfigresolver.New(ctx, domain, dOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create resolver: %w", err) + } + + return resolver, nil +} + +// buildResolverOptions builds resolver options based on global options and security settings +func buildResolverOptions(globalOptions types.GlobalCommandOptions, insecure bool) []dockerconfigresolver.Opt { + var dOpts []dockerconfigresolver.Opt + + if insecure { + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir)) + + return dOpts +} + +// FetchManifestData fetches manifest descriptor and data from the registry +func FetchManifestData(ctx context.Context, resolver remotes.Resolver, ref string) (ocispec.Descriptor, []byte, error) { + _, desc, err := resolver.Resolve(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to resolve %s: %w", ref, err) + } + + fetcher, err := resolver.Fetcher(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to create fetcher: %w", err) + } + + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to fetch manifest: %w", err) + } + defer rc.Close() + + data, err := io.ReadAll(rc) + if err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read manifest data: %w", err) + } + + return desc, data, nil +} + +// GetManifest returns manifest, descriptor, and raw data in one call +func GetManifest(ctx context.Context, parsedRef *referenceutil.ImageReference, globalOptions types.GlobalCommandOptions, insecure bool) (interface{}, ocispec.Descriptor, []byte, error) { + resolver, err := CreateResolver(ctx, parsedRef.Domain, globalOptions, insecure) + if err != nil { + return nil, ocispec.Descriptor{}, nil, fmt.Errorf("failed to create resolver: %w", err) + } + + desc, data, err := FetchManifestData(ctx, resolver, parsedRef.String()) + if err != nil { + return nil, ocispec.Descriptor{}, nil, err + } + + manifest, err := ParseManifest(desc.MediaType, data) + if err != nil { + return nil, ocispec.Descriptor{}, nil, err + } + + return manifest, desc, data, nil +} + +// getManifestFieldName returns the appropriate field name based on media type +func getManifestFieldName(mediaType string) string { + switch mediaType { + case images.MediaTypeDockerSchema2Manifest: + return "SchemaV2Manifest" + case ocispec.MediaTypeImageManifest: + return "OCIManifest" + default: + return "ManifestStruct" + } +} + +// CreateManifestEntry creates a DockerManifestEntry with proper ManifestStruct +func CreateManifestEntry(parsedRef *referenceutil.ImageReference, desc ocispec.Descriptor, rawData []byte) (manifesttypes.DockerManifestEntry, error) { + var ref string + if parsedRef.Digest != "" { + ref = parsedRef.String() + } else { + ref = fmt.Sprintf("%s@%s", parsedRef.String(), desc.Digest.String()) + } + + entry := manifesttypes.DockerManifestEntry{ + Ref: ref, + Descriptor: desc, + Raw: base64.StdEncoding.EncodeToString(rawData), + } + + manifest, err := ParseManifest(desc.MediaType, rawData) + if err != nil { + return manifesttypes.DockerManifestEntry{}, fmt.Errorf("failed to parse manifest: %w", err) + } + + fieldName := getManifestFieldName(desc.MediaType) + switch fieldName { + case "SchemaV2Manifest": + entry.SchemaV2Manifest = manifest + case "OCIManifest": + entry.OCIManifest = manifest + } + + // Special handling for OCI manifests to match Docker output + if desc.MediaType == ocispec.MediaTypeImageManifest { + entry.Descriptor.Annotations = nil + } + + return entry, nil +} + +// getPlatformFromConfig return platform information from the config blob +func getPlatformFromConfig(ctx context.Context, resolver remotes.Resolver, ref string, configDesc ocispec.Descriptor) (*ocispec.Platform, error) { + fetcher, err := resolver.Fetcher(ctx, ref) + if err != nil { + return nil, fmt.Errorf("failed to create fetcher: %w", err) + } + + rc, err := fetcher.Fetch(ctx, configDesc) + if err != nil { + return nil, fmt.Errorf("failed to fetch config: %w", err) + } + defer rc.Close() + + data, err := io.ReadAll(rc) + if err != nil { + return nil, fmt.Errorf("failed to read config data: %w", err) + } + + var config ocispec.Image + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + return &config.Platform, nil + +} + +// GetPlatform return the platform information from manifest config +func GetPlatform(ctx context.Context, domain string, globalOptions types.GlobalCommandOptions, insecure bool, ref string, manifest interface{}) (*ocispec.Platform, error) { + resolver, err := CreateResolver(ctx, domain, globalOptions, insecure) + if err != nil { + return nil, fmt.Errorf("failed to create resolver: %w", err) + } + + if ociManifest, ok := manifest.(manifesttypes.OCIManifestStruct); ok { + if ociManifest.Config.Digest != "" { + platform, err := getPlatformFromConfig(ctx, resolver, ref, ociManifest.Config) + if err == nil && platform != nil { + return platform, nil + } + } + } + + if dockerManifest, ok := manifest.(manifesttypes.DockerManifestStruct); ok { + if dockerManifest.Config.Digest != "" { + platform, err := getPlatformFromConfig(ctx, resolver, ref, dockerManifest.Config) + if err == nil && platform != nil { + return platform, nil + } + } + } + + return &ocispec.Platform{}, nil +} From a663bd09eeb9bbedb2e5480819b5da2fbeb6073d Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 3 Aug 2025 10:10:39 +0800 Subject: [PATCH 156/868] manifest: add unit tests for nerdctl manifest create add unit tests for `nerdctl manifest create` Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_create_linux_test.go | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_create_linux_test.go diff --git a/cmd/nerdctl/manifest/manifest_create_linux_test.go b/cmd/nerdctl/manifest/manifest_create_linux_test.go new file mode 100644 index 00000000000..5873eb303d0 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_create_linux_test.go @@ -0,0 +1,135 @@ +/* + 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 manifest + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestManifestCreateErrors(t *testing.T) { + testCase := nerdtest.Setup() + manifestListName := "test-list:v1" + manifestName := "example.com/alpine:latest" + invalidName := "invalid/name/with/special@chars" + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("manifest", "create", manifestListName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "requires at least 2 arg", + }), + }, + { + Description: "invalid-list-name", + Command: test.Command("manifest", "create", invalidName, manifestName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "invalid reference format", + }), + }, + { + Description: "invalid-manifest-reference", + Command: test.Command("manifest", "create", manifestListName, invalidName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "invalid reference format", + }), + }, + } + + testCase.Run(t) +} + +func TestManifestCreate(t *testing.T) { + testCase := nerdtest.Setup() + manifestListName := "test-list-create:v1" + manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") + testCase.SubTests = []*test.Case{ + { + Description: "create-manifest-list", + Command: test.Command("manifest", "create", manifestListName, manifestRef), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "output": "Created manifest list ", + }), + }, + { + Description: "create-existed-manifest-list-without-amend-flag", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("manifest", "create", manifestListName+"-without-amend-flag", manifestRef) + cmd.Run(&test.Expected{ExitCode: 0}) + }, + Command: test.Command("manifest", "create", manifestListName+"-without-amend-flag", manifestRef), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "refusing to amend an existing manifest list with no --amend flag", + }), + }, + { + Description: "create-manifest-list-with-amend-flag", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("manifest", "create", manifestListName+"-with-amend-flag", manifestRef) + cmd.Run(&test.Expected{ExitCode: 0}) + }, + Command: test.Command("manifest", "create", "--amend", manifestListName+"-with-amend-flag", manifestRef), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "output": "Created manifest list", + }), + }, + } + + testCase.Run(t) +} From 6e23148bc36644e14792582836f2c88e86f578b9 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 3 Aug 2025 13:21:44 +0800 Subject: [PATCH 157/868] docs: add manifest create command reference add manifest create command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index cca699c3f2a..110f792732f 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -52,6 +52,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) - [Manifest management](#manifest-management) + - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) @@ -1039,6 +1040,23 @@ Flags: ## Manifest management +### :whale: nerdctl manifest create + +Create a local index/manifest list. + +Usage: `nerdctl manifest create [OPTIONS] INDEX/MANIFESTLIST MANIFEST [MANIFEST...]` + +Flags: + +- `--amend`: Amend the existing index/manifest list +- `--insecure`: Allow communication with an insecure registry + +Example: + +```bash +nerdctl manifest create myapp:latest alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f +``` + ### :whale: nerdctl manifest inspect Display the contents of a manifest list or manifest. From 8a29d0a0837c2f5725e617ea01d18bcc31469475 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 4 Aug 2025 15:55:51 +0800 Subject: [PATCH 158/868] manifest: support nerdctl manifest annotate command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest.go | 1 + cmd/nerdctl/manifest/manifest_annotate.go | 98 +++++++++++++++++++++++ pkg/api/types/manifest_types.go | 11 +++ pkg/cmd/manifest/annotate.go | 90 +++++++++++++++++++++ pkg/manifeststore/manifeststore.go | 14 ++++ 5 files changed, 214 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_annotate.go create mode 100644 pkg/cmd/manifest/annotate.go diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go index 201b72b14db..a2e574defd5 100644 --- a/cmd/nerdctl/manifest/manifest.go +++ b/cmd/nerdctl/manifest/manifest.go @@ -35,6 +35,7 @@ func Command() *cobra.Command { cmd.AddCommand( InspectCommand(), CreateCommand(), + AnnotateCommand(), ) return cmd diff --git a/cmd/nerdctl/manifest/manifest_annotate.go b/cmd/nerdctl/manifest/manifest_annotate.go new file mode 100644 index 00000000000..8649bf41dfa --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_annotate.go @@ -0,0 +1,98 @@ +/* + 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 manifest + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" +) + +func AnnotateCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "annotate INDEX/MANIFESTLIST MANIFEST", + Short: "Add additional information to a local image manifest", + Args: cobra.ExactArgs(2), + RunE: annotateAction, + ValidArgsFunction: annotateShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().String("os", "", "Set operating system") + cmd.Flags().String("arch", "", "Set architecture") + cmd.Flags().String("os-version", "", "Set operating system version") + cmd.Flags().String("variant", "", "Set operating system feature") + cmd.Flags().StringArray("os-features", []string{}, "Set architecture variant") + return cmd +} + +func processAnnotateFlags(cmd *cobra.Command) (types.ManifestAnnotateOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + + os, err := cmd.Flags().GetString("os") + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + arch, err := cmd.Flags().GetString("arch") + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + osVersion, err := cmd.Flags().GetString("os-version") + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + variant, err := cmd.Flags().GetString("variant") + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + osFeatures, err := cmd.Flags().GetStringArray("os-features") + if err != nil { + return types.ManifestAnnotateOptions{}, err + } + + return types.ManifestAnnotateOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Os: os, + Arch: arch, + OsVersion: osVersion, + Variant: variant, + OsFeatures: osFeatures, + }, nil +} + +func annotateAction(cmd *cobra.Command, args []string) error { + annotateOptions, err := processAnnotateFlags(cmd) + if err != nil { + return err + } + + listRef := args[0] + manifestRef := args[1] + + return manifest.Annotate(cmd.Context(), listRef, manifestRef, annotateOptions) +} + +func annotateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/manifest_types.go b/pkg/api/types/manifest_types.go index 9dd6bfa1cf8..92a6f8634a5 100644 --- a/pkg/api/types/manifest_types.go +++ b/pkg/api/types/manifest_types.go @@ -18,6 +18,17 @@ package types import "io" +// ManifestAnnotateOptions specifies options for `nerdctl manifest annotate`. +type ManifestAnnotateOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + Os string + Arch string + OsVersion string + Variant string + OsFeatures []string +} + // ManifestCreateOptions specifies options for `nerdctl manifest create`. type ManifestCreateOptions struct { Stdout io.Writer diff --git a/pkg/cmd/manifest/annotate.go b/pkg/cmd/manifest/annotate.go new file mode 100644 index 00000000000..66e74f693bb --- /dev/null +++ b/pkg/cmd/manifest/annotate.go @@ -0,0 +1,90 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifest + +import ( + "context" + "errors" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/manifeststore" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/store" +) + +func Annotate(ctx context.Context, listRef string, manifestRef string, options types.ManifestAnnotateOptions) error { + parsedListRef, err := referenceutil.Parse(listRef) + if err != nil { + return fmt.Errorf("failed to parse list reference: %w", err) + } + + parsedManifestRef, err := referenceutil.Parse(manifestRef) + if err != nil { + return fmt.Errorf("failed to parse manifest reference: %w", err) + } + + manifestStore, err := manifeststore.NewStore(options.GOptions.DataRoot) + if err != nil { + return fmt.Errorf("failed to create manifest store: %w", err) + } + + imageManifest, err := manifestStore.Get(parsedListRef, parsedManifestRef) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return fmt.Errorf("manifest for image %s does not exist in %s", manifestRef, listRef) + } + return fmt.Errorf("failed to get manifest: %w", err) + } + + if imageManifest.Descriptor.Platform == nil { + imageManifest.Descriptor.Platform = new(ocispec.Platform) + } + + if options.Os != "" { + imageManifest.Descriptor.Platform.OS = options.Os + } + + if options.Arch != "" { + imageManifest.Descriptor.Platform.Architecture = options.Arch + } + + if options.Variant != "" { + imageManifest.Descriptor.Platform.Variant = options.Variant + } + + if options.OsVersion != "" { + imageManifest.Descriptor.Platform.OSVersion = options.OsVersion + } + + for _, osFeature := range options.OsFeatures { + imageManifest.Descriptor.Platform.OSFeatures = appendIfUnique(imageManifest.Descriptor.Platform.OSFeatures, osFeature) + } + + return manifestStore.Save(parsedListRef, parsedManifestRef, imageManifest) +} + +func appendIfUnique(list []string, str string) []string { + for _, s := range list { + if s == str { + return list + } + } + return append(list, str) +} diff --git a/pkg/manifeststore/manifeststore.go b/pkg/manifeststore/manifeststore.go index 6681caedc5d..6457052b7ab 100644 --- a/pkg/manifeststore/manifeststore.go +++ b/pkg/manifeststore/manifeststore.go @@ -28,6 +28,7 @@ import ( ) type Store interface { + Get(listRef *referenceutil.ImageReference, manifestRef *referenceutil.ImageReference) (*manifesttypes.DockerManifestEntry, error) // GetList returns all the local manifests for a index or manifest list GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error) // Save saves a manifest as part of a index or local manifest list @@ -47,6 +48,19 @@ func NewStore(dataRoot string) (Store, error) { return &manifestStore{store: st}, nil } +func (s *manifestStore) Get(listRef *referenceutil.ImageReference, manifestRef *referenceutil.ImageReference) (*manifesttypes.DockerManifestEntry, error) { + var manifest *manifesttypes.DockerManifestEntry + err := s.store.WithLock(func() error { + listPath := makeFilesafeName(listRef.String()) + manifestPath := makeFilesafeName(manifestRef.String()) + + var err error + manifest, err = s.getManifestFromPath(listPath, manifestPath) + return err + }) + return manifest, err +} + func (s *manifestStore) GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error) { listPath := makeFilesafeName(listRef.String()) From dbf3752b7ea399d4131f8feaa09da7c71d1d4404 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 4 Aug 2025 15:55:51 +0800 Subject: [PATCH 159/868] manifest: add unit tests for nerdctl manifest annotate command add unit tests for nerdctl manifest annotate command Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_annotate_linux_test.go | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_annotate_linux_test.go diff --git a/cmd/nerdctl/manifest/manifest_annotate_linux_test.go b/cmd/nerdctl/manifest/manifest_annotate_linux_test.go new file mode 100644 index 00000000000..fda04cad3c2 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_annotate_linux_test.go @@ -0,0 +1,121 @@ +/* + 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 manifest + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestManifestAnnotateErrors(t *testing.T) { + testCase := nerdtest.Setup() + manifestListName := "test-list:v1" + manifestName := "example.com/alpine:latest" + invalidName := "invalid/name/with/special@chars" + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("manifest", "annotate", manifestListName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "invalid-list-name", + Command: test.Command("manifest", "annotate", invalidName, manifestName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "invalid reference format", + }), + }, + { + Description: "invalid-manifest-reference", + Command: test.Command("manifest", "annotate", manifestListName, invalidName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "invalid reference format", + }), + }, + } + + testCase.Run(t) +} + +func TestManifestAnnotate(t *testing.T) { + testCase := nerdtest.Setup() + manifestListName := "example.com/test-list-annotate:v1" + manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") + + testCase.SubTests = []*test.Case{ + { + Description: "annotate-non-existent-manifest", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("manifest", "create", manifestListName, manifestRef) + cmd.Run(&test.Expected{ExitCode: 0}) + }, + Command: test.Command("manifest", "annotate", manifestListName, "example.com/fake:0.0"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "manifest for image example.com/fake:0.0 does not exist", + }), + }, + { + Description: "annotate-success", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("manifest", "create", manifestListName+"-success", manifestRef) + cmd.Run(&test.Expected{ExitCode: 0}) + }, + Command: test.Command("manifest", "annotate", + manifestListName+"-success", + manifestRef, + "--os", "freebsd", + "--arch", "arm", + "--os-version", "1", + "--os-features", "feature1", + "--variant", "v7"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + } + + testCase.Run(t) +} From 7ac8397eb268686d2edc0a009060096a9e52c346 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 4 Aug 2025 17:05:59 +0800 Subject: [PATCH 160/868] docs: add manifest annotate command reference add manifest annotate command reference Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index 110f792732f..a1506fadc53 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -52,6 +52,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) - [Manifest management](#manifest-management) + - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) - [Registry](#registry) @@ -1040,6 +1041,27 @@ Flags: ## Manifest management +### :whale: nerdctl manifest annotate + +Add additional information to a local image manifest. + +Usage: `nerdctl manifest annotate [OPTIONS] INDEX/MANIFESTLIST MANIFEST` + +Flags: + +- :whale: `--os`: Set operating system (e.g., "linux", "windows", "freebsd") +- :whale: `--arch`: Set architecture (e.g., "amd64", "arm64", "arm") +- :whale: `--os-version`: Set operating system version (e.g., "10.0.19041") +- :whale: `--variant`: Set architecture variant (e.g., "v7", "v8") +- :whale: `--os-features`: Set operating system features (e.g., "win32k") + +Examples: + +```bash +nerdctl manifest annotate myapp:latest alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f \ + --os linux --arch arm --variant v7 --os-features feature1,feature2 +``` + ### :whale: nerdctl manifest create Create a local index/manifest list. From 12a61d380a7969b3ecf95f87909970c0142d07a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 07:22:33 +0000 Subject: [PATCH 161/868] build(deps): bump docker/login-action from 3.4.0 to 3.5.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/74a5d142397b4f367a81961eba4e8cd7edddf772...184bdaa0721073962dff0199f1fb9940f07167d1) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 14fe15e225a..9c5e0564986 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From b334950940e5a8f49b7b27f5526ff296f18cf982 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 5 Aug 2025 19:12:37 +0800 Subject: [PATCH 162/868] manifest: support nerdctl manifest rm command support nerdctl manifest rm command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest.go | 1 + cmd/nerdctl/manifest/manifest_remove.go | 59 +++++++++++++++++++++++++ pkg/cmd/manifest/rm.go | 51 +++++++++++++++++++++ pkg/manifeststore/manifeststore.go | 9 ++++ pkg/manifestutil/manifestutils.go | 14 ++++++ pkg/testutil/images.yaml | 2 + 6 files changed, 136 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_remove.go create mode 100644 pkg/cmd/manifest/rm.go diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go index a2e574defd5..69010c299ab 100644 --- a/cmd/nerdctl/manifest/manifest.go +++ b/cmd/nerdctl/manifest/manifest.go @@ -36,6 +36,7 @@ func Command() *cobra.Command { InspectCommand(), CreateCommand(), AnnotateCommand(), + RemoveCommand(), ) return cmd diff --git a/cmd/nerdctl/manifest/manifest_remove.go b/cmd/nerdctl/manifest/manifest_remove.go new file mode 100644 index 00000000000..0aabdbd3a26 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_remove.go @@ -0,0 +1,59 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifest + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" +) + +func RemoveCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]", + Short: "Remove one or more index/manifest lists", + Args: cobra.MinimumNArgs(1), + RunE: removeAction, + ValidArgsFunction: removeShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + return cmd +} + +func removeAction(cmd *cobra.Command, refs []string) error { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return err + } + var errs []error + for _, ref := range refs { + err := manifest.Remove(cmd.Context(), ref, globalOptions) + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/cmd/manifest/rm.go b/pkg/cmd/manifest/rm.go new file mode 100644 index 00000000000..191e56dd4ff --- /dev/null +++ b/pkg/cmd/manifest/rm.go @@ -0,0 +1,51 @@ +/* + 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 manifest + +import ( + "context" + "fmt" + "strings" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/manifeststore" + "github.com/containerd/nerdctl/v2/pkg/manifestutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +func Remove(ctx context.Context, ref string, options types.GlobalCommandOptions) error { + parsedRef, err := referenceutil.Parse(ref) + if err != nil { + return fmt.Errorf("failed to parse reference: %w", err) + } + manifestStore, err := manifeststore.NewStore(options.DataRoot) + if err != nil { + return fmt.Errorf("failed to create manifest store: %w", err) + } + _, err = manifestStore.GetList(parsedRef) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return manifestutil.NewNoSuchManifestError(parsedRef.String()) + } + return err + } + err = manifestStore.Remove(parsedRef) + if err != nil { + return fmt.Errorf("failed to remove manifest list: %w", err) + } + return nil +} diff --git a/pkg/manifeststore/manifeststore.go b/pkg/manifeststore/manifeststore.go index 6457052b7ab..252c74a8f0f 100644 --- a/pkg/manifeststore/manifeststore.go +++ b/pkg/manifeststore/manifeststore.go @@ -33,6 +33,8 @@ type Store interface { GetList(listRef *referenceutil.ImageReference) ([]*manifesttypes.DockerManifestEntry, error) // Save saves a manifest as part of a index or local manifest list Save(listRef, manifestRef *referenceutil.ImageReference, manifest *manifesttypes.DockerManifestEntry) error + // Remove removes a index or local manifest list + Remove(listRef *referenceutil.ImageReference) error } type manifestStore struct { @@ -103,6 +105,13 @@ func (s *manifestStore) Save(listRef, manifestRef *referenceutil.ImageReference, }) } +func (s *manifestStore) Remove(listRef *referenceutil.ImageReference) error { + return s.store.WithLock(func() error { + listPath := makeFilesafeName(listRef.String()) + return s.store.Delete(listPath) + }) +} + func (s *manifestStore) getManifestFromPath(listPath, manifestPath string) (*manifesttypes.DockerManifestEntry, error) { data, err := s.store.Get(listPath, manifestPath) if err != nil { diff --git a/pkg/manifestutil/manifestutils.go b/pkg/manifestutil/manifestutils.go index 978084f6793..acf4e385db1 100644 --- a/pkg/manifestutil/manifestutils.go +++ b/pkg/manifestutil/manifestutils.go @@ -45,6 +45,20 @@ var manifestParsers = map[string]manifestParser{ ocispec.MediaTypeImageIndex: parseOCIIndex, } +// NoSuchManifestError represents an error when a manifest is not found +type NoSuchManifestError struct { + Ref string +} + +func (e *NoSuchManifestError) Error() string { + return fmt.Sprintf("No such manifest: %s", e.Ref) +} + +// NewNoSuchManifestError creates a new NoSuchManifestError +func NewNoSuchManifestError(ref string) error { + return &NoSuchManifestError{Ref: ref} +} + // ParseManifest parses manifest data based on media type func ParseManifest(mediaType string, data []byte) (interface{}, error) { if parser, exists := manifestParsers[mediaType]; exists { diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 405ba8fd3f3..73bac34ea3a 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -15,6 +15,8 @@ alpine: manifest: "sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310" config: "sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c" raw: "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6NDlmMzU2ZmE0NTEzNjc2YzVlMjJlM2E4NDA0YWFkNmM3MjYyY2M3YWFlZDE1MzQxNDU4MjY1MzIwNzg2YzU4YyIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI4MTE5NDcsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OmNhM2NkNDJhN2M5NTI1ZjZjZTNkNjRjMWE3MDk4MjYxM2E4MjM1ZjBjYzA1N2VjOTI0NDA1MjkyMTg1M2VmMTUiCiAgICAgIH0KICAgXQp9" + linux/arm64: + manifest: "sha256:071fa5de01a240dbef5be09d69f8fef2f89d68445d9175393773ee389b6f5935" busybox: ref: "ghcr.io/containerd/busybox" From 385c0be694b2c1388dbecdec7d911d153e4c3783 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 5 Aug 2025 19:31:12 +0800 Subject: [PATCH 163/868] manifest: add unit tests for nerdctl manifest rm command add unit tests for nerdctl manifest rm command Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_remove_linux_test.go | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_remove_linux_test.go diff --git a/cmd/nerdctl/manifest/manifest_remove_linux_test.go b/cmd/nerdctl/manifest/manifest_remove_linux_test.go new file mode 100644 index 00000000000..ca8fcd72969 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_remove_linux_test.go @@ -0,0 +1,68 @@ +/* + 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 manifest + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestManifestsRemove(t *testing.T) { + testCase := nerdtest.Setup() + manifestListName1 := "example.com/test-list-remove:v1" + manifestListName2 := "example.com/test-list-remove:v2" + manifestRef1 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") + manifestRef2 := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/arm64") + + testCase.SubTests = []*test.Case{ + { + Description: "remove-several-manifestlists", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("manifest", "create", manifestListName1, manifestRef1) + cmd.Run(&test.Expected{ExitCode: 0}) + cmd = helpers.Command("manifest", "create", manifestListName2, manifestRef2) + cmd.Run(&test.Expected{ExitCode: 0}) + }, + Command: test.Command("manifest", "rm", manifestListName1, manifestListName2), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + { + Description: "remove-non-existent-manifestlist", + Command: test.Command("manifest", "rm", "example.com/non-existent:latest"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "No such manifest: example.com/non-existent:latest", + }), + }, + } + + testCase.Run(t) +} From 8690f739207c54b155e3af0f4f1a96ffe9123695 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 5 Aug 2025 20:02:40 +0800 Subject: [PATCH 164/868] docs: add nerdctl manifest rm reference add nerdctl manifest rm reference Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index a1506fadc53..fdbf101d27c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -55,6 +55,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) + - [:whale: nerdctl manifest rm](#whale-nerdctl-manifest-rm) - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) - [:whale: nerdctl logout](#whale-nerdctl-logout) @@ -1102,6 +1103,18 @@ nerdctl manifest inspect alpine:3.22.1 nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f ``` +### :whale: nerdctl manifest rm + +Remove one or more index/manifest lists. + +Usage: `nerdctl manifest rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]` + +Example: + +```bash +nerdctl manifest rm alpine:3.22.1 alpine:3.22.2 +``` + ## Registry ### :whale: nerdctl login From 3e50703e2f7a32a01106d4b626c59ee88c1f1bb4 Mon Sep 17 00:00:00 2001 From: Craig Loewen Date: Tue, 5 Aug 2025 11:14:55 -0400 Subject: [PATCH 165/868] feat: Added export command Signed-off-by: Craig Loewen --- cmd/nerdctl/container/container.go | 1 + cmd/nerdctl/container/container_export.go | 94 ++++++++++ .../container/container_export_test.go | 170 ++++++++++++++++++ cmd/nerdctl/main.go | 1 + docs/command-reference.md | 9 +- pkg/api/types/container_types.go | 7 + pkg/cmd/container/export.go | 151 ++++++++++++++++ 7 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 cmd/nerdctl/container/container_export.go create mode 100644 cmd/nerdctl/container/container_export_test.go create mode 100644 pkg/cmd/container/export.go diff --git a/cmd/nerdctl/container/container.go b/cmd/nerdctl/container/container.go index 6188e7013a0..1696874be01 100644 --- a/cmd/nerdctl/container/container.go +++ b/cmd/nerdctl/container/container.go @@ -55,6 +55,7 @@ func Command() *cobra.Command { StatsCommand(), AttachCommand(), HealthCheckCommand(), + ExportCommand(), ) AddCpCommand(cmd) return cmd diff --git a/cmd/nerdctl/container/container_export.go b/cmd/nerdctl/container/container_export.go new file mode 100644 index 00000000000..d7fc7abc580 --- /dev/null +++ b/cmd/nerdctl/container/container_export.go @@ -0,0 +1,94 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "fmt" + "os" + + "github.com/mattn/go-isatty" + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/container" +) + +func ExportCommand() *cobra.Command { + var exportCommand = &cobra.Command{ + Use: "export [OPTIONS] CONTAINER", + Args: cobra.ExactArgs(1), + Short: "Export a containers filesystem as a tar archive", + Long: "Export a containers filesystem as a tar archive", + RunE: exportAction, + ValidArgsFunction: exportShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + exportCommand.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") + + return exportCommand +} + +func exportAction(cmd *cobra.Command, args []string) error { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return err + } + if len(args) == 0 { + return fmt.Errorf("requires at least 1 argument") + } + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) + if err != nil { + return err + } + defer cancel() + + writer := cmd.OutOrStdout() + if output != "" { + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + writer = f + } else { + if isatty.IsTerminal(os.Stdout.Fd()) { + return fmt.Errorf("cowardly refusing to save to a terminal. Use the -o flag or redirect") + } + } + + options := types.ContainerExportOptions{ + Stdout: writer, + GOptions: globalOptions, + } + + return container.Export(ctx, client, args[0], options) +} + +func exportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show container names + return completion.ContainerNames(cmd, nil) +} diff --git a/cmd/nerdctl/container/container_export_test.go b/cmd/nerdctl/container/container_export_test.go new file mode 100644 index 00000000000..ee25fe2dc9d --- /dev/null +++ b/cmd/nerdctl/container/container_export_test.go @@ -0,0 +1,170 @@ +/* + 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 ( + "archive/tar" + "io" + "os" + "path/filepath" + "runtime" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// validateExportedTar checks that the tar file exists and contains /bin/busybox +func validateExportedTar(outFile string) test.Comparator { + return func(stdout string, t tig.T) { + // Check if the tar file was created + _, err := os.Stat(outFile) + assert.Assert(t, !os.IsNotExist(err), "exported tar file %s was not created", outFile) + + // Open and read the tar file to check for /bin/busybox + file, err := os.Open(outFile) + assert.NilError(t, err, "failed to open tar file %s", outFile) + defer file.Close() + + tarReader := tar.NewReader(file) + busyboxFound := false + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + assert.NilError(t, err, "failed to read tar entry") + + if header.Name == "bin/busybox" || header.Name == "./bin/busybox" { + busyboxFound = true + break + } + } + + assert.Assert(t, busyboxFound, "exported tar file %s does not contain /bin/busybox", outFile) + t.Log("Export validation passed: tar file exists and contains /bin/busybox") + } +} + +func TestExportStoppedContainer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("export is not supported on Windows") + } + + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier("container") + helpers.Ensure("create", "--name", identifier, testutil.CommonImage) + data.Labels().Set("cID", identifier) + data.Labels().Set("outFile", filepath.Join(os.TempDir(), identifier+".tar")) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Labels().Get("cID")) + helpers.Anyhow("rm", "-f", data.Labels().Get("cID")) + os.Remove(data.Labels().Get("outFile")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "export command succeeds", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("export", "-o", data.Labels().Get("outFile"), data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "tar file exists and has content", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Use a simple command that always succeeds to trigger the validation + return helpers.Custom("echo", "validating tar file") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: validateExportedTar(data.Labels().Get("outFile")), + } + }, + }, + } + + testCase.Run(t) +} + +func TestExportRunningContainer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("export is not supported on Windows") + } + + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier("container") + helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) + data.Labels().Set("cID", identifier) + data.Labels().Set("outFile", filepath.Join(os.TempDir(), identifier+".tar")) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("cID")) + os.Remove(data.Labels().Get("outFile")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "export command succeeds", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("export", "-o", data.Labels().Get("outFile"), data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "tar file exists and has content", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Use a simple command that always succeeds to trigger the validation + return helpers.Custom("echo", "validating tar file") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: validateExportedTar(data.Labels().Get("outFile")), + } + }, + }, + } + + testCase.Run(t) +} + +func TestExportNonexistentContainer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("export is not supported on Windows") + } + + testCase := nerdtest.Setup() + testCase.Command = test.Command("export", "nonexistent-container") + testCase.Expected = test.Expects(1, nil, nil) + + testCase.Run(t) +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 55cc12c9bd6..019271b47d1 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -287,6 +287,7 @@ Config file ($NERDCTL_TOML): %s container.PauseCommand(), container.UnpauseCommand(), container.CommitCommand(), + container.ExportCommand(), container.WaitCommand(), container.RenameCommand(), container.AttachCommand(), diff --git a/docs/command-reference.md b/docs/command-reference.md index d2e82e8a0ea..d9156827030 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -34,6 +34,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl attach](#whale-nerdctl-attach) - [:whale: nerdctl container prune](#whale-nerdctl-container-prune) - [:whale: nerdctl diff](#whale-nerdctl-diff) + - [:whale: nerdctl export](#whale-nerdctl-export) - [Build](#build) - [:whale: nerdctl build](#whale-nerdctl-build) - [:whale: nerdctl commit](#whale-nerdctl-commit) @@ -719,6 +720,12 @@ Inspect changes to files or directories on a container's filesystem Usage: `nerdctl diff CONTAINER` +### :whale: nerdctl export + +Export a containers filesystem as a tar archive. + +Usage: `nerdctl export CONTAINER` + ## Build ### :whale: nerdctl build @@ -1814,7 +1821,7 @@ Container management: Image: -- `docker export` and `docker import` +- `docker import` - `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign|notation` and `nerdctl push --sign=cosign|notation`. See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md).) - `docker manifest *` diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 4583e44d733..3e157bb303d 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -44,6 +44,13 @@ type ContainerKillOptions struct { KillSignal string } +// ContainerExportOptions specifies options for `nerdctl (container) export`. +type ContainerExportOptions struct { + Stdout io.Writer + // GOptions is the global options + GOptions GlobalCommandOptions +} + // ContainerCreateOptions specifies options for `nerdctl (container) create` and `nerdctl (container) run`. type ContainerCreateOptions struct { Stdout io.Writer diff --git a/pkg/cmd/container/export.go b/pkg/cmd/container/export.go new file mode 100644 index 00000000000..57d457cd239 --- /dev/null +++ b/pkg/cmd/container/export.go @@ -0,0 +1,151 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "fmt" + "os" + "runtime" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/pkg/archive" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +// Export exports a container's filesystem as a tar archive +func Export(ctx context.Context, client *containerd.Client, containerReq string, options types.ContainerExportOptions) error { + if runtime.GOOS == "windows" { + return fmt.Errorf("export command is not supported on Windows") + } + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + return exportContainer(ctx, client, found.Container, options) + }, + } + + n, err := walker.Walk(ctx, containerReq) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("no such container %s", containerReq) + } + return nil +} + +func exportContainer(ctx context.Context, client *containerd.Client, container containerd.Container, options types.ContainerExportOptions) error { + // Get container info to access the snapshot + conInfo, err := container.Info(ctx) + if err != nil { + return fmt.Errorf("failed to get container info: %w", err) + } + + // Use the container's snapshot service to get mounts + // This works for both running and stopped containers + sn := client.SnapshotService(conInfo.Snapshotter) + mounts, err := sn.Mounts(ctx, container.ID()) + if err != nil { + return fmt.Errorf("failed to get container mounts: %w", err) + } + + // Create a temporary directory to mount the snapshot + tempDir, err := os.MkdirTemp("", "nerdctl-export-") + if err != nil { + return fmt.Errorf("failed to create temporary mount directory: %w", err) + } + defer os.RemoveAll(tempDir) + + // Mount the container's filesystem + err = mount.All(mounts, tempDir) + if err != nil { + return fmt.Errorf("failed to mount container snapshot: %w", err) + } + defer func() { + if unmountErr := mount.Unmount(tempDir, 0); unmountErr != nil { + log.G(ctx).WithError(unmountErr).Warn("Failed to unmount snapshot") + } + }() + + log.G(ctx).Debugf("Mounted container snapshot at %s", tempDir) + + // Create tar archive using WriteDiff + return createTarArchiveWithWriteDiff(ctx, tempDir, options) +} + +func createTarArchiveWithWriteDiff(ctx context.Context, rootPath string, options types.ContainerExportOptions) error { + // Create a temporary empty directory to use as the "before" state for WriteDiff + emptyDir, err := os.MkdirTemp("", "nerdctl-export-empty-") + if err != nil { + return fmt.Errorf("failed to create temporary empty directory: %w", err) + } + defer os.RemoveAll(emptyDir) + + // Debug logging + log.G(ctx).Debugf("Using WriteDiff to export container filesystem from %s", rootPath) + log.G(ctx).Debugf("Empty directory: %s", emptyDir) + log.G(ctx).Debugf("Output writer type: %T", options.Stdout) + + // Check if the rootPath directory exists and has contents + if entries, err := os.ReadDir(rootPath); err != nil { + log.G(ctx).Debugf("Failed to read rootPath directory %s: %v", rootPath, err) + } else { + log.G(ctx).Debugf("RootPath %s contains %d entries", rootPath, len(entries)) + for i, entry := range entries { + if i < 10 { // Only log first 10 entries to avoid spam + log.G(ctx).Debugf(" - %s (dir: %v)", entry.Name(), entry.IsDir()) + } + } + if len(entries) > 10 { + log.G(ctx).Debugf(" ... and %d more entries", len(entries)-10) + } + } + + // Double check that emptyDir is empty + if entries, err := os.ReadDir(emptyDir); err != nil { + log.G(ctx).Debugf("Failed to read emptyDir directory %s: %v", emptyDir, err) + } else { + log.G(ctx).Debugf("EmptyDir %s contains %d entries", emptyDir, len(entries)) + for i, entry := range entries { + if i < 10 { // Only log first 10 entries to avoid spam + log.G(ctx).Debugf(" - %s (dir: %v)", entry.Name(), entry.IsDir()) + } + } + if len(entries) > 10 { + log.G(ctx).Debugf(" ... and %d more entries", len(entries)-10) + } + } + + // Use WriteDiff to create a tar stream comparing the container rootfs (rootPath) + // with an empty directory (emptyDir). This produces a complete export of the container. + err = archive.WriteDiff(ctx, options.Stdout, emptyDir, rootPath) + if err != nil { + return fmt.Errorf("failed to write tar diff: %w", err) + } + + log.G(ctx).Debugf("WriteDiff completed successfully") + + return nil +} From c56c49d45db82f159f64dc04fa535ba20997132f Mon Sep 17 00:00:00 2001 From: Ruihua Wen Date: Fri, 1 Aug 2025 17:21:51 +0900 Subject: [PATCH 166/868] feat: add --format flag to namespace ls command Signed-off-by: Ruihua Wen --- cmd/nerdctl/namespace/namespace.go | 97 ------------- cmd/nerdctl/namespace/namespace_inspect.go | 1 - cmd/nerdctl/namespace/namespace_list.go | 76 ++++++++++ cmd/nerdctl/namespace/namespace_remove.go | 1 - cmd/nerdctl/namespace/namespace_update.go | 1 - docs/command-reference.md | 1 + pkg/api/types/namespace_types.go | 10 ++ pkg/cmd/namespace/list.go | 153 +++++++++++++++++++++ 8 files changed, 240 insertions(+), 100 deletions(-) create mode 100644 cmd/nerdctl/namespace/namespace_list.go create mode 100644 pkg/cmd/namespace/list.go diff --git a/cmd/nerdctl/namespace/namespace.go b/cmd/nerdctl/namespace/namespace.go index 133e63cd6f1..0e88c8a4e17 100644 --- a/cmd/nerdctl/namespace/namespace.go +++ b/cmd/nerdctl/namespace/namespace.go @@ -17,19 +17,9 @@ package namespace import ( - "fmt" - "sort" - "strings" - "text/tabwriter" - "github.com/spf13/cobra" - "github.com/containerd/containerd/v2/pkg/namespaces" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/clientutil" - "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" ) func Command() *cobra.Command { @@ -50,90 +40,3 @@ func Command() *cobra.Command { cmd.AddCommand(inspectCommand()) return cmd } - -func listCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List containerd namespaces", - RunE: listAction, - SilenceUsage: true, - SilenceErrors: true, - } - cmd.Flags().BoolP("quiet", "q", false, "Only display names") - return cmd -} - -func listAction(cmd *cobra.Command, args []string) error { - globalOptions, err := helpers.ProcessRootCmdFlags(cmd) - if err != nil { - return err - } - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if err != nil { - return err - } - defer cancel() - - nsService := client.NamespaceService() - nsList, err := nsService.List(ctx) - if err != nil { - return err - } - quiet, err := cmd.Flags().GetBool("quiet") - if err != nil { - return err - } - if quiet { - for _, ns := range nsList { - fmt.Fprintln(cmd.OutOrStdout(), ns) - } - return nil - } - dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) - if err != nil { - return err - } - - w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) - // no "NETWORKS", because networks are global objects - fmt.Fprintln(w, "NAME\tCONTAINERS\tIMAGES\tVOLUMES\tLABELS") - for _, ns := range nsList { - ctx = namespaces.WithNamespace(ctx, ns) - var numContainers, numImages, numVolumes int - var labelStrings []string - - containers, err := client.Containers(ctx) - if err != nil { - log.L.Warn(err) - } - numContainers = len(containers) - - images, err := client.ImageService().List(ctx) - if err != nil { - log.L.Warn(err) - } - numImages = len(images) - - volStore, err := volumestore.New(dataStore, ns) - if err != nil { - log.L.Warn(err) - } else { - numVolumes, err = volStore.Count() - if err != nil { - log.L.Warn(err) - } - } - - labels, err := client.NamespaceService().Labels(ctx, ns) - if err != nil { - return err - } - for k, v := range labels { - labelStrings = append(labelStrings, strings.Join([]string{k, v}, "=")) - } - sort.Strings(labelStrings) - fmt.Fprintf(w, "%s\t%d\t%d\t%d\t%v\t\n", ns, numContainers, numImages, numVolumes, strings.Join(labelStrings, ",")) - } - return w.Flush() -} diff --git a/cmd/nerdctl/namespace/namespace_inspect.go b/cmd/nerdctl/namespace/namespace_inspect.go index b79868dbb48..57c3ba5a197 100644 --- a/cmd/nerdctl/namespace/namespace_inspect.go +++ b/cmd/nerdctl/namespace/namespace_inspect.go @@ -75,6 +75,5 @@ func inspectAction(cmd *cobra.Command, args []string) error { } func namespaceInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/cmd/nerdctl/namespace/namespace_list.go b/cmd/nerdctl/namespace/namespace_list.go new file mode 100644 index 00000000000..d1c81dd1713 --- /dev/null +++ b/cmd/nerdctl/namespace/namespace_list.go @@ -0,0 +1,76 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package namespace + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/namespace" +) + +func listCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List containerd namespaces", + RunE: listAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().BoolP("quiet", "q", false, "Only display names") + cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'") + return cmd +} + +func listOptions(cmd *cobra.Command) (types.NamespaceListOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.NamespaceListOptions{}, err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return types.NamespaceListOptions{}, err + } + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return types.NamespaceListOptions{}, err + } + return types.NamespaceListOptions{ + GOptions: globalOptions, + Format: format, + Quiet: quiet, + Stdout: cmd.OutOrStdout(), + }, nil +} + +func listAction(cmd *cobra.Command, args []string) error { + options, err := listOptions(cmd) + if err != nil { + return err + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return namespace.List(ctx, client, options) +} diff --git a/cmd/nerdctl/namespace/namespace_remove.go b/cmd/nerdctl/namespace/namespace_remove.go index 3ce29a5741f..5206b5e7ded 100644 --- a/cmd/nerdctl/namespace/namespace_remove.go +++ b/cmd/nerdctl/namespace/namespace_remove.go @@ -73,6 +73,5 @@ func removeAction(cmd *cobra.Command, args []string) error { } func namespaceRemoveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/cmd/nerdctl/namespace/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go index a15a865bde1..dd15cd91a49 100644 --- a/cmd/nerdctl/namespace/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -71,6 +71,5 @@ func updateAction(cmd *cobra.Command, args []string) error { } func namespaceUpdateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show namespace names return completion.NamespaceNames(cmd, args, toComplete) } diff --git a/docs/command-reference.md b/docs/command-reference.md index 110f792732f..a2380887e18 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1289,6 +1289,7 @@ Usage: `nerdctl namespace ls [OPTIONS]` Flags: - `-q, --quiet`: Only display namespace names +- `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}` ### :nerd_face: :blue_square: nerdctl namespace remove diff --git a/pkg/api/types/namespace_types.go b/pkg/api/types/namespace_types.go index c3e8d2c4b08..23b7814dd9e 100644 --- a/pkg/api/types/namespace_types.go +++ b/pkg/api/types/namespace_types.go @@ -43,3 +43,13 @@ type NamespaceInspectOptions struct { // Format the output using the given Go template, e.g, '{{json .}}' Format string } + +// NamespaceListOptions specifies options for `nerdctl namespace ls`. +type NamespaceListOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Format the output using the given Go template, e.g, '{{json .}}' + Format string + // Quiet suppresses extra information and only prints namespace names + Quiet bool +} diff --git a/pkg/cmd/namespace/list.go b/pkg/cmd/namespace/list.go new file mode 100644 index 00000000000..c01fb04c058 --- /dev/null +++ b/pkg/cmd/namespace/list.go @@ -0,0 +1,153 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package namespace + +import ( + "bytes" + "context" + "errors" + "fmt" + "sort" + "strings" + "text/tabwriter" + "text/template" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" +) + +func List(ctx context.Context, client *containerd.Client, options types.NamespaceListOptions) error { + nsStore := client.NamespaceService() + nsList, err := nsStore.List(ctx) + if err != nil { + return err + } + + dataStore, err := clientutil.DataStore(options.GOptions.DataRoot, options.GOptions.Address) + if err != nil { + return err + } + + w := options.Stdout + var tmpl *template.Template + namespaceList := []namespace{} + for _, ns := range nsList { + ctx = namespaces.WithNamespace(ctx, ns) + var numContainers, numImages, numVolumes int + + containers, err := client.Containers(ctx) + if err != nil { + log.L.Warn(err) + } + numContainers = len(containers) + + images, err := client.ImageService().List(ctx) + if err != nil { + log.L.Warn(err) + } + numImages = len(images) + + volStore, err := volumestore.New(dataStore, ns) + if err != nil { + log.L.Warn(err) + } else { + numVolumes, err = volStore.Count() + if err != nil { + log.L.Warn(err) + } + } + + labels, err := client.NamespaceService().Labels(ctx, ns) + if err != nil { + return err + } + namespaceList = append(namespaceList, namespace{ + Name: ns, + Containers: numContainers, + Images: numImages, + Volumes: numVolumes, + Labels: labels, + }) + } + + switch options.Format { + case "", "table", "wide": + if !options.Quiet { + w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) + // no "NETWORKS", because networks are global objects + fmt.Fprintln(w, "NAME\tCONTAINERS\tIMAGES\tVOLUMES\tLABELS") + } + case "raw": + return errors.New("unsupported format: \"raw\"") + default: + if options.Quiet { + return errors.New("format and quiet must not be specified together") + } + var err error + tmpl, err = formatter.ParseTemplate(options.Format) + if err != nil { + return err + } + } + + for _, namespace := range namespaceList { + if tmpl != nil { + var b bytes.Buffer + if err := tmpl.Execute(&b, namespace); err != nil { + return err + } + if _, err := fmt.Fprintln(w, b.String()); err != nil { + return err + } + } else if options.Quiet { + if _, err := fmt.Fprintln(w, namespace.Name); err != nil { + return err + } + } else { + format := "%s\t%d\t%d\t%d\t%v\t\n" + var labelStrings []string + for k, v := range namespace.Labels { + labelStrings = append(labelStrings, strings.Join([]string{k, v}, "=")) + } + sort.Strings(labelStrings) + args := []interface{}{} + args = append(args, namespace.Name, namespace.Containers, namespace.Images, namespace.Volumes, strings.Join(labelStrings, ",")) + if _, err := fmt.Fprintf(w, format, args...); err != nil { + return err + } + } + } + + if f, ok := w.(formatter.Flusher); ok { + return f.Flush() + } + return nil +} + +type namespace struct { + Name string + Containers int + Images int + Volumes int + Labels map[string]string +} From 8c80d4fb30ba0af345f4b1f3cc80588720c8037d Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 7 Aug 2025 10:51:05 +0800 Subject: [PATCH 167/868] manifest: support nerdctl manifest push command support nerdctl manifest push command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest.go | 1 + cmd/nerdctl/manifest/manifest_push.go | 80 +++++++++ pkg/api/types/manifest_types.go | 10 ++ pkg/cmd/manifest/push.go | 229 ++++++++++++++++++++++++++ pkg/manifesttypes/manifesttypes.go | 26 ++- 5 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 cmd/nerdctl/manifest/manifest_push.go create mode 100644 pkg/cmd/manifest/push.go diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go index 69010c299ab..50cecfd97e2 100644 --- a/cmd/nerdctl/manifest/manifest.go +++ b/cmd/nerdctl/manifest/manifest.go @@ -37,6 +37,7 @@ func Command() *cobra.Command { CreateCommand(), AnnotateCommand(), RemoveCommand(), + PushCommand(), ) return cmd diff --git a/cmd/nerdctl/manifest/manifest_push.go b/cmd/nerdctl/manifest/manifest_push.go new file mode 100644 index 00000000000..abb94f98007 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_push.go @@ -0,0 +1,80 @@ +/* + 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 manifest + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" +) + +func PushCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "push [OPTIONS] INDEX/MANIFESTLIST", + Short: "Push a manifest list to a registry", + Args: cobra.ExactArgs(1), + RunE: pushAction, + ValidArgsFunction: pushShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry") + cmd.Flags().Bool("purge", false, "Remove the manifest list after pushing") + return cmd +} + +func processPushFlags(cmd *cobra.Command) (types.ManifestPushOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.ManifestPushOptions{}, err + } + + insecure, err := cmd.Flags().GetBool("insecure") + if err != nil { + return types.ManifestPushOptions{}, err + } + purge, err := cmd.Flags().GetBool("purge") + if err != nil { + return types.ManifestPushOptions{}, err + } + + return types.ManifestPushOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Insecure: insecure, + Purge: purge, + }, nil +} + +func pushAction(cmd *cobra.Command, args []string) error { + pushOptions, err := processPushFlags(cmd) + if err != nil { + return err + } + err = manifest.Push(cmd.Context(), args[0], pushOptions) + if err != nil { + return err + } + return nil +} + +func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/manifest_types.go b/pkg/api/types/manifest_types.go index 92a6f8634a5..0bbf45af651 100644 --- a/pkg/api/types/manifest_types.go +++ b/pkg/api/types/manifest_types.go @@ -48,3 +48,13 @@ type ManifestInspectOptions struct { // Allow communication with an insecure registry Insecure bool } + +// ManifestPushOptions specifies options for `nerdctl manifest push`. +type ManifestPushOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Allow communication with an insecure registry + Insecure bool + // Remove the manifest list after pushing + Purge bool +} diff --git a/pkg/cmd/manifest/push.go b/pkg/cmd/manifest/push.go new file mode 100644 index 00000000000..01923b514dd --- /dev/null +++ b/pkg/cmd/manifest/push.go @@ -0,0 +1,229 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package manifest + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/remotes" + "github.com/containerd/errdefs" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/manifeststore" + "github.com/containerd/nerdctl/v2/pkg/manifesttypes" + "github.com/containerd/nerdctl/v2/pkg/manifestutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +func Push(ctx context.Context, listRef string, options types.ManifestPushOptions) error { + parsedTargetRef, err := referenceutil.Parse(listRef) + if err != nil { + return fmt.Errorf("failed to parse target reference %s: %w", listRef, err) + } + + manifestStore, err := manifeststore.NewStore(options.GOptions.DataRoot) + if err != nil { + return fmt.Errorf("failed to create manifest store: %w", err) + } + + manifests, err := manifestStore.GetList(parsedTargetRef) + if err != nil { + return fmt.Errorf("failed to get manifests: %w", err) + } + + if len(manifests) == 0 { + return fmt.Errorf("no manifests found for %s", listRef) + } + + resolver, err := manifestutil.CreateResolver(ctx, parsedTargetRef.Domain, options.GOptions, options.Insecure) + if err != nil { + return fmt.Errorf("failed to create resolver: %w", err) + } + + if err := pushIndividualManifests(ctx, resolver, manifests, parsedTargetRef, options); err != nil { + return fmt.Errorf("failed to push individual manifests: %w", err) + } + + manifestList, err := buildManifestList(manifests) + if err != nil { + return fmt.Errorf("failed to build manifest list: %w", err) + } + + digest, err := pushManifestList(ctx, resolver, parsedTargetRef, manifestList) + if err != nil { + return fmt.Errorf("failed to push manifest list: %w", err) + } + + fmt.Fprintln(options.Stdout, digest) + + if options.Purge { + if err := manifestStore.Remove(parsedTargetRef); err != nil { + return fmt.Errorf("failed to remove manifest list from store: %w", err) + } + } + + return nil +} + +func buildManifestList(manifests []*manifesttypes.DockerManifestEntry) (manifesttypes.DockerManifestList, error) { + if len(manifests) == 0 { + return manifesttypes.DockerManifestList{}, fmt.Errorf("no manifests to build list from") + } + + var descriptors []manifesttypes.DockerManifestDescriptor + useOCIIndex := false + + for _, manifest := range manifests { + if manifest.Descriptor.Platform == nil || + manifest.Descriptor.Platform.Architecture == "" || + manifest.Descriptor.Platform.OS == "" { + return manifesttypes.DockerManifestList{}, fmt.Errorf("manifest %s must have an OS and Architecture to be pushed to a registry", manifest.Ref) + } + + if manifest.Descriptor.MediaType == ocispec.MediaTypeImageManifest { + useOCIIndex = true + } + + descriptors = append(descriptors, manifesttypes.DockerManifestDescriptor{ + MediaType: manifest.Descriptor.MediaType, + Size: manifest.Descriptor.Size, + Digest: manifest.Descriptor.Digest, + Platform: *manifest.Descriptor.Platform, + }) + } + manifestList := manifesttypes.DockerManifestList{ + SchemaVersion: 2, + MediaType: images.MediaTypeDockerSchema2ManifestList, + Manifests: descriptors, + } + if useOCIIndex { + manifestList.MediaType = ocispec.MediaTypeImageIndex + } + + return manifestList, nil +} + +func pushIndividualManifests(ctx context.Context, resolver remotes.Resolver, manifests []*manifesttypes.DockerManifestEntry, targetRef *referenceutil.ImageReference, options types.ManifestPushOptions) error { + targetDomain := targetRef.Domain + targetRepo := targetRef.Path + + for _, manifest := range manifests { + manifestRef, err := referenceutil.Parse(manifest.Ref) + if err != nil { + return fmt.Errorf("failed to parse manifest reference %s: %w", manifest.Ref, err) + } + + var targetManifestRef string + if manifestRef.Domain != targetDomain { + targetManifestRef = fmt.Sprintf("%s/%s@%s", targetDomain, manifestRef.Path, manifest.Descriptor.Digest) + } else { + targetManifestRef = fmt.Sprintf("%s/%s@%s", targetDomain, targetRepo, manifest.Descriptor.Digest) + } + + if err := pushManifest(ctx, resolver, targetManifestRef, manifest); err != nil { + return fmt.Errorf("failed to push manifest %s: %w", targetManifestRef, err) + } + + fmt.Fprintf(options.Stdout, "Pushed ref %s with digest: %s\n", targetManifestRef, manifest.Descriptor.Digest) + } + + return nil +} + +func pushManifest(ctx context.Context, resolver remotes.Resolver, ref string, manifest *manifesttypes.DockerManifestEntry) error { + rawData, err := base64.StdEncoding.DecodeString(manifest.Raw) + if err != nil { + return fmt.Errorf("failed to decode manifest data: %w", err) + } + + pusher, err := resolver.Pusher(ctx, ref) + if err != nil { + return fmt.Errorf("failed to create pusher: %w", err) + } + + writer, err := pusher.Push(ctx, manifest.Descriptor) + if err != nil { + if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") { + return nil + } + return fmt.Errorf("failed to create content writer: %w", err) + } + defer writer.Close() + + if _, err := writer.Write(rawData); err != nil { + return fmt.Errorf("failed to write manifest data: %w", err) + } + + if err := writer.Commit(ctx, manifest.Descriptor.Size, manifest.Descriptor.Digest); err != nil { + if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") { + return nil + } + return fmt.Errorf("failed to commit manifest: %w", err) + } + + return nil +} + +func pushManifestList(ctx context.Context, resolver remotes.Resolver, targetRef *referenceutil.ImageReference, manifestList manifesttypes.DockerManifestList) (digest.Digest, error) { + data, err := json.MarshalIndent(manifestList, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal manifest list: %w", err) + } + + dgst := digest.FromBytes(data) + + desc := ocispec.Descriptor{ + MediaType: manifestList.MediaType, + Size: int64(len(data)), + Digest: dgst, + } + + pusher, err := resolver.Pusher(ctx, targetRef.String()) + if err != nil { + return "", fmt.Errorf("failed to create pusher: %w", err) + } + + writer, err := pusher.Push(ctx, desc) + if err != nil { + if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") { + return dgst, nil + } + return "", fmt.Errorf("failed to create content writer: %w", err) + } + defer writer.Close() + + if _, err := writer.Write(data); err != nil { + return "", fmt.Errorf("failed to write manifest list data: %w", err) + } + + if err := writer.Commit(ctx, desc.Size, desc.Digest); err != nil { + if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") { + return dgst, nil + } + return "", fmt.Errorf("failed to commit manifest list: %w", err) + } + + return dgst, nil +} diff --git a/pkg/manifesttypes/manifesttypes.go b/pkg/manifesttypes/manifesttypes.go index 129c234a13f..7e43b383c54 100644 --- a/pkg/manifesttypes/manifesttypes.go +++ b/pkg/manifesttypes/manifesttypes.go @@ -17,11 +17,12 @@ package manifesttypes import ( + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) +// For Docker's verbose format type ( - // DockerManifestEntry represents a single manifest entry in Docker's verbose format DockerManifestEntry struct { Ref string `json:"Ref"` @@ -30,6 +31,7 @@ type ( SchemaV2Manifest interface{} `json:"SchemaV2Manifest,omitempty"` OCIManifest interface{} `json:"OCIManifest,omitempty"` } + ManifestStruct struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType"` @@ -38,15 +40,29 @@ type ( Annotations map[string]string `json:"annotations,omitempty"` } - DockerManifestStruct ManifestStruct - DockerManifestListStruct struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType"` Manifests []ocispec.Descriptor `json:"manifests"` } - OCIIndexStruct ocispec.Index + DockerManifestStruct = ManifestStruct + OCIManifestStruct = ManifestStruct + OCIIndexStruct = ocispec.Index +) + +// For manifest push, compatible with Docker distribution spec +type ( + DockerManifestDescriptor struct { + MediaType string `json:"mediaType"` + Size int64 `json:"size"` + Digest digest.Digest `json:"digest"` + Platform ocispec.Platform `json:"platform"` + } - OCIManifestStruct ManifestStruct + DockerManifestList struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType,omitempty"` + Manifests []DockerManifestDescriptor `json:"manifests"` + } ) From 8acb15ac54d793184168c3b52c6725a9c3fb66b4 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 7 Aug 2025 13:25:59 +0800 Subject: [PATCH 168/868] manifest: add unit tests for nerdctl manifest push command add unit tests for nerdctl manifest push command Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_push_linux_test.go | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 cmd/nerdctl/manifest/manifest_push_linux_test.go diff --git a/cmd/nerdctl/manifest/manifest_push_linux_test.go b/cmd/nerdctl/manifest/manifest_push_linux_test.go new file mode 100644 index 00000000000..c92bc1eb436 --- /dev/null +++ b/cmd/nerdctl/manifest/manifest_push_linux_test.go @@ -0,0 +1,124 @@ +/* + 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 manifest + +import ( + "errors" + "fmt" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" +) + +func TestManifestPushErrors(t *testing.T) { + testCase := nerdtest.Setup() + invalidName := "invalid/name/with/special@chars" + testCase.SubTests = []*test.Case{ + { + Description: "require-one-argument", + Command: test.Command("manifest", "push", "arg1", "arg2"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "invalid-list-name", + Command: test.Command("manifest", "push", invalidName), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "invalid reference format", + }), + }, + } + + testCase.Run(t) +} + +func TestManifestPush(t *testing.T) { + nerdtest.Setup() + + var registryTokenAuthHTTPSRandom *registry.Server + var tokenServer *registry.TokenAuthServer + + manifestRef := testutil.GetTestImageWithoutTag("alpine") + "@" + testutil.GetTestImageManifestDigest("alpine", "linux/amd64") + expectedDigest := "sha256:5317ce2da263afa23570c692d62c1b01381285b2198b3ea9739ce64bec22aff2" + + testCase := &test.Case{ + Require: require.All( + require.Linux, + nerdtest.Registry, + ), + Setup: func(data test.Data, helpers test.Helpers) { + registryTokenAuthHTTPSRandom, tokenServer = nerdtest.RegistryWithTokenAuth(data, helpers, "admin", "badmin", 0, true) + tokenServer.Setup(data, helpers) + registryTokenAuthHTTPSRandom.Setup(data, helpers) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if registryTokenAuthHTTPSRandom != nil { + registryTokenAuthHTTPSRandom.Cleanup(data, helpers) + } + if tokenServer != nil { + tokenServer.Cleanup(data, helpers) + } + }, + SubTests: []*test.Case{ + { + Description: "push-to-registry", + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + targetRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") + helpers.Ensure("pull", manifestRef) + helpers.Ensure("tag", manifestRef, targetRef) + helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", + fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) + helpers.Ensure("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, targetRef) + helpers.Ensure("rmi", targetRef) + helpers.Ensure("manifest", "create", "--insecure", targetRef+"-success", targetRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + targetRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") + return helpers.Command("manifest", "push", "--insecure", targetRef+"-success") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "output": expectedDigest, + }), + }, + }, + } + testCase.Run(t) +} From 322e8ca60644b216d845bf930a4ff3c4425f3f67 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 7 Aug 2025 14:39:21 +0800 Subject: [PATCH 169/868] docs: add nerdctl manifest push reference add nerdctl manifest push reference Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index fdbf101d27c..fffec04658e 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -55,6 +55,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) - [:whale: nerdctl manifest inspect](#whale-nerdctl-manifest-inspect) + - [:whale: nerdctl manifest push](#whale-nerdctl-manifest-push) - [:whale: nerdctl manifest rm](#whale-nerdctl-manifest-rm) - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) @@ -1103,6 +1104,24 @@ nerdctl manifest inspect alpine:3.22.1 nerdctl manifest inspect alpine@sha256:eafc1edb577d2e9b458664a15f23ea1c370214193226069eb22921169fc7e43f ``` +### :whale: nerdctl manifest push + +Push a manifest list to a registry. + +Usage: `nerdctl manifest push [OPTIONS] INDEX/MANIFESTLIST` + +Flags: + +- `--insecure`: Allow communication with an insecure registry +- `--purge`: Remove the manifest list after pushing + +Examples: + +```bash +# Push a manifest list to a registry +nerdctl manifest push myapp:latest +``` + ### :whale: nerdctl manifest rm Remove one or more index/manifest lists. From 44b68c005c38e2d38c482fc4a5956d55750b660a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:10:10 +0000 Subject: [PATCH 170/868] build(deps): bump actions/cache from 4.2.3 to 4.2.4 Bumps [actions/cache](https://github.com/actions/cache) from 4.2.3 to 4.2.4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/5a3ec84eff668545956fd18022155c47e93e2684...0400d5f644dc74513175e3cd8d07132dd4860809) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 4.2.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 8a7567cb120..5973d315f1a 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -35,7 +35,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 2c12637326e..579a8260d88 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: /root/.vagrant.d key: vagrant From 828cee32622c4d1745d377862ee6e97fb7267b7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:41:20 +0000 Subject: [PATCH 171/868] build(deps): bump github.com/docker/go-connections from 0.5.0 to 0.6.0 Bumps [github.com/docker/go-connections](https://github.com/docker/go-connections) from 0.5.0 to 0.6.0. - [Commits](https://github.com/docker/go-connections/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: github.com/docker/go-connections dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 068ad22ba2b..a9b1c5b8ae2 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/docker/cli v28.3.3+incompatible //gomodjail:unconfined github.com/docker/docker v28.3.3+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.5.0 + github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.18.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 05de58cc6b7..5f172a94962 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjY github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= From c35a3a7baef45955b6bc905c37ac062936cc187d Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 10:17:10 +0800 Subject: [PATCH 172/868] network: support --internal flag Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/network/network_create.go | 6 ++++++ pkg/api/types/network_types.go | 1 + pkg/netutil/netutil.go | 4 ++-- pkg/netutil/netutil_unix.go | 24 +++++++++++++++++------- pkg/netutil/netutil_windows.go | 6 +++--- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index ca8b414654f..720a6ff1cec 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -51,6 +51,7 @@ func createCommand() *cobra.Command { cmd.Flags().String("ip-range", "", `Allocate container ip from a sub-range`) cmd.Flags().StringArray("label", nil, "Set metadata for a network") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") + cmd.Flags().Bool("internal", false, "Restrict external access to the network") return cmd } @@ -100,6 +101,10 @@ func createAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + internal, err := cmd.Flags().GetBool("internal") + if err != nil { + return err + } return network.Create(types.NetworkCreateOptions{ GOptions: globalOptions, @@ -113,5 +118,6 @@ func createAction(cmd *cobra.Command, args []string) error { IPRange: ipRangeStr, Labels: labels, IPv6: ipv6, + Internal: internal, }, cmd.OutOrStdout()) } diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go index 5cb26b3ea15..530f66fa729 100644 --- a/pkg/api/types/network_types.go +++ b/pkg/api/types/network_types.go @@ -35,6 +35,7 @@ type NetworkCreateOptions struct { IPRange string Labels []string IPv6 bool + Internal bool } // NetworkInspectOptions specifies options for `nerdctl network inspect`. diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 66c80c430d7..c3312bb5191 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -306,11 +306,11 @@ func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, if _, ok := netMap[opts.Name]; ok { return nil, errdefs.ErrAlreadyExists } - ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6) + ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6, opts.Internal) if err != nil { return nil, err } - plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6) + plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6, opts.Internal) if err != nil { return nil, err } diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index ffb1d8503a8..f71dc100742 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -90,7 +90,7 @@ func (n *NetworkConfig) clean() error { return nil } -func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool) ([]CNIPlugin, error) { +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool, internal bool) ([]CNIPlugin, error) { var ( plugins []CNIPlugin err error @@ -123,13 +123,21 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] } bridge.MTU = mtu bridge.IPAM = ipam - bridge.IsGW = true - bridge.IPMasq = iPMasq + bridge.IsGW = !internal + if internal { + bridge.IPMasq = false + } else { + bridge.IPMasq = iPMasq + } bridge.HairpinMode = true if ipv6 { bridge.Capabilities["ips"] = true } - plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} + if internal { + plugins = []CNIPlugin{bridge, newFirewallPlugin(), newTuningPlugin()} + } else { + plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} + } if name != DefaultNetworkName { firewallPath := filepath.Join(e.Path, "firewall") ok, err := firewallPluginGEQ110(firewallPath) @@ -186,13 +194,15 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": ipamConf := newHostLocalIPAMConfig() - ipamConf.Routes = []IPAMRoute{ - {Dst: "0.0.0.0/0"}, + if !internal { + ipamConf.Routes = []IPAMRoute{ + {Dst: "0.0.0.0/0"}, + } } ranges, findIPv4, err := e.parseIPAMRanges(subnets, gatewayStr, ipRangeStr, ipv6) if err != nil { diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 8e0e67a01ed..bd03e6ec4aa 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -30,7 +30,7 @@ const ( // When creating non-default network without passing in `--subnet` option, // nerdctl assigns subnet address for the creation starting from `StartingCIDR` - // This prevents subnet address overlapping with `DefaultCIDR` used by the default networkß + // This prevents subnet address overlapping with `DefaultCIDR` used by the default network StartingCIDR = "10.4.1.0/24" ) @@ -58,7 +58,7 @@ func (n *NetworkConfig) clean() error { return nil } -func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool) ([]CNIPlugin, error) { +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool, internal bool) ([]CNIPlugin, error) { var plugins []CNIPlugin switch driver { case "nat": @@ -71,7 +71,7 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { switch driver { case "default": default: From be4a8c5243079b8dbb24a24c0de47f8db4e54400 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 11:44:35 +0800 Subject: [PATCH 173/868] docs: update command reference Remove some unimplemented lines which have been supported Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index fdbf101d27c..f6f0e9d8c9b 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -552,8 +552,6 @@ Flags: - :whale: `--type`: Return JSON for specified type - :whale: `--size`: Display total file sizes if the type is container -Unimplemented `docker inspect` flags: `--size` - ### :whale: nerdctl logs Fetch the logs of a container. @@ -1559,7 +1557,7 @@ Flags: - :whale: `--pull`: Pull image before running ("always"|"missing"|"never") Unimplemented `docker-compose up` (V1) flags: `--no-deps`, `--always-recreate-deps`, -`--no-start`, `--abort-on-container-exit`, `--attach-dependencies`, `--timeout`, `--renew-anon-volumes`, `--exit-code-from` +`--no-start`, `--attach-dependencies`, `--timeout`, `--renew-anon-volumes`, `--exit-code-from` Unimplemented `docker compose up` (V2) flags: `--environment` @@ -1896,7 +1894,6 @@ Image: - `docker export` and `docker import` - `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign|notation` and `nerdctl push --sign=cosign|notation`. See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md).) -- `docker manifest *` Network management: From 49f4f4d570a7443f4de54b05936f3cd830ea667a Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 11:13:13 +0800 Subject: [PATCH 174/868] network: add unit tests for internal flag Signed-off-by: ChengyuZhu6 --- .../network/network_create_linux_test.go | 48 +++++++++++++++++++ pkg/testutil/nerdtest/utilities.go | 13 +++++ 2 files changed, 61 insertions(+) diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index 6d42809f2ff..843ec56c44b 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -17,6 +17,7 @@ package network import ( + "encoding/json" "fmt" "net" "strings" @@ -107,6 +108,53 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "internal enabled", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", "--internal", data.Identifier()) + netw := nerdtest.InspectNetwork(helpers, data.Identifier()) + assert.Equal(t, len(netw.IPAM.Config), 1) + data.Labels().Set("subnet", netw.IPAM.Config[0].Subnet) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "route") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet"))) + assert.Assert(t, !strings.Contains(stdout, "default ")) + if nerdtest.IsDocker() { + return + } + nativeNet := nerdtest.InspectNetworkNative(helpers, data.Identifier()) + var cni struct { + Plugins []struct { + Type string `json:"type"` + IsGW bool `json:"isGateway"` + IPMasq bool `json:"ipMasq"` + } `json:"plugins"` + } + _ = json.Unmarshal(nativeNet.CNI, &cni) + // bridge plugin assertions and no portmap + foundBridge := false + for _, p := range cni.Plugins { + assert.Assert(t, p.Type != "portmap") + if p.Type == "bridge" { + foundBridge = true + assert.Assert(t, !p.IsGW) + assert.Assert(t, !p.IPMasq) + } + } + assert.Assert(t, foundBridge) + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index ca70166b725..b3f1d15ac2e 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -88,6 +88,19 @@ func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { return res } +func InspectNetworkNative(helpers test.Helpers, name string) native.Network { + helpers.T().Helper() + var res native.Network + cmd := helpers.Command("network", "inspect", "--mode", "native", name) + cmd.Run(&test.Expected{ + Output: expect.JSON([]native.Network{}, func(dc []native.Network, t tig.T) { + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results") + res = dc[0] + }), + }) + return res +} + func InspectImage(helpers test.Helpers, name string) dockercompat.Image { helpers.T().Helper() var res dockercompat.Image From ab47199c51383c63e00e4d001089d09c4710b38e Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 11:29:41 +0800 Subject: [PATCH 175/868] docs: add network create --internal reference add network create --internal reference Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index fdbf101d27c..0cc67e9571c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1169,8 +1169,9 @@ Flags: - :whale: `--ip-range`: Allocate container ip from a sub-range - :whale: `--label`: Set metadata on a network - :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet. +- :whale: `--internal`: Restrict external access to the network. -Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--scope` +Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--scope` ### :whale: nerdctl network ls From 05cc089ed7bd6a4e409c3a189a2cfb78f3d5dd9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 22:02:48 +0000 Subject: [PATCH 176/868] build(deps): bump github.com/containerd/go-cni from 1.1.12 to 1.1.13 Bumps [github.com/containerd/go-cni](https://github.com/containerd/go-cni) from 1.1.12 to 1.1.13. - [Release notes](https://github.com/containerd/go-cni/releases) - [Commits](https://github.com/containerd/go-cni/compare/v1.1.12...v1.1.13) --- updated-dependencies: - dependency-name: github.com/containerd/go-cni dependency-version: 1.1.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 068ad22ba2b..cfb8cfa83b4 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined - github.com/containerd/go-cni v1.1.12 //gomodjail:unconfined + github.com/containerd/go-cni v1.1.13 //gomodjail:unconfined github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 diff --git a/go.sum b/go.sum index 05de58cc6b7..2ec4dd326c4 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= -github.com/containerd/go-cni v1.1.12 h1:wm/5VD/i255hjM4uIZjBRiEQ7y98W9ACy/mHeLi4+94= -github.com/containerd/go-cni v1.1.12/go.mod h1:+jaqRBdtW5faJxj2Qwg1Of7GsV66xcvnCx4mSJtUlxU= +github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis= +github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA= github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlKUy6kOio= From 7cd0e2bc7e629f6803782cac8690060ea4a15ded Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:44:23 +0000 Subject: [PATCH 177/868] build(deps): bump actions/checkout from 4.2.2 to 5.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...08c6903cd8c0fde910a37f88322edcfb5dd907a8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 14fe15e225a..252f5aea3a2 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index c1b2700ea31..938bfd7b351 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index b4eac0e4668..1b5442a698b 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 2f012789877..509f2bbf950 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index a3c840642a5..dbf6d8f3912 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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 625dca61fa8..9a025c1ac27 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 32a0088822d..da776b86db5 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -63,7 +63,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index da6822bb5f3..932a070e00d 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -71,7 +71,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 8a7567cb120..33201d7e5e3 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -26,7 +26,7 @@ jobs: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 2c12637326e..630a870183a 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index c7af9804e75..e892876eea9 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55cf55111f3..7501eaed669 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 85b5c1dd650..9851047f308 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -45,7 +45,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 - name: "Run" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index e74b34a9082..7be9d65ac86 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -32,7 +32,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 100 - if: ${{ matrix.canary }} From 1257015ac3f70126319f41502a6de203eb9f0c94 Mon Sep 17 00:00:00 2001 From: Manu Gupta Date: Tue, 12 Aug 2025 18:59:18 -0700 Subject: [PATCH 178/868] Move manugupt1 to emeritus status - Add manugupt1 to EMERITUS.md with documented contributions - Remove manugupt1 from active MAINTAINERS list Thank you for all your support and I got to learn a lot. Signed-off-by: Manu Gupta --- EMERITUS.md | 9 +++++++++ MAINTAINERS | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/EMERITUS.md b/EMERITUS.md index ed17a87f02f..5fc3c2189dd 100644 --- a/EMERITUS.md +++ b/EMERITUS.md @@ -19,3 +19,12 @@ a Reviewer of nerdctl from November 2022 to June 2024. Hanchin has made significant contributions such as the addition of [syslog driver](https://github.com/containerd/nerdctl/pull/1377) and [IPv6 networking](https://github.com/containerd/nerdctl/pull/1558). + +### Manu Gupta ([@manugupt1](https://github.com/manugupt1)) +Manu Gupta (GitHub ID [@manugupt1](https://github.com/manugupt1)) served as +a Reviewer of nerdctl from 2022 to August 2025. + +Manu has made [significant improvements](https://github.com/containerd/nerdctl/pulls?q=author%3Amanugupt1+) +especially to image and volume management, container runtime features, build system enhancements, +and CI/CD infrastructure. Notable contributions include image filtering capabilities, volume size +inspection, Docker Compose enhancements, and multi-architecture build support. diff --git a/MAINTAINERS b/MAINTAINERS index d245e39c902..ab9d1a97d77 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -21,7 +21,6 @@ # REVIEWERS # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" -"manugupt1", "Manu Gupta", "manugupt1@gmail.com","FCA9 504A 4118 EA5C F466 CC30 A5C3 A8F4 E7FE 9E10" "Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" # EMERITUS From d1544e4bcaae3291ea3d118ad8aecdacb02054f0 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 15 Aug 2025 15:43:23 +0800 Subject: [PATCH 179/868] ci: retry downloading the file from github Signed-off-by: ChengyuZhu6 --- Dockerfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5969b2d2c1c..fbcebf855d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -154,7 +154,7 @@ RUN echo "- runc: ${RUNC_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG CNI_PLUGINS_VERSION RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION%%@*}; \ fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VERSION}.tgz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/cni-plugins-${CNI_PLUGINS_VERSION}" | sha256sum -c && \ mkdir -p /out/libexec/cni && \ tar xzf "${fname}" -C /out/libexec/cni && \ @@ -163,7 +163,7 @@ RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION%%@*}; \ ARG BUILDKIT_VERSION RUN BUILDKIT_VERSION=${BUILDKIT_VERSION%%@*}; \ fname="buildkit-${BUILDKIT_VERSION}.${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildkit-${BUILDKIT_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out && \ rm -f "${fname}" /out/bin/buildkit-qemu-* /out/bin/buildkit-cni-* /out/bin/buildkit-runc && \ @@ -179,7 +179,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \ http::helper github::file containerd/stargz-snapshotter script/config/etc/systemd/system/stargz-snapshotter.service "${STARGZ_SNAPSHOTTER_VERSION}" > "stargz-snapshotter.service" && \ grep "${fname}" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ grep "stargz-snapshotter.service" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \ @@ -196,7 +196,7 @@ RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%%@*}" https://githu ARG SLIRP4NETNS_VERSION RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION%%@*}; \ fname="slirp4netns-$(cat /target_uname_m)" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/slirp4netns-${SLIRP4NETNS_VERSION}" | sha256sum -c && \ mv "${fname}" /out/bin/slirp4netns && \ chmod +x /out/bin/slirp4netns && \ @@ -207,7 +207,7 @@ RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION%%@*}" >> /out/share/doc/nerdctl ARG FUSE_OVERLAYFS_VERSION RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION%%@*}; \ fname="fuse-overlayfs-$(cat /target_uname_m)" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/fuse-overlayfs-${FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ mv "${fname}" /out/bin/fuse-overlayfs && \ chmod +x /out/bin/fuse-overlayfs && \ @@ -215,7 +215,7 @@ RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION%%@*}; \ ARG CONTAINERD_FUSE_OVERLAYFS_VERSION RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION%%@*}; \ fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION##*v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" && \ @@ -223,7 +223,7 @@ RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION%%@*}; ARG TINI_VERSION RUN TINI_VERSION=${TINI_VERSION%%@*}; \ fname="tini-static-${TARGETARCH:-amd64}" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/tini-${TINI_VERSION}" | sha256sum -c && \ cp -a "${fname}" /out/bin/tini && chmod +x /out/bin/tini && \ echo "- Tini: ${TINI_VERSION}" >> /out/share/doc/nerdctl-full/README.md @@ -232,7 +232,7 @@ ARG BUILDG_VERSION # confusing debugging information, eg: BUILDG_VERSION will appear as if the original ARG value was used. RUN BUILDG_VERSION=${BUILDG_VERSION%%@*}; \ fname="buildg-${BUILDG_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/buildg-${BUILDG_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" && \ @@ -240,7 +240,7 @@ RUN BUILDG_VERSION=${BUILDG_VERSION%%@*}; \ ARG ROOTLESSKIT_VERSION RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION%%@*}; \ fname="rootlesskit-$(cat /target_uname_m).tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \ grep "${fname}" "/SHA256SUMS.d/rootlesskit-${ROOTLESSKIT_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out/bin && \ rm -f "${fname}" /out/bin/rootlesskit-docker-proxy && \ @@ -328,7 +328,7 @@ COPY --from=ghcr.io/sigstore/cosign/cosign:v2.2.3@sha256:8fc9cad121611e8479f65f7 # installing soci for integration test ARG SOCI_SNAPSHOTTER_VERSION RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc && \ mkdir -p /etc/soci-snapshotter-grpc && \ touch /etc/soci-snapshotter-grpc/config.toml && \ @@ -349,7 +349,7 @@ RUN systemctl enable test-integration-ipfs-offline test-integration-buildkit-ner ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889" # install nydus components ARG NYDUS_VERSION -RUN curl -o nydus-static.tgz -fsSL --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \ +RUN curl -o nydus-static.tgz -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \ tar xzf nydus-static.tgz && \ mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \ rm nydus-static.tgz From 7dcdd2143307fbacf616b7a92066cd0769c96e2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 01:54:56 +0000 Subject: [PATCH 180/868] build(deps): bump the golang-x group with 5 updates Bumps the golang-x group with 5 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.40.0` | `0.41.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.42.0` | `0.43.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.34.0` | `0.35.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.33.0` | `0.34.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.27.0` | `0.28.0` | Updates `golang.org/x/crypto` from 0.40.0 to 0.41.0 - [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.41.0) Updates `golang.org/x/net` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/net/compare/v0.42.0...v0.43.0) Updates `golang.org/x/sys` from 0.34.0 to 0.35.0 - [Commits](https://github.com/golang/sys/compare/v0.34.0...v0.35.0) Updates `golang.org/x/term` from 0.33.0 to 0.34.0 - [Commits](https://github.com/golang/term/compare/v0.33.0...v0.34.0) Updates `golang.org/x/text` from 0.27.0 to 0.28.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.27.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 068ad22ba2b..6d594ad9845 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.5.2 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.40.0 - golang.org/x/net v0.42.0 + golang.org/x/crypto v0.41.0 + golang.org/x/net v0.43.0 golang.org/x/sync v0.16.0 //gomodjail:unconfined - golang.org/x/sys v0.34.0 //gomodjail:unconfined - golang.org/x/term v0.33.0 //gomodjail:unconfined - golang.org/x/text v0.27.0 + golang.org/x/sys v0.35.0 //gomodjail:unconfined + golang.org/x/term v0.34.0 //gomodjail:unconfined + golang.org/x/text v0.28.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) @@ -136,7 +136,7 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.25.0 // indirect + golang.org/x/mod v0.26.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect //gomodjail:unconfined google.golang.org/grpc v1.72.2 // indirect diff --git a/go.sum b/go.sum index 05de58cc6b7..0fe41e33db0 100644 --- a/go.sum +++ b/go.sum @@ -364,8 +364,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -379,8 +379,8 @@ 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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 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= @@ -397,8 +397,8 @@ 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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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= @@ -437,8 +437,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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -448,8 +448,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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 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= @@ -459,8 +459,8 @@ 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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 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= @@ -473,8 +473,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 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= From 16268057a01475cfd5fc92ef28a02c8de627083f Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 18 Aug 2025 16:25:53 +0800 Subject: [PATCH 181/868] manifest: fix manifest push error on cross-registry fix manifest push error on cross-registry Signed-off-by: ChengyuZhu6 --- .../manifest/manifest_push_linux_test.go | 23 +++++++++++++++++++ pkg/cmd/manifest/push.go | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/cmd/nerdctl/manifest/manifest_push_linux_test.go b/cmd/nerdctl/manifest/manifest_push_linux_test.go index c92bc1eb436..c254b33c09b 100644 --- a/cmd/nerdctl/manifest/manifest_push_linux_test.go +++ b/cmd/nerdctl/manifest/manifest_push_linux_test.go @@ -118,6 +118,29 @@ func TestManifestPush(t *testing.T) { "output": expectedDigest, }), }, + { + Description: "reject-cross-registry-sources", + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + targetRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") + helpers.Ensure("manifest", "create", "--insecure", targetRef+"-cross", manifestRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + targetRef := fmt.Sprintf("%s:%d/%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, "test-list-push:v1") + return helpers.Command("manifest", "push", "--insecure", targetRef+"-cross") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "cannot use source images from a different registry than the target image:", + }), + }, }, } testCase.Run(t) diff --git a/pkg/cmd/manifest/push.go b/pkg/cmd/manifest/push.go index 01923b514dd..02a6b7181a2 100644 --- a/pkg/cmd/manifest/push.go +++ b/pkg/cmd/manifest/push.go @@ -135,6 +135,10 @@ func pushIndividualManifests(ctx context.Context, resolver remotes.Resolver, man return fmt.Errorf("failed to parse manifest reference %s: %w", manifest.Ref, err) } + if manifestRef.Domain != targetDomain { + return fmt.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRef.Domain, targetDomain) + } + var targetManifestRef string if manifestRef.Domain != targetDomain { targetManifestRef = fmt.Sprintf("%s/%s@%s", targetDomain, manifestRef.Path, manifest.Descriptor.Digest) From 09301120d2e6100bece65a8cb60696c4fc163367 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 14:08:11 +0800 Subject: [PATCH 182/868] image: support import command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/image/image.go | 1 + cmd/nerdctl/image/image_import.go | 133 ++++++++++++++++ cmd/nerdctl/main.go | 1 + pkg/api/types/import_types.go | 31 ++++ pkg/cmd/image/import.go | 252 ++++++++++++++++++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 cmd/nerdctl/image/image_import.go create mode 100644 pkg/api/types/import_types.go create mode 100644 pkg/cmd/image/import.go diff --git a/cmd/nerdctl/image/image.go b/cmd/nerdctl/image/image.go index 47db856f069..a711e392797 100644 --- a/cmd/nerdctl/image/image.go +++ b/cmd/nerdctl/image/image.go @@ -41,6 +41,7 @@ func Command() *cobra.Command { PushCommand(), LoadCommand(), SaveCommand(), + ImportCommand(), TagCommand(), imageRemoveCommand(), convertCommand(), diff --git a/cmd/nerdctl/image/image_import.go b/cmd/nerdctl/image/image_import.go new file mode 100644 index 00000000000..555bbcf7e05 --- /dev/null +++ b/cmd/nerdctl/image/image_import.go @@ -0,0 +1,133 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/image" +) + +func ImportCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]", + Short: "Import the contents from a tarball to create a filesystem image", + Args: cobra.MinimumNArgs(1), + RunE: importAction, + ValidArgsFunction: imageImportShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + + cmd.Flags().StringP("message", "m", "", "Set commit message for imported image") + cmd.Flags().String("platform", "", "Set platform for imported image (e.g., linux/amd64)") + return cmd +} + +func importOptions(cmd *cobra.Command, args []string) (types.ImageImportOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.ImageImportOptions{}, err + } + message, err := cmd.Flags().GetString("message") + if err != nil { + return types.ImageImportOptions{}, err + } + platform, err := cmd.Flags().GetString("platform") + if err != nil { + return types.ImageImportOptions{}, err + } + var reference string + if len(args) > 1 { + reference = args[1] + } + + var in io.ReadCloser + src := args[0] + switch { + case src == "-": + in = io.NopCloser(cmd.InOrStdin()) + case hasHTTPPrefix(src): + resp, err := http.Get(src) + if err != nil { + return types.ImageImportOptions{}, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + defer resp.Body.Close() + return types.ImageImportOptions{}, fmt.Errorf("failed to download %s: %s", src, resp.Status) + } + in = resp.Body + default: + f, err := os.Open(src) + if err != nil { + return types.ImageImportOptions{}, err + } + in = f + } + + return types.ImageImportOptions{ + Stdout: cmd.OutOrStdout(), + Stdin: in, + GOptions: globalOptions, + Source: args[0], + Reference: reference, + Message: message, + Platform: platform, + }, nil +} + +func importAction(cmd *cobra.Command, args []string) error { + opt, err := importOptions(cmd, args) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), opt.GOptions.Namespace, opt.GOptions.Address) + if err != nil { + return err + } + defer cancel() + defer func() { + if rc, ok := opt.Stdin.(io.ReadCloser); ok { + _ = rc.Close() + } + }() + + name, err := image.Import(ctx, client, opt) + if err != nil { + return err + } + _, err = cmd.OutOrStdout().Write([]byte(name + "\n")) + return err +} + +func imageImportShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} + +func hasHTTPPrefix(s string) bool { + return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 45f9f6dfaa6..c5abcc60a6c 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -304,6 +304,7 @@ Config file ($NERDCTL_TOML): %s image.PushCommand(), image.LoadCommand(), image.SaveCommand(), + image.ImportCommand(), image.TagCommand(), image.RmiCommand(), image.HistoryCommand(), diff --git a/pkg/api/types/import_types.go b/pkg/api/types/import_types.go new file mode 100644 index 00000000000..e78d03ae92d --- /dev/null +++ b/pkg/api/types/import_types.go @@ -0,0 +1,31 @@ +/* + 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 types + +import "io" + +// ImageImportOptions specifies options for `nerdctl (image) import`. +type ImageImportOptions struct { + Stdout io.Writer + Stdin io.Reader + GOptions GlobalCommandOptions + + Source string + Reference string + Message string + Platform string +} diff --git a/pkg/cmd/image/import.go b/pkg/cmd/image/import.go new file mode 100644 index 00000000000..a8e61eb6944 --- /dev/null +++ b/pkg/cmd/image/import.go @@ -0,0 +1,252 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "time" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/identity" + "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/leases" + "github.com/containerd/containerd/v2/pkg/archive/compression" + "github.com/containerd/errdefs" + "github.com/containerd/platforms" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +func Import(ctx context.Context, client *containerd.Client, options types.ImageImportOptions) (string, error) { + img, err := importRootfs(ctx, client, options.GOptions.Snapshotter, options) + if err != nil { + return "", err + } + return img.Name, nil +} + +func importRootfs(ctx context.Context, client *containerd.Client, snapshotter string, options types.ImageImportOptions) (images.Image, error) { + var zero images.Image + + ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) + if err != nil { + return zero, err + } + defer done(ctx) + + if options.Stdin == nil { + return zero, fmt.Errorf("no input stream provided") + } + decomp, err := compression.DecompressStream(options.Stdin) + if err != nil { + return zero, err + } + defer decomp.Close() + + cs := client.ContentStore() + + ref := randomRef("import-rootfs-") + w, err := content.OpenWriter(ctx, cs, content.WithRef(ref)) + if err != nil { + return zero, err + } + defer w.Close() + if err := w.Truncate(0); err != nil { + return zero, err + } + + digester := digest.Canonical.Digester() + tee := io.TeeReader(decomp, digester.Hash()) + pr, pw := io.Pipe() + gz := gzip.NewWriter(pw) + doneCh := make(chan error, 1) + go func() { + _, err := io.Copy(gz, tee) + if err != nil { + doneCh <- err + _ = gz.Close() + _ = pw.CloseWithError(err) + return + } + if err := gz.Close(); err != nil { + doneCh <- err + _ = pw.CloseWithError(err) + return + } + doneCh <- pw.Close() + }() + + n, err := io.Copy(w, pr) + if err != nil { + return zero, err + } + if err := <-doneCh; err != nil { + return zero, err + } + + diffID := digester.Digest() + labels := map[string]string{ + "containerd.io/uncompressed": diffID.String(), + } + if err := w.Commit(ctx, n, "", content.WithLabels(labels)); err != nil && !errdefs.IsAlreadyExists(err) { + return zero, err + } + layerDesc := ocispec.Descriptor{ + MediaType: images.MediaTypeDockerSchema2LayerGzip, + Digest: w.Digest(), + Size: n, + } + + ociplat := platforms.DefaultSpec() + if options.Platform != "" { + p, err := platforms.Parse(options.Platform) + if err != nil { + return zero, err + } + ociplat = p + } + + created := time.Now().UTC() + imgConfig := ocispec.Image{ + Platform: ocispec.Platform{ + Architecture: ociplat.Architecture, + OS: ociplat.OS, + OSVersion: ociplat.OSVersion, + Variant: ociplat.Variant, + }, + Created: &created, + Config: ocispec.ImageConfig{}, + RootFS: ocispec.RootFS{ + Type: "layers", + DiffIDs: []digest.Digest{diffID}, + }, + History: []ocispec.History{{ + Created: &created, + Comment: options.Message, + }}, + } + + manifestDesc, _, err := writeConfigAndManifest(ctx, cs, snapshotter, imgConfig, []ocispec.Descriptor{layerDesc}) + if err != nil { + return zero, err + } + + storedName := options.Reference + if storedName == "" { + storedName = manifestDesc.Digest.String() + } else if refParsed, err := referenceutil.Parse(storedName); err == nil { + if refParsed.ExplicitTag == "" { + storedName = refParsed.FamiliarName() + ":latest" + } + if p2, err := referenceutil.Parse(storedName); err == nil { + storedName = p2.String() + } + } + name := storedName + + img := images.Image{ + Name: name, + Target: manifestDesc, + CreatedAt: time.Now(), + } + if _, err := client.ImageService().Update(ctx, img); err != nil { + if !errdefs.IsNotFound(err) { + return zero, err + } + if _, err := client.ImageService().Create(ctx, img); err != nil { + return zero, err + } + } + + cimg := containerd.NewImage(client, img) + if err := cimg.Unpack(ctx, snapshotter); err != nil { + return zero, err + } + return img, nil +} + +func randomRef(prefix string) string { + var b [6]byte + _, _ = rand.Read(b[:]) + return prefix + base64.RawURLEncoding.EncodeToString(b[:]) +} + +func writeConfigAndManifest(ctx context.Context, cs content.Store, snapshotter string, config ocispec.Image, layers []ocispec.Descriptor) (ocispec.Descriptor, digest.Digest, error) { + configJSON, err := json.Marshal(config) + if err != nil { + return ocispec.Descriptor{}, "", err + } + configDesc := ocispec.Descriptor{ + MediaType: images.MediaTypeDockerSchema2Config, + Digest: digest.FromBytes(configJSON), + Size: int64(len(configJSON)), + } + + gcLabel := map[string]string{} + if len(config.RootFS.DiffIDs) > 0 && snapshotter != "" { + gcLabel[fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snapshotter)] = identity.ChainID(config.RootFS.DiffIDs).String() + } + if err := content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(configJSON), configDesc, content.WithLabels(gcLabel)); err != nil && !errdefs.IsAlreadyExists(err) { + return ocispec.Descriptor{}, "", err + } + + manifest := struct { + MediaType string `json:"mediaType,omitempty"` + ocispec.Manifest + }{ + MediaType: images.MediaTypeDockerSchema2Manifest, + Manifest: ocispec.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + Config: configDesc, + Layers: layers, + }, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + return ocispec.Descriptor{}, "", err + } + manifestDesc := ocispec.Descriptor{ + MediaType: images.MediaTypeDockerSchema2Manifest, + Digest: digest.FromBytes(manifestJSON), + Size: int64(len(manifestJSON)), + } + + refLabels := map[string]string{ + "containerd.io/gc.ref.content.0": configDesc.Digest.String(), + } + for i, l := range layers { + refLabels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String() + } + if err := content.WriteBlob(ctx, cs, manifestDesc.Digest.String(), bytes.NewReader(manifestJSON), manifestDesc, content.WithLabels(refLabels)); err != nil && !errdefs.IsAlreadyExists(err) { + return ocispec.Descriptor{}, "", err + } + + return manifestDesc, configDesc.Digest, nil +} From 09f3b205544f83726ed8a75dcd35298f35d6a2a4 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 15:24:08 +0800 Subject: [PATCH 183/868] image: add unit tests for image import command add unit tests for image import command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/image/image_import_linux_test.go | 202 +++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 cmd/nerdctl/image/image_import_linux_test.go diff --git a/cmd/nerdctl/image/image_import_linux_test.go b/cmd/nerdctl/image/image_import_linux_test.go new file mode 100644 index 00000000000..49d7b64fa8d --- /dev/null +++ b/cmd/nerdctl/image/image_import_linux_test.go @@ -0,0 +1,202 @@ +/* + 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 ( + "archive/tar" + "bytes" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// minimalRootfsTar returns a valid tar archive with no files. +func minimalRootfsTar(t *testing.T) *bytes.Buffer { + t.Helper() + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + assert.NilError(t, tw.Close()) + return buf +} + +func TestImageImportErrors(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImageImportErrors", + Require: require.Linux, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("import", "", "image:tag") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("error"))}, + } + }, + Data: test.WithLabels(map[string]string{ + "error": "no such file or directory", + }), + } + + testCase.Run(t) +} + +func TestImageImport(t *testing.T) { + testCase := nerdtest.Setup() + + var urlServer *httptest.Server + + testCase.SubTests = []*test.Case{ + { + Description: "image import from stdin", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("import", "-", data.Identifier()) + cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + identifier := data.Identifier() + return &test.Expected{ + Output: expect.All( + func(stdout string, t tig.T) { + imgs := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgs, identifier)) + }, + ), + } + }, + }, + { + Description: "image import from file", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + p := filepath.Join(data.Temp().Path(), "rootfs.tar") + assert.NilError(t, os.WriteFile(p, minimalRootfsTar(t).Bytes(), 0644)) + data.Labels().Set("tar", p) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("import", data.Labels().Get("tar"), data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + identifier := data.Identifier() + return &test.Expected{ + Output: expect.All( + func(stdout string, t tig.T) { + imgs := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgs, identifier)) + }, + ), + } + }, + }, + { + Description: "image import with message", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("import", "-m", "A message", "-", data.Identifier()) + cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + identifier := data.Identifier() + ":latest" + return &test.Expected{ + Output: expect.All( + func(stdout string, t tig.T) { + img := nerdtest.InspectImage(helpers, identifier) + assert.Equal(t, img.Comment, "A message") + }, + ), + } + }, + }, + { + Description: "image import with platform", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("import", "--platform", "linux/amd64", "-", data.Identifier()) + cmd.Feed(bytes.NewReader(minimalRootfsTar(t).Bytes())) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + identifier := data.Identifier() + ":latest" + return &test.Expected{ + Output: expect.All( + func(stdout string, t tig.T) { + img := nerdtest.InspectImage(helpers, identifier) + assert.Equal(t, img.Architecture, "amd64") + assert.Equal(t, img.Os, "linux") + }, + ), + } + }, + }, + { + Description: "image import from URL", + Cleanup: func(data test.Data, helpers test.Helpers) { + if urlServer != nil { + urlServer.Close() + } + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + urlServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-tar") + _, _ = w.Write(minimalRootfsTar(t).Bytes()) + })) + data.Labels().Set("url", urlServer.URL) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("import", data.Labels().Get("url"), data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + identifier := data.Identifier() + return &test.Expected{ + Output: expect.All( + func(stdout string, t tig.T) { + imgs := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgs, identifier)) + }, + ), + } + }, + }, + } + testCase.Run(t) +} From 4f292f03cb63dc98740617f81ed5073753c86350 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 8 Aug 2025 16:57:35 +0800 Subject: [PATCH 184/868] docs: add import command reference Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 084719763a5..f9744bbc9b5 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -44,6 +44,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl push](#whale-nerdctl-push) - [:whale: nerdctl load](#whale-nerdctl-load) - [:whale: nerdctl save](#whale-nerdctl-save) + - [:whale: nerdctl import](#whale-nerdctl-import) - [:whale: nerdctl tag](#whale-nerdctl-tag) - [:whale: nerdctl rmi](#whale-nerdctl-rmi) - [:whale: nerdctl image inspect](#whale-nerdctl-image-inspect) @@ -905,6 +906,19 @@ Flags: - :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform - :nerd_face: `--all-platforms`: Export content for all platforms +### :whale: nerdctl import + +Import the contents from a tarball to create a filesystem image. + +Usage: `nerdctl import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]` + +Flags: + +- :whale: `-m, --message`: Set commit message for imported image +- :nerd_face: `--platform=(linux/amd64|linux/arm64|...)`: Set platform for the imported image + +Unimplemented `docker import` flags: `--change` + ### :whale: nerdctl tag Create a tag TARGET\_IMAGE that refers to SOURCE\_IMAGE. @@ -1919,7 +1933,6 @@ Container management: Image: -- `docker import` - `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign|notation` and `nerdctl push --sign=cosign|notation`. See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md).) Network management: From c01827dd8740f8a27a6d00ecba91531fbf3b4379 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 18 Aug 2025 17:13:44 +0800 Subject: [PATCH 185/868] unittest: Add helper to run a cross-namespace HTTP server Add helper to run a cross-namespace HTTP server. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/image/image_import_linux_test.go | 17 +++++++----- pkg/testutil/nerdtest/utilities_linux.go | 28 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/cmd/nerdctl/image/image_import_linux_test.go b/cmd/nerdctl/image/image_import_linux_test.go index 49d7b64fa8d..7052c101a8e 100644 --- a/cmd/nerdctl/image/image_import_linux_test.go +++ b/cmd/nerdctl/image/image_import_linux_test.go @@ -21,7 +21,6 @@ import ( "bytes" "errors" "net/http" - "net/http/httptest" "os" "path/filepath" "strings" @@ -72,7 +71,7 @@ func TestImageImportErrors(t *testing.T) { func TestImageImport(t *testing.T) { testCase := nerdtest.Setup() - var urlServer *httptest.Server + var stopServer func() testCase.SubTests = []*test.Case{ { @@ -170,17 +169,21 @@ func TestImageImport(t *testing.T) { { Description: "image import from URL", Cleanup: func(data test.Data, helpers test.Helpers) { - if urlServer != nil { - urlServer.Close() + if stopServer != nil { + stopServer() + stopServer = nil } helpers.Anyhow("rmi", "-f", data.Identifier()) }, Setup: func(data test.Data, helpers test.Helpers) { - urlServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-tar") _, _ = w.Write(minimalRootfsTar(t).Bytes()) - })) - data.Labels().Set("url", urlServer.URL) + }) + url, stop, err := nerdtest.StartHTTPServer(handler) + assert.NilError(t, err) + stopServer = stop + data.Labels().Set("url", url) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("import", data.Labels().Get("url"), data.Identifier()) diff --git a/pkg/testutil/nerdtest/utilities_linux.go b/pkg/testutil/nerdtest/utilities_linux.go index 017dd1adb00..0c996d77ce9 100644 --- a/pkg/testutil/nerdtest/utilities_linux.go +++ b/pkg/testutil/nerdtest/utilities_linux.go @@ -17,6 +17,9 @@ package nerdtest import ( + "net" + "net/http" + "net/http/httptest" "os" "strconv" "strings" @@ -26,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) const SignalCaught = "received" @@ -73,3 +77,27 @@ func RunSigProxyContainer(signal os.Signal, exitOnSignal bool, args []string, da return cmd } + +// StartHTTPServer starts an HTTP server bound to 0.0.0.0 and returns a URL reachable +// from processes that cannot access 127.0.0.1 due to namespace isolation. +// It also returns a cleanup function that stops the server. +func StartHTTPServer(handler http.Handler) (url string, stop func(), err error) { + l, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + return "", nil, err + } + srv := &httptest.Server{Config: &http.Server{Handler: handler}} + srv.Listener = l + srv.Start() + hostIP, herr := nettestutil.NonLoopbackIPv4() + if herr != nil { + srv.Close() + return "", nil, herr + } + _, port, perr := net.SplitHostPort(l.Addr().String()) + if perr != nil { + srv.Close() + return "", nil, perr + } + return "http://" + hostIP.String() + ":" + port, func() { srv.Close() }, nil +} From e1cabc4a71ecaef3653c3eef31ea500c16c13cca Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 18 Aug 2025 17:54:43 +0800 Subject: [PATCH 186/868] manifest: normalize references by adding docker.io/library prefix normalize references by adding docker.io/library prefix in output Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest_create_linux_test.go | 4 ++-- pkg/cmd/manifest/create.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/manifest/manifest_create_linux_test.go b/cmd/nerdctl/manifest/manifest_create_linux_test.go index 5873eb303d0..d3588facd2f 100644 --- a/cmd/nerdctl/manifest/manifest_create_linux_test.go +++ b/cmd/nerdctl/manifest/manifest_create_linux_test.go @@ -92,7 +92,7 @@ func TestManifestCreate(t *testing.T) { } }, Data: test.WithLabels(map[string]string{ - "output": "Created manifest list ", + "output": "Created manifest list docker.io/library/" + manifestListName, }), }, { @@ -126,7 +126,7 @@ func TestManifestCreate(t *testing.T) { } }, Data: test.WithLabels(map[string]string{ - "output": "Created manifest list", + "output": "Created manifest list docker.io/library/" + manifestListName + "-with-amend-flag", }), }, } diff --git a/pkg/cmd/manifest/create.go b/pkg/cmd/manifest/create.go index 3fc2e168651..58774631b7a 100644 --- a/pkg/cmd/manifest/create.go +++ b/pkg/cmd/manifest/create.go @@ -82,5 +82,5 @@ func Create(ctx context.Context, listRef string, manifestRefs []string, options } } - return listRef, nil + return parsedListRef.String(), nil } From c031c5f8b63ec2adac815489e935dbb503581a15 Mon Sep 17 00:00:00 2001 From: Shubhranshu Mahapatra Date: Tue, 19 Aug 2025 01:01:12 -0700 Subject: [PATCH 187/868] feat: Add image info to inspect commands Signed-off-by: Shubhranshu Mahapatra --- .../container/container_inspect_linux_test.go | 31 +++++++++++++++++++ cmd/nerdctl/image/image_inspect_test.go | 12 +++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 4 ++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 855abe82aaa..ad43f0bf87e 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -17,6 +17,7 @@ package container import ( + "encoding/json" "fmt" "os" "os/exec" @@ -29,6 +30,7 @@ import ( "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/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" @@ -185,6 +187,35 @@ func TestContainerInspectContainsInternalLabel(t *testing.T) { assert.Equal(base.T, expectedLabelMount, labelMount) } +func TestContainerInspectConfigImage(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "Container inspect contains Config.Image field", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var containers []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &containers) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(containers), "Expected exactly one container in inspect output") + + container := containers[0] + assert.Assert(t, container.Config != nil, "container Config should not be nil") + assert.Assert(t, container.Config.Image != "", "Config.Image should not be empty") + }), + } + + testCase.Run(t) +} + func TestContainerInspectState(t *testing.T) { t.Parallel() testContainer := testutil.Identifier(t) diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 5ee549686c6..68124c53d34 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -66,6 +66,18 @@ func TestImageInspectSimpleCases(t *testing.T) { Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}"), Expected: test.Expects(0, nil, nil), }, + { + Description: "Config.Image field is set", + Command: test.Command("image", "inspect", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + var dc []dockercompat.Image + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") + assert.Assert(t, dc[0].Config != nil, "image Config should not be nil") + assert.Assert(t, dc[0].Config.Image != "", "Config.Image should not be empty") + }), + }, { Description: "Error for image not found", Command: test.Command("image", "inspect", "dne:latest", "dne2:latest"), diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 407d7985ab6..8077d72886a 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -215,7 +215,7 @@ type Config struct { Cmd []string `json:",omitempty"` // Command to run when starting the container Healthcheck *healthcheck.Healthcheck `json:",omitempty"` // Healthcheck describes how to check the container is healthy // TODO: ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific). - // TODO: Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) + Image string `json:",omitempty"` // Name of the image as it was passed by the operator (e.g. could be symbolic) Volumes map[string]struct{} `json:",omitempty"` // List of volumes (mounts) used for the container WorkingDir string `json:",omitempty"` // Current directory (PWD) in the command will be launched Entrypoint []string `json:",omitempty"` // Entrypoint to run when starting the container @@ -549,6 +549,7 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.State = cs c.Config = &Config{ Labels: n.Labels, + Image: c.Image, } if n.Labels[labels.Hostname] != "" { hostname = n.Labels[labels.Hostname] @@ -648,6 +649,7 @@ func ImageFromNative(nativeImage *native.Image) (*Image, error) { Entrypoint: imgOCI.Config.Entrypoint, Labels: imgOCI.Config.Labels, ExposedPorts: portSet, + Image: nativeImage.Image.Name, } // Add health check if present in labels From 39a1ba844b58563e5abbd2421c2fb226d20adb1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:10:18 +0000 Subject: [PATCH 188/868] build(deps): bump go.uber.org/mock from 0.5.2 to 0.6.0 Bumps [go.uber.org/mock](https://github.com/uber/mock) from 0.5.2 to 0.6.0. - [Release notes](https://github.com/uber/mock/releases) - [Changelog](https://github.com/uber-go/mock/blob/main/CHANGELOG.md) - [Commits](https://github.com/uber/mock/compare/v0.5.2...v0.6.0) --- updated-dependencies: - dependency-name: go.uber.org/mock dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index b58102d8296..daf8cde5431 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( 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.uber.org/mock v0.5.2 + go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.41.0 golang.org/x/net v0.43.0 @@ -136,7 +136,7 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.26.0 // indirect + golang.org/x/mod v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect //gomodjail:unconfined google.golang.org/grpc v1.72.2 // indirect diff --git a/go.sum b/go.sum index a02f9d57aed..7c02c493c34 100644 --- a/go.sum +++ b/go.sum @@ -352,8 +352,8 @@ 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= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -379,8 +379,8 @@ 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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 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= @@ -473,8 +473,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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= From 23a73bbdeab743360bf2267669f7cfb9118368a3 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Tue, 5 Aug 2025 09:29:02 +0800 Subject: [PATCH 189/868] test:add ci for cleanup fifos Signed-off-by: ningmingxiao --- .../container/container_stop_linux_test.go | 20 +++++++++ pkg/containerutil/containerutil.go | 41 ++++++++++--------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index 19eb58bf9a4..e2f581a1ca8 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -199,3 +199,23 @@ func TestStopWithTimeout(t *testing.T) { // The container should get the SIGKILL before the 10s default timeout assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag") } +func TestStopCleanupFIFOs(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("/run/containerd/fifo/ doesn't exist on rootless") + } + testutil.DockerIncompatible(t) + base := testutil.NewBase(t) + testContainerName := testutil.Identifier(t) + oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + // Stop the container after 2 seconds + go func() { + time.Sleep(2 * time.Second) + base.Cmd("stop", testContainerName).AssertOK() + newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + assert.Equal(t, oldNumFifos, newNumFifos) + }() + // Start a container that is automatically removed after it exits + base.Cmd("run", "--rm", "--name", testContainerName, testutil.NginxAlpineImage).AssertOK() +} diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 64352f75e7b..1a3601341c5 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -385,6 +385,12 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur switch status.Status { case containerd.Created, containerd.Stopped: + // Cleanup the IO after a successful Stop + if io := task.IO(); io != nil { + if cerr := io.Close(); cerr != nil { + log.G(ctx).Warnf("failed to close IO for container %s: %v", container.ID(), cerr) + } + } return nil case containerd.Paused, containerd.Pausing: paused = true @@ -397,6 +403,13 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } + // signal will be sent once resume is finished + if paused { + if err := task.Resume(ctx); err != nil { + log.G(ctx).Errorf("cannot unpause container %s: %s", container.ID(), err) + return err + } + } if *timeout > 0 { sig, err := getSignal(signalValue, l) if err != nil { @@ -407,20 +420,10 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } - // signal will be sent once resume is finished - if paused { - if err := task.Resume(ctx); err != nil { - log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) - } else { - // no need to do it again when send sigkill signal - paused = false - } - } - sigtermCtx, sigtermCtxCancel := context.WithTimeout(ctx, *timeout) defer sigtermCtxCancel() - err = waitContainerStop(sigtermCtx, exitCh, container.ID()) + err = waitContainerStop(sigtermCtx, task, exitCh, container.ID()) if err == nil { return nil } @@ -439,13 +442,7 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } - // signal will be sent once resume is finished - if paused { - if err := task.Resume(ctx); err != nil { - log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) - } - } - return waitContainerStop(ctx, exitCh, container.ID()) + return waitContainerStop(ctx, task, exitCh, container.ID()) } func getSignal(signalValue string, containerLabels map[string]string) (syscall.Signal, error) { @@ -460,7 +457,7 @@ func getSignal(signalValue string, containerLabels map[string]string) (syscall.S return signal.ParseSignal("SIGTERM") } -func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error { +func waitContainerStop(ctx context.Context, task containerd.Task, exitCh <-chan containerd.ExitStatus, id string) error { select { case <-ctx.Done(): if err := ctx.Err(); err != nil { @@ -468,6 +465,12 @@ func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, } return nil case status := <-exitCh: + // Cleanup the IO after a successful Stop + if io := task.IO(); io != nil { + if cerr := io.Close(); cerr != nil { + log.G(ctx).Warnf("failed to close IO for container %s: %v", id, cerr) + } + } return status.Error() } } From 21722134fee045d55de0de77fd0d323fbfee5d58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:15:27 +0000 Subject: [PATCH 190/868] build(deps): bump github.com/coreos/go-systemd/v22 from 22.5.0 to 22.6.0 Bumps [github.com/coreos/go-systemd/v22](https://github.com/coreos/go-systemd) from 22.5.0 to 22.6.0. - [Release notes](https://github.com/coreos/go-systemd/releases) - [Commits](https://github.com/coreos/go-systemd/compare/v22.5.0...v22.6.0) --- updated-dependencies: - dependency-name: github.com/coreos/go-systemd/v22 dependency-version: 22.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index daf8cde5431..3b8062c33e9 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.7.1 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined - github.com/coreos/go-systemd/v22 v22.5.0 + github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v28.3.3+incompatible //gomodjail:unconfined diff --git a/go.sum b/go.sum index 7c02c493c34..44239e38983 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpV github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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= @@ -125,7 +125,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= From 6f5acc0cf16750d07f56f4df41d79fa3d45d27b4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 19 Aug 2025 19:18:46 +0900 Subject: [PATCH 191/868] MAINTAINERS: add Chengyu Zhu (ChengyuZhu6) as a REVIEWER Signed-off-by: Akihiro Suda --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index ab9d1a97d77..0999f7c2b80 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -22,6 +22,7 @@ # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" +"ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","" # EMERITUS # See EMERITUS.md From 1b2bfb92011306210a137633daf87762e5a4a3a8 Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Thu, 21 Aug 2025 22:21:22 +0000 Subject: [PATCH 192/868] ci: Load br_netfilter module in linux runners Signed-off-by: Swagat Bora --- .github/workflows/job-test-in-container.yml | 4 ++++ .github/workflows/job-test-in-host.yml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index da776b86db5..be742f8ab00 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -70,6 +70,10 @@ jobs: - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + - name: "Init: install br-netfilter" + run: | + # This ensures that bridged traffic goes through netfilter + sudo modprobe br-netfilter - name: "Init: register QEMU (tonistiigi/binfmt)" run: | # `--install all` will only install emulation for architectures that cannot be natively executed diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 932a070e00d..5d944a72dab 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -156,6 +156,9 @@ jobs: sudo apt-get install -qq expect echo "::endgroup::" + # This ensures that bridged traffic goes through netfilter + sudo modprobe br-netfilter + - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" env: From 5fde64dc52cb4c4ea3219cbabc07c54042232c30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:07:54 +0000 Subject: [PATCH 193/868] build(deps): bump github.com/fluent/fluent-logger-golang Bumps [github.com/fluent/fluent-logger-golang](https://github.com/fluent/fluent-logger-golang) from 1.10.0 to 1.10.1. - [Changelog](https://github.com/fluent/fluent-logger-golang/blob/master/CHANGELOG.md) - [Commits](https://github.com/fluent/fluent-logger-golang/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: github.com/fluent/fluent-logger-golang dependency-version: 1.10.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3b8062c33e9..3d3163740be 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.18.0 //gomodjail:unconfined - github.com/fluent/fluent-logger-golang v1.10.0 + github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.4.0 github.com/ipfs/go-cid v0.5.0 @@ -115,7 +115,7 @@ require ( github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect diff --git a/go.sum b/go.sum index 44239e38983..ed77a9446da 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluent/fluent-logger-golang v1.10.0 h1:JcLj8u3WclQv2juHGKTSzBRM5vIZjEqbrmvn/n+m1W0= -github.com/fluent/fluent-logger-golang v1.10.0/go.mod h1:UNyv8FAGmQcYJRtk+yfxhWqWUwsabTipgjXvBDR8kTs= +github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ= +github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= @@ -259,8 +259,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/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.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= From 457bd8301249fb21bd4f7803fd2e186098cad61f Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Wed, 27 Aug 2025 02:28:47 +0000 Subject: [PATCH 194/868] ci: install br_netfilter for test-in-lima jobs Signed-off-by: Swagat Bora --- .github/workflows/job-test-in-lima.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 4690f83766d..27d3fae765e 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -75,6 +75,10 @@ jobs: docker info docker version + - name: "Init: install br-netfilter in the guest VM" + run: | + lima sudo modprobe br-netfilter + - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 From 45816cd7d5f5f617347237d9799d5d5c8d58b347 Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Tue, 10 Jun 2025 23:18:59 +0000 Subject: [PATCH 195/868] feat: add support for com.docker.network.bridge.enable_icc network option Signed-off-by: Swagat Bora --- .../network/network_create_linux_test.go | 98 +++++++++++++++++++ docs/command-reference.md | 2 + pkg/netutil/cni_plugin_unix.go | 9 +- pkg/netutil/netutil_unix.go | 36 +++++-- pkg/netutil/netutil_windows.go | 5 + pkg/testutil/nerdtest/requirements.go | 23 +++++ 6 files changed, 164 insertions(+), 9 deletions(-) diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index 843ec56c44b..f9c35c845aa 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -26,6 +26,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -159,3 +160,100 @@ func TestNetworkCreate(t *testing.T) { testCase.Run(t) } + +func TestNetworkCreateICC(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Linux, + ) + + testCase.SubTests = []*test.Case{ + { + Description: "with enable_icc=false", + Require: nerdtest.CNIFirewallVersion("1.7.1"), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + // Create a network with ICC disabled + helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", + "--opt", "com.docker.network.bridge.enable_icc=false") + + // Run a container in that network + data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), + "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) + + // Wait for container to be running + nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // DEBUG: Check br_netfilter module status + helpers.Custom("sh", "-ec", "lsmod | grep br_netfilter || echo 'br_netfilter not loaded'").Run(&test.Expected{}) + helpers.Custom("sh", "-ec", "cat /proc/sys/net/bridge/bridge-nf-call-iptables 2>/dev/null || echo 'bridge-nf-call-iptables not available'").Run(&test.Expected{}) + helpers.Custom("sh", "-ec", "ls /proc/sys/net/bridge/ 2>/dev/null || echo 'bridge sysctl not available'").Run(&test.Expected{}) + // Try to ping the other container in the same network + // This should fail when ICC is disabled + return helpers.Command("run", "--rm", "--net", data.Identifier(), + testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), // Expect ping to fail with exit code 1 + }, + { + Description: "with enable_icc=true", + Require: nerdtest.CNIFirewallVersion("1.7.1"), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + // Create a network with ICC enabled (default) + helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", + "--opt", "com.docker.network.bridge.enable_icc=true") + + // Run a container in that network + data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), + "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) + // Wait for container to be running + nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Try to ping the other container in the same network + // This should succeed when ICC is enabled + return helpers.Command("run", "--rm", "--net", data.Identifier(), + testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) + }, + Expected: test.Expects(0, nil, nil), // Expect ping to succeed with exit code 0 + }, + { + Description: "with no enable_icc option set", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + // Create a network with ICC enabled (default) + helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge") + + // Run a container in that network + data.Labels().Set("container1", helpers.Capture("run", "-d", "--net", data.Identifier(), + "--name", data.Identifier("c1"), testutil.CommonImage, "sleep", "infinity")) + // Wait for container to be running + nerdtest.EnsureContainerStarted(helpers, data.Identifier("c1")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier("c1")) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Try to ping the other container in the same network + // This should succeed when no ICC is set + return helpers.Command("run", "--rm", "--net", data.Identifier(), + testutil.CommonImage, "ping", "-c", "1", "-W", "1", data.Identifier("c1")) + }, + Expected: test.Expects(0, nil, nil), // Expect ping to succeed with exit code 0 + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index c5d6863de1a..a3a8979f8de 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1193,6 +1193,8 @@ Flags: - :whale: `-o, --opt`: Set driver specific options - :whale: `--opt=com.docker.network.driver.mtu=`: Set the containers network MTU - :nerd_face: `--opt=mtu=`: Alias of `--opt=com.docker.network.driver.mtu=` + - :whale: `--opt=com.docker.network.bridge.enable_icc=`: Enable or Disable inter-container connectivity + - :nerd_face: `--opt=icc=`: Alias of `--opt=com.docker.network.bridge.enable_icc` - :whale: `--opt=macvlan_mode=(bridge)>`: Set macvlan network mode (default: bridge) - :whale: `--opt=ipvlan_mode=(l2|l3)`: Set IPvlan network mode (default: l2) - :nerd_face: `--opt=mode=(bridge|l2|l3)`: Alias of `--opt=macvlan_mode=(bridge)` and `--opt=ipvlan_mode=(l2|l3)` diff --git a/pkg/netutil/cni_plugin_unix.go b/pkg/netutil/cni_plugin_unix.go index 8d863d3be93..2851c7b5a3a 100644 --- a/pkg/netutil/cni_plugin_unix.go +++ b/pkg/netutil/cni_plugin_unix.go @@ -95,13 +95,18 @@ type firewallConfig struct { // IngressPolicy is supported since firewall plugin v1.1.0. // "same-bridge" mode replaces the deprecated "isolation" plugin. + // "isolated" mode has been added since firewall plugin v1.7.1 IngressPolicy string `json:"ingressPolicy,omitempty"` } -func newFirewallPlugin() *firewallConfig { +func newFirewallPlugin(ingressPolicy string) *firewallConfig { + if ingressPolicy != "same-bridge" && ingressPolicy != "isolated" { + ingressPolicy = "same-bridge" // Default to "same-bridge" if invalid value provided + } + c := &firewallConfig{ PluginType: "firewall", - IngressPolicy: "same-bridge", + IngressPolicy: ingressPolicy, } if rootlessutil.IsRootless() { // https://github.com/containerd/nerdctl/issues/2818 diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index f71dc100742..046c173d122 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -99,6 +99,7 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] case "bridge": mtu := 0 iPMasq := true + icc := true for opt, v := range opts { switch opt { case "mtu", "com.docker.network.driver.mtu": @@ -111,6 +112,11 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] if err != nil { return nil, err } + case "icc", "com.docker.network.bridge.enable_icc": + icc, err = strconv.ParseBool(v) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported %q network option %q", driver, opt) } @@ -133,14 +139,29 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] if ipv6 { bridge.Capabilities["ips"] = true } + + // Determine the appropriate firewall ingress policy based on icc setting + ingressPolicy := "same-bridge" // Default policy + firewallPath := filepath.Join(e.Path, "firewall") + if !icc { + // Check if firewall plugin supports the "isolated" policy (v1.7.1+) + ok, err := FirewallPluginGEQVersion(firewallPath, "v1.7.1") + if err != nil { + log.L.WithError(err).Warnf("Failed to detect whether %q is newer than v1.7.1", firewallPath) + } else if ok { + ingressPolicy = "isolated" + } else { + log.L.Warnf("To use 'isolated' ingress policy, CNI plugin \"firewall\" (>= 1.7.1) needs to be installed in CNI_PATH (%q), see https://www.cni.dev/plugins/current/meta/firewall/", e.Path) + } + } + if internal { - plugins = []CNIPlugin{bridge, newFirewallPlugin(), newTuningPlugin()} + plugins = []CNIPlugin{bridge, newFirewallPlugin(ingressPolicy), newTuningPlugin()} } else { - plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} + plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(ingressPolicy), newTuningPlugin()} } if name != DefaultNetworkName { - firewallPath := filepath.Join(e.Path, "firewall") - ok, err := firewallPluginGEQ110(firewallPath) + ok, err := FirewallPluginGEQVersion(firewallPath, "v1.1.0") if err != nil { log.L.WithError(err).Warnf("Failed to detect whether %q is newer than v1.1.0", firewallPath) } @@ -291,7 +312,8 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateway, ipRange string, ipv6 return ranges, findIPv4, nil } -func firewallPluginGEQ110(firewallPath string) (bool, error) { +// FirewallPluginGEQVersion checks if the firewall plugin is greater than or equal to the specified version +func FirewallPluginGEQVersion(firewallPath string, versionStr string) (bool, error) { // TODO: guess true by default in 2023 guessed := false @@ -320,8 +342,8 @@ func firewallPluginGEQ110(firewallPath string) (bool, error) { if err != nil { return guessed, fmt.Errorf("failed to guess the version of %q: %w", firewallPath, err) } - ver110 := semver.MustParse("v1.1.0") - return ver.GreaterThan(ver110) || ver.Equal(ver110), nil + targetVer := semver.MustParse(versionStr) + return ver.GreaterThan(targetVer) || ver.Equal(targetVer), nil } // guessFirewallPluginVersion guess the version of the CNI firewall plugin (not the version of the implemented CNI spec). diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index bd03e6ec4aa..484b03c9b77 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -18,6 +18,7 @@ package netutil import ( "encoding/json" + "errors" "fmt" "net" @@ -95,3 +96,7 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRan } return ipam, nil } + +func FirewallPluginGEQVersion(firewallPath string, versionStr string) (bool, error) { + return false, errors.New("unsupported in windows") +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 46bbeee675d..40e3e08e955 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "os/exec" + "path/filepath" "strings" "github.com/Masterminds/semver/v3" @@ -32,8 +33,10 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/clientutil" + ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -447,3 +450,23 @@ func ContainerdVersion(v string) *test.Requirement { }, } } + +// CNIFirewallVersion checks if the CNI firewall plugin version is greater than or equal to the specified version +func CNIFirewallVersion(requiredVersion string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + cniPath := ncdefaults.CNIPath() + firewallPath := filepath.Join(cniPath, "firewall") + ok, err := netutil.FirewallPluginGEQVersion(firewallPath, requiredVersion) + if err != nil { + return false, fmt.Sprintf("Failed to check CNI firewall version: %v", err) + } + + if !ok { + return false, fmt.Sprintf("CNI firewall plugin version is less than required version %s", requiredVersion) + } + + return true, fmt.Sprintf("CNI firewall plugin version is greater than or equal to required version %s", requiredVersion) + }, + } +} From 8d8869f9d0fef7bb706b71c6192fbb9ddcb27081 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 17:50:30 +0900 Subject: [PATCH 196/868] CI: remove almalinux-9 due to flakiness Signed-off-by: Akihiro Suda --- .github/workflows/workflow-flaky.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 9851047f308..9165f372813 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -17,12 +17,12 @@ jobs: strategy: fail-fast: false # EL8 is used for testing compatibility with cgroup v1. - # Unfortunately, EL8 is hard to debug for M1 users (as Lima+M1+EL8 is not runnable because of page size), + # Unfortunately, EL8 is hard to debug for ARM Mac users (as Lima+ARM Mac+EL8 is not runnable because of page size), # and it currently shows numerous issues. - # Thus, EL9 is also added as target (for a limited time?) so that we can figure out which issues are EL8 specific, - # and which issues could be reproduced on EL9 as well (which would be easier to debug). + # ARM Mac users may use oraclelinux-8 instead for debugging cgroup v1 issues, although its kernel is different from + # other EL8 variants. matrix: - guest: ["almalinux-8", "almalinux-9"] + guest: ["almalinux-8"] target: ["rootful", "rootless"] with: timeout: 60 From ec3ffd57e824f1b2f4a9cd272d098d0fb763de16 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 17:55:22 +0900 Subject: [PATCH 197/868] CI: remove containerd v1.6 as it reached EOL Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 6 +++--- docs/dev/auditing_dockerfile.md | 4 +--- hack/generate-release-note.sh | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index de5799d5786..c8de814ee5e 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -48,7 +48,7 @@ jobs: - runner: ubuntu-24.04-arm # Additionally build for old containerd on amd - runner: ubuntu-24.04 - containerd-version: v1.6.38 + containerd-version: v1.7.28 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -75,7 +75,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.6.38 + containerd-version: v1.7.28 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-24.04 @@ -91,7 +91,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.6.38 + containerd-version: v1.7.28 # ipv6 - runner: ubuntu-24.04 target: rootful diff --git a/docs/dev/auditing_dockerfile.md b/docs/dev/auditing_dockerfile.md index 37034fd981d..b323caf78fa 100644 --- a/docs/dev/auditing_dockerfile.md +++ b/docs/dev/auditing_dockerfile.md @@ -103,9 +103,7 @@ ci_run(){ local no_cache="${1:-}" export UBUNTU_VERSION=24.04 - CONTAINERD_VERSION=v1.6.36 run "$no_cache" arm64 Dockerfile.origin build-dependencies - UBUNTU_VERSION=20.04 CONTAINERD_VERSION=v1.6.36 run "" arm64 Dockerfile.origin test-integration - + # The actual version may differ CONTAINERD_VERSION=v1.7.25 run "$no_cache" arm64 Dockerfile.origin build-dependencies UBUNTU_VERSION=22.04 CONTAINERD_VERSION=v1.7.25 run "" arm64 Dockerfile.origin test-integration diff --git a/hack/generate-release-note.sh b/hack/generate-release-note.sh index 68f876e17f4..b4e75493397 100755 --- a/hack/generate-release-note.sh +++ b/hack/generate-release-note.sh @@ -25,7 +25,8 @@ cat <<-EOX (To be documented) ## Compatible containerd versions -This release of nerdctl is expected to be used with containerd v1.6, v1.7, v2.0, or v2.1. +This release of nerdctl is expected to be used with containerd v1.7, v2.0, or v2.1. +Some features may not work with other releases of containerd. ## About the binaries - Minimal (\`${minimal_amd64tgz_basename}\`): nerdctl only From c00653c0e1976d8c08f8a5d79b194f8ee41bd2fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:02:02 +0000 Subject: [PATCH 198/868] build(deps): bump actions/attest-build-provenance from 2.4.0 to 3.0.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 3.0.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/e8998f949152b193b063cb0ec769d69d929409be...977bb373ede98d70efdf65b84cb5f73e068dcc2a) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7501eaed669..6341809ef5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From 9c59986dc6a6f0d734a6e4b58d8df08389c2bf86 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 20:03:14 +0900 Subject: [PATCH 199/868] update gomodjail (0.1.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fbcebf855d1..5e3aebc6279 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug ARG BUILDG_VERSION=v0.5.3@BINARY # Extra deps: gomodjail -ARG GOMODJAIL_VERSION=v0.1.2@0a86b34442a491fa8f5e4565e9c846fce310239c +ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash From 035ee1e5200337be8b6c49323711f09c776c4402 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 20:05:58 +0900 Subject: [PATCH 200/868] update golangci-lint (2.4.0); silence new errors Some rules are disabled to silence the errors introduced in this release of golangci-lint. See PR 4490 Signed-off-by: Akihiro Suda --- .golangci.yml | 13 ++++++++++++- Makefile | 4 ++-- mod/tigron/.golangci.yml | 10 ++++++++++ mod/tigron/Makefile | 4 ++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 058e0ec87d3..ec60c924491 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -99,6 +99,9 @@ linters: - name: use-errors-new # 84 occurrences. Improves error testing. disabled: true + - name: struct-tag + # 2 occurrences. + disabled: true ##### P1: consider making a dent on these, but not critical. - name: argument-limit @@ -131,6 +134,9 @@ linters: - name: cognitive-complexity arguments: [205] # 441 occurrences (at default 7). We should try to lower it (involves significant refactoring). + - name: var-naming + # 1 occurrence. + disabled: true ##### P2: nice to have. - name: max-public-structs @@ -155,6 +161,9 @@ linters: - name: exported # 577 occurrences. Forces documentation of any exported symbol. disabled: true + - name: unnecessary-format + # Many occurrences. + disabled: true ###### Permanently disabled. Below have been reviewed and vetted to be unnecessary. - name: line-length-limit @@ -175,6 +184,9 @@ linters: - name: add-constant # 2605 occurrences. Kind of useful in itself, but unacceptable amount of effort to fix disabled: true + - name: enforce-switch-style + # Many occurrences. + disabled: true depguard: rules: @@ -241,7 +253,6 @@ linters: - typeAssertChain - unlabelStmt - builtinShadow - - importShadow - initClause - nestingReduce - unnecessaryBlock diff --git a/Makefile b/Makefile index 1dd5fbd0720..ae9d04de5d6 100644 --- a/Makefile +++ b/Makefile @@ -217,7 +217,7 @@ fix-mod: ########################## install-dev-tools: $(call title, $@) - # golangci: v2.0.2 (2024-03-26) + # golangci: v2.4.0 (2025-08-14) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) @@ -225,7 +225,7 @@ install-dev-tools: # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@43d03392d7dc3746fa776dbddd66dfcccff70651 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d diff --git a/mod/tigron/.golangci.yml b/mod/tigron/.golangci.yml index dfaf0f0c86f..f58a2d37221 100644 --- a/mod/tigron/.golangci.yml +++ b/mod/tigron/.golangci.yml @@ -54,6 +54,10 @@ linters: - sloglint # no slog - testifylint # no testify - zerologlint # no zerolog + - funcorder + - noctx + - noinlineerr + - wsl_v5 settings: interfacebloat: # Default is 10 @@ -91,6 +95,12 @@ linters: - "fmt.Fprint" - "fmt.Fprintln" - "fmt.Fprintf" + - name: redundant-test-main-exit + disabled: true + - name: enforce-switch-style + disabled: true + - name: var-naming + disabled: true depguard: rules: main: diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index de48ce35c39..3613ade3ba8 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -164,14 +164,14 @@ up: ########################## install-dev-tools: $(call title, $@) - # golangci: v2.0.2 (2024-03-26) + # golangci: v2.4.0 (2025-08-14) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@43d03392d7dc3746fa776dbddd66dfcccff70651 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a From 1f8a17516a005ecdd118dc2a1feafbcca10431d9 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 20:47:43 +0900 Subject: [PATCH 201/868] update Kubo (0.37.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5e3aebc6279..10775c01093 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 ARG NYDUS_VERSION=v2.3.2 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 -ARG KUBO_VERSION=v0.35.0 +ARG KUBO_VERSION=v0.37.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx From 3afdb13846bf4f6b7fabeae0a03cc102afcd71cb Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 28 Aug 2025 20:08:31 +0900 Subject: [PATCH 202/868] update Go (1.25) Signed-off-by: Akihiro Suda --- .github/workflows/release.yml | 2 +- .github/workflows/workflow-lint.yml | 8 ++++---- .github/workflows/workflow-test.yml | 4 ++-- .github/workflows/workflow-tigron.yml | 2 +- Dockerfile | 2 +- hack/provisioning/kube/kind.sh | 2 +- pkg/testutil/images.yaml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6341809ef5d..9f5e98bff47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: - name: "Install go" uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: "1.24" + go-version: "1.25" check-latest: true - name: "Compile binaries" env: diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index c6d6f6a4e7a..b133b7fe6cf 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -35,7 +35,7 @@ jobs: canary: true with: timeout: 5 - go-version: "1.24" + go-version: "1.25" runner: ubuntu-24.04 # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job # will not run. However, if you test it, it will coerce to `false`, hence: @@ -48,7 +48,7 @@ jobs: uses: ./.github/workflows/job-lint-project.yml with: timeout: 5 - go-version: "1.24" + go-version: "1.25" runner: ubuntu-24.04 # Lint for shell and yaml files @@ -68,10 +68,10 @@ jobs: matrix: include: # Build for both old and stable go - - go-version: "1.23" - go-version: "1.24" + - go-version: "1.25" # Additionally build for canary - - go-version: "1.24" + - go-version: "1.25" canary: true with: timeout: 10 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index c8de814ee5e..b4be8e9d871 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -30,7 +30,7 @@ jobs: canary: ${{ matrix.canary && true || false }} # Windows routinely go over 5 minutes timeout: 10 - go-version: 1.24 + go-version: 1.25 windows-cni-version: v0.3.1 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 @@ -138,7 +138,7 @@ jobs: runner: ${{ matrix.runner }} binary: ${{ matrix.binary != '' && matrix.binary || 'nerdctl' }} canary: ${{ matrix.canary && true || false }} - go-version: 1.24 + go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble containerd-version: 2.1.3 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 7be9d65ac86..7e505057faf 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -9,7 +9,7 @@ on: paths: 'mod/tigron/**' env: - GO_VERSION: "1.24" + GO_VERSION: "1.25" GOTOOLCHAIN: local jobs: diff --git a/Dockerfile b/Dockerfile index 10775c01093..1588e7c62d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash -ARG GO_VERSION=1.24 +ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index d62afcaf905..cabaddab967 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -20,7 +20,7 @@ readonly root # shellcheck source=/dev/null . "$root/../../scripts/lib.sh" -GO_VERSION=1.24 +GO_VERSION=1.25 KIND_VERSION=v0.27.0 CNI_PLUGINS_VERSION=v1.7.1 # shellcheck disable=SC2034 diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 73bac34ea3a..67c0e3a3551 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -32,7 +32,7 @@ fluentd: golang: ref: "golang" - tag: "1.23.8-bookworm" + tag: "1.25.0-trixie" kubo: ref: "ghcr.io/stargz-containers/ipfs/kubo" From 57ccde3da8c7251da5dd2b7dc4d59f8f73081105 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 29 Aug 2025 11:11:27 +0900 Subject: [PATCH 203/868] CI: make timeout of gotestsum shorter than GHA job This will allow gotestsum to timeout before the timeout of GHA Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- hack/test-integration.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index c8de814ee5e..0896fb068aa 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -102,7 +102,7 @@ jobs: canary: true with: - timeout: 60 + timeout: 80 runner: ${{ matrix.runner }} target: ${{ matrix.target }} binary: ${{ matrix.binary && matrix.binary || 'nerdctl' }} diff --git a/hack/test-integration.sh b/hack/test-integration.sh index 3d1a21365a5..cdbeb61957f 100755 --- a/hack/test-integration.sh +++ b/hack/test-integration.sh @@ -26,7 +26,7 @@ if [[ "$(id -u)" = "0" ]]; then fi fi -readonly timeout="60m" +readonly timeout="30m" readonly retries="2" readonly needsudo="${WITH_SUDO:-}" From b7274c24c7d89199736ff21c508516c26947980a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 06:49:41 +0000 Subject: [PATCH 204/868] build(deps): bump the stargz group with 3 updates Bumps the stargz group with 3 updates: [github.com/containerd/stargz-snapshotter](https://github.com/containerd/stargz-snapshotter), [github.com/containerd/stargz-snapshotter/estargz](https://github.com/containerd/stargz-snapshotter) and [github.com/containerd/stargz-snapshotter/ipfs](https://github.com/containerd/stargz-snapshotter). Updates `github.com/containerd/stargz-snapshotter` from 0.16.3 to 0.17.0 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.16.3...v0.17.0) Updates `github.com/containerd/stargz-snapshotter/estargz` from 0.16.3 to 0.17.0 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.16.3...v0.17.0) Updates `github.com/containerd/stargz-snapshotter/ipfs` from 0.16.3 to 0.17.0 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.16.3...v0.17.0) --- updated-dependencies: - dependency-name: github.com/containerd/stargz-snapshotter dependency-version: 0.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/estargz dependency-version: 0.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/ipfs dependency-version: 0.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: stargz ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 31 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 3d3163740be..6d554b021f3 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.23.5 +go 1.24.0 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -22,9 +22,9 @@ require ( github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.2 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter v0.16.3 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/estargz v0.16.3 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/ipfs v0.16.3 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter v0.17.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/estargz v0.17.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/ipfs v0.17.0 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.7.1 //gomodjail:unconfined @@ -108,7 +108,7 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.13.0 // indirect + github.com/multiformats/go-multiaddr v0.16.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect @@ -127,7 +127,7 @@ require ( //gomodjail:unconfined github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.3.0 // indirect - github.com/vbatts/tar-split v0.11.6 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -137,9 +137,9 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.27.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect //gomodjail:unconfined - google.golang.org/grpc v1.72.2 // indirect + google.golang.org/grpc v1.73.0 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ed77a9446da..eba472458ec 100644 --- a/go.sum +++ b/go.sum @@ -53,12 +53,12 @@ github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsW github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4= -github.com/containerd/stargz-snapshotter v0.16.3/go.mod h1:XPOl2oa9zjWidTM2IX191smolwWc3/zkKtp02TzTFb0= -github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/containerd/stargz-snapshotter/ipfs v0.16.3 h1:d6IBSzYo0vlFcujwTqJRwpI3cZgX3E2I6Ev7LtMaZ4M= -github.com/containerd/stargz-snapshotter/ipfs v0.16.3/go.mod h1:d4EuGnC3RteInKAdddUbDOL88uw3vZySSLZ44pbriGM= +github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc= +github.com/containerd/stargz-snapshotter v0.17.0/go.mod h1:ySEul1ck7jCE4jqsuFCo8FFLrHU20UWQeI9g7mdsanI= +github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE= +github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM= +github.com/containerd/stargz-snapshotter/ipfs v0.17.0 h1:Q7UO2U0nKXtQFYVeX8WUmMKXtJR9ZAPgISt/sMEo8Ng= +github.com/containerd/stargz-snapshotter/ipfs v0.17.0/go.mod h1:zRJECfc6IPSr50ljYX36kVmrSd1Wdi3aXLzZhFuhfR4= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -161,9 +161,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -231,8 +230,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= -github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= @@ -311,8 +310,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= -github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -483,15 +482,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-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 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= From c6ce3babea5a29fe369be53ed9f3660cf8a0477d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 29 Aug 2025 22:29:33 +0900 Subject: [PATCH 205/868] update containerd (2.1.4) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 5016fb21ff1..45afa39961c 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -141,9 +141,9 @@ jobs: go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.1.3 + containerd-version: 2.1.4 # Note: these as for amd64 - containerd-sha: 436cc160c33b37ec25b89fb5c72fc879ab2b3416df5d7af240c3e9c2f4065d3c + containerd-sha: 316d510a0428276d931023f72c09fdff1a6ba81d6cc36f31805fea6a3c88f515 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 diff --git a/Dockerfile b/Dockerfile index 1588e7c62d5..2d42b884e7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.1.3@c787fb98911740dd3ff2d0e45ce88cdf01410486 +ARG CONTAINERD_VERSION=v2.1.4@75cb2b7193e4e490e9fbdc236c0e811ccaba3376 ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY From c75cf733a9123170d35e3cf9d24d6f792c1c2e33 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 29 Aug 2025 22:31:22 +0900 Subject: [PATCH 206/868] update stargz-snapshotter (0.17.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 | 3 --- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 create mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 diff --git a/Dockerfile b/Dockerfile index 2d42b884e7c..2b3f84f48a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.23.2@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.17.0@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 deleted file mode 100644 index e9b2bfa457c..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 +++ /dev/null @@ -1,3 +0,0 @@ -516984d13e10396f7f6090c51e4e42cc1af9a0d4b16aa81837bcdb1d5a5608d6 stargz-snapshotter-v0.16.3-linux-amd64.tar.gz -d3ac8215603cfd002901c88c568ff5c0685d6953c012fa6ff709deb50f90b023 stargz-snapshotter-v0.16.3-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 new file mode 100644 index 00000000000..785c3b2acec --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 @@ -0,0 +1,3 @@ +e3cd9aed03a0fc82adc2484a3fe94381d21f52d998419e15ca019744d27e18b7 stargz-snapshotter-v0.17.0-linux-amd64.tar.gz +9b3e85729885d7b5c4a3b7b67a8c8048065f60b2098fec17251f256d49bb24bb stargz-snapshotter-v0.17.0-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service From 807286a1165bc23a9f6ded649a7f96a3812dd1f1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 29 Aug 2025 22:33:20 +0900 Subject: [PATCH 207/868] update Nydus (2.3.5) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2b3f84f48a3..df1f9787bcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 -ARG NYDUS_VERSION=v2.3.2 +ARG NYDUS_VERSION=v2.3.5 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 From 0a4c6cd806cd81efd6a85f114247ba0ff5cdebf1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 29 Aug 2025 22:35:12 +0900 Subject: [PATCH 208/868] update Debian (13) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- docs/dev/auditing_dockerfile.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index df1f9787bcc..0edf11284a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ ARG KUBO_VERSION=v0.37.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base COPY --from=xx / / ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ diff --git a/docs/dev/auditing_dockerfile.md b/docs/dev/auditing_dockerfile.md index b323caf78fa..81a57592e53 100644 --- a/docs/dev/auditing_dockerfile.md +++ b/docs/dev/auditing_dockerfile.md @@ -34,7 +34,7 @@ is the local ip of the Charles proxy (non-localhost) Add the following stages in the dockerfile: ```dockerfile -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS hack-build-base-debian +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS hack-build-base-debian RUN apt-get update -qq; apt-get -qq install ca-certificates COPY charles-ssl-proxying-certificate.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates @@ -52,7 +52,7 @@ RUN update-ca-certificates Then replace any later "FROM" with our modified bases: ``` -golang:${GO_VERSION}-bookworm => hack-build-base-debian +golang:${GO_VERSION}-trixie => hack-build-base-debian golang:${GO_VERSION}-alpine => hack-build-base ubuntu:${UBUNTU_VERSION} => hack-base ``` From ea5a68b1fbad507fbb38d99dbd56984c8ec95896 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:53:49 +0000 Subject: [PATCH 209/868] build(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.1 Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.9.1 to 1.10.1. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-version: 1.10.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 6d554b021f3..e4cddc34312 100644 --- a/go.mod +++ b/go.mod @@ -56,8 +56,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined - github.com/spf13/cobra v1.9.1 //gomodjail:unconfined - github.com/spf13/pflag v1.0.7 //gomodjail:unconfined + github.com/spf13/cobra v1.10.1 //gomodjail:unconfined + github.com/spf13/pflag v1.0.9 //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 diff --git a/go.sum b/go.sum index eba472458ec..05fb4fca517 100644 --- a/go.sum +++ b/go.sum @@ -286,11 +286,10 @@ github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From ffb2084b68e02acd67c10ee7316375bdfe836147 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:53:49 +0000 Subject: [PATCH 210/868] build(deps): bump github.com/containernetworking/plugins Bumps [github.com/containernetworking/plugins](https://github.com/containernetworking/plugins) from 1.7.1 to 1.8.0. - [Release notes](https://github.com/containernetworking/plugins/releases) - [Commits](https://github.com/containernetworking/plugins/compare/v1.7.1...v1.8.0) --- updated-dependencies: - dependency-name: github.com/containernetworking/plugins dependency-version: 1.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 6d554b021f3..0297a480536 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.24.0 +go 1.24.2 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -27,7 +27,7 @@ require ( github.com/containerd/stargz-snapshotter/ipfs v0.17.0 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined - github.com/containernetworking/plugins v1.7.1 //gomodjail:unconfined + github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined @@ -86,7 +86,7 @@ require ( github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -141,7 +141,7 @@ require ( //gomodjail:unconfined google.golang.org/grpc v1.73.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index eba472458ec..7eeca3a1604 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= 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.7.1 h1:CNAR0jviDj6FS5Vg85NTgKWLDzZPfi/lj+VJfhMDTIs= -github.com/containernetworking/plugins v1.7.1/go.mod h1:xuMdjuio+a1oVQsHKjr/mgzuZ24leAsqUYRnzGoXHy0= +github.com/containernetworking/plugins v1.8.0 h1:WjGbV/0UQyo8A4qBsAh6GaDAtu1hevxVxsEuqtBqUFk= +github.com/containernetworking/plugins v1.8.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= @@ -115,8 +115,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= @@ -155,8 +155,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -238,10 +238,10 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= +github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= +github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= +github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -500,8 +500,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From f4f7763e9fba28c1925ebe0f0c65d506c03297c5 Mon Sep 17 00:00:00 2001 From: Swagat Bora Date: Tue, 2 Sep 2025 21:14:39 +0000 Subject: [PATCH 211/868] support all-platforms flag with soci convert Signed-off-by: Swagat Bora --- cmd/nerdctl/image/image_convert.go | 2 ++ cmd/nerdctl/image/image_convert_linux_test.go | 20 ++++++++++++++++++- pkg/api/types/image_types.go | 4 ++++ pkg/cmd/image/convert.go | 2 +- pkg/snapshotterutil/sociutil.go | 8 +++++--- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 49496f09ee7..48a8bed42f9 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -303,6 +303,8 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { SociOptions: types.SociOptions{ SpanSize: sociSpanSize, MinLayerSize: sociMinLayerSize, + Platforms: platforms, + AllPlatforms: allPlatforms, }, }, Stdout: cmd.OutOrStdout(), diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index be24918fb31..07cd2a7003d 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -39,7 +39,7 @@ func TestImageConvert(t *testing.T) { require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("pull", "--quiet", testutil.CommonImage) + helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.CommonImage) }, SubTests: []*test.Case{ { @@ -107,6 +107,24 @@ func TestImageConvert(t *testing.T) { }, Expected: test.Expects(0, nil, nil), }, + { + Description: "soci with all-platforms", + Require: require.All( + require.Not(nerdtest.Docker), + nerdtest.Soci, + nerdtest.SociVersion("0.10.0"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--soci", "--all-platforms", + "--soci-span-size", "2097152", + "--soci-min-layer-size", "0", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, }, } diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 5ff507ccc7c..0ceb3148896 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -310,4 +310,8 @@ type SociOptions struct { SpanSize int64 // Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. MinLayerSize int64 + // Platforms convert content for a specific platform + Platforms []string + // AllPlatforms convert content for all platforms + AllPlatforms bool } diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index 12a2040d598..c197755b3a5 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -171,7 +171,7 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa convertType = "nydus" case soci: // Convert image to SOCI format - convertedRef, err := snapshotterutil.ConvertSociIndexV2(ctx, client, srcRef, targetRef, options.GOptions, options.Platforms, options.SociOptions) + convertedRef, err := snapshotterutil.ConvertSociIndexV2(ctx, client, srcRef, targetRef, options.GOptions, options.SociOptions) if err != nil { return fmt.Errorf("failed to convert image to SOCI format: %w", err) } diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 240ef54737e..82e8773ce66 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -104,7 +104,7 @@ func CheckSociVersion(requiredVersion string) error { } // ConvertSociIndexV2 converts an image to SOCI format and returns the converted image reference with digest -func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) { +func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, sOpts types.SociOptions) (string, error) { // Check if SOCI version is at least 0.10.0 which is required for the convert operation if err := CheckSociVersion("0.10.0"); err != nil { return "", err @@ -117,10 +117,12 @@ func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef strin sociCmd.Args = append(sociCmd.Args, "convert") - if len(platforms) > 0 { + if sOpts.AllPlatforms { + sociCmd.Args = append(sociCmd.Args, "--all-platforms") + } else if len(sOpts.Platforms) > 0 { // multiple values need to be passed as separate, repeating flags in soci as it uses urfave // https://github.com/urfave/cli/blob/main/docs/v2/examples/flags.md#multiple-values-per-single-flag - for _, p := range platforms { + for _, p := range sOpts.Platforms { sociCmd.Args = append(sociCmd.Args, "--platform", p) } } From 2db0b6c0a9377d4f7d40bdc57ce6009a72daf067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:04:20 +0000 Subject: [PATCH 212/868] build(deps): bump github.com/spf13/pflag from 1.0.7 to 1.0.10 Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.7 to 1.0.10. - [Release notes](https://github.com/spf13/pflag/releases) - [Commits](https://github.com/spf13/pflag/compare/v1.0.7...v1.0.10) --- updated-dependencies: - dependency-name: github.com/spf13/pflag dependency-version: 1.0.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e5020ce8048..f45dc01e910 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined github.com/spf13/cobra v1.10.1 //gomodjail:unconfined - github.com/spf13/pflag v1.0.9 //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 diff --git a/go.sum b/go.sum index ab8acb21aa6..be0f7d2372f 100644 --- a/go.sum +++ b/go.sum @@ -288,8 +288,9 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From c591a5f4ec49f6b47ac9cc613cb93206a2b05e39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:18:48 +0000 Subject: [PATCH 213/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.3.3+incompatible to 28.4.0+incompatible - [Commits](https://github.com/docker/cli/compare/v28.3.3...v28.4.0) Updates `github.com/docker/docker` from 28.3.3+incompatible to 28.4.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.4.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.4.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e5020ce8048..522846b19bc 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.3.3+incompatible //gomodjail:unconfined - github.com/docker/docker v28.3.3+incompatible //gomodjail:unconfined + github.com/docker/cli v28.4.0+incompatible //gomodjail:unconfined + github.com/docker/docker v28.4.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index ab8acb21aa6..d5382985c83 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo= -github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= +github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= From a1319c9f2e65d44f51c167efc887583a5c863523 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:01:52 +0000 Subject: [PATCH 214/868] build(deps): bump actions/setup-go from 5.5.0 to 6.0.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.5.0 to 6.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/d35c59abb061a4a6fb18e82ac0862c26744d6ab5...44694675825211faa026b3c33043df3e48a5fa00) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 938bfd7b351..29556e259de 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 1b5442a698b..65c8bbd3374 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index dbf6d8f3912..6a1309918bc 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 5d944a72dab..8e3b11bdf13 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index e892876eea9..1c7aa9a0069 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f5e98bff47..d74012312e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: "Install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 7e505057faf..16cc728e000 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From e20f036698ff151f985bd36ca0479315d0daaab1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 5 Sep 2025 10:34:50 +0900 Subject: [PATCH 215/868] update runc (1.3.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0edf11284a4..fa247a0ccc0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.1.4@75cb2b7193e4e490e9fbdc236c0e811ccaba3376 -ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e +ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY # Extra deps: Build From 5677d6b192ef1209b1928efa9f5fef9992e5c22c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 5 Sep 2025 10:36:08 +0900 Subject: [PATCH 216/868] update CNI plugins (1.8.0) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 | 2 -- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 | 2 ++ hack/provisioning/kube/kind.sh | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 45afa39961c..19ed223761c 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -145,5 +145,5 @@ jobs: # Note: these as for amd64 containerd-sha: 316d510a0428276d931023f72c09fdff1a6ba81d6cc36f31805fea6a3c88f515 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 - linux-cni-version: v1.7.1 - linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 + linux-cni-version: v1.8.0 + linux-cni-sha: ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 diff --git a/Dockerfile b/Dockerfile index fa247a0ccc0..18e804c49b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.1.4@75cb2b7193e4e490e9fbdc236c0e811ccaba3376 ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e -ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY +ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.23.2@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 deleted file mode 100644 index c9f57e39739..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 +++ /dev/null @@ -1,2 +0,0 @@ -1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 cni-plugins-linux-amd64-v1.7.1.tgz -119fcb508d1ac2149e49a550752f9cd64d023a1d70e189b59c476e4d2bf7c497 cni-plugins-linux-arm64-v1.7.1.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 new file mode 100644 index 00000000000..40b7ebdd7f2 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 @@ -0,0 +1,2 @@ +ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 cni-plugins-linux-amd64-v1.8.0.tgz +57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb cni-plugins-linux-arm64-v1.8.0.tgz diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index cabaddab967..fa3591355f3 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -22,11 +22,11 @@ readonly root GO_VERSION=1.25 KIND_VERSION=v0.27.0 -CNI_PLUGINS_VERSION=v1.7.1 +CNI_PLUGINS_VERSION=v1.8.0 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_AMD64=1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 +CNI_PLUGINS_SHA_AMD64=ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_ARM64=119fcb508d1ac2149e49a550752f9cd64d023a1d70e189b59c476e4d2bf7c497 +CNI_PLUGINS_SHA_ARM64=57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb [ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64 From 3c9e07a592797d9fbe834c5dad8c55aca4526d54 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 5 Sep 2025 10:37:33 +0900 Subject: [PATCH 217/868] update BuildKit (0.24.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 diff --git a/Dockerfile b/Dockerfile index 18e804c49b8..7f581d28049 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.23.2@BINARY +ARG BUILDKIT_VERSION=v0.24.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.17.0@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 deleted file mode 100644 index e74581e9551..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 +++ /dev/null @@ -1,2 +0,0 @@ -2771c3403e3a1f75a83cde387a05365794d3b900c355e864772a36c3ce541f82 buildkit-v0.23.2.linux-amd64.tar.gz -6385ff70b2fb4134b50ac3183eea3a0b06c6f6129173940d73178ae0477368f1 buildkit-v0.23.2.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 new file mode 100644 index 00000000000..61d26717528 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 @@ -0,0 +1,2 @@ +af8064eca16077b4d6937745988ba2d2dfa439540874cdcd918318315f3ba1d3 buildkit-v0.24.0.linux-amd64.tar.gz +38dc4433d220bb43c198df2070e49d5dde5ed44ee31fb80d6b13722eec21d4ea buildkit-v0.24.0.linux-arm64.tar.gz From d97808ddd33e6b4be26aa0d76fa73fea9972887e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 5 Sep 2025 11:41:27 +0900 Subject: [PATCH 218/868] update kind (0.30.0) Signed-off-by: Akihiro Suda --- hack/provisioning/kube/kind.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index fa3591355f3..43b74e4eb8b 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -21,7 +21,7 @@ readonly root . "$root/../../scripts/lib.sh" GO_VERSION=1.25 -KIND_VERSION=v0.27.0 +KIND_VERSION=v0.30.0 CNI_PLUGINS_VERSION=v1.8.0 # shellcheck disable=SC2034 CNI_PLUGINS_SHA_AMD64=ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 From 3ccb05b406fa2f8b7a269314381ada502bb1eb31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:02:19 +0000 Subject: [PATCH 219/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.8.1...v2.8.2) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8d10651f6d0..91a1530494d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.8.1 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.8.2 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 239f1ebb5e5..8c5fc3f13b1 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.8.1 h1:27O4dzyhiS/UEUKp1zHOHCBWD1WbxGsYGMNNaSejTk4= -github.com/compose-spec/compose-go/v2 v2.8.1/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww= +github.com/compose-spec/compose-go/v2 v2.8.2 h1:A1iVoZJUex7buGv1CpnC5uwNuyTMBYpDAmBnAQmia9Q= +github.com/compose-spec/compose-go/v2 v2.8.2/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From b0a5cf0df8d2541566cc966acd213ab259adbec8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:01:53 +0000 Subject: [PATCH 220/868] build(deps): bump tonistiigi/xx from 1.6.1 to 1.7.0 Bumps tonistiigi/xx from 1.6.1 to 1.7.0. --- updated-dependencies: - dependency-name: tonistiigi/xx dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7f581d28049..4443b3ffba6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ ARG NYDUS_VERSION=v2.3.5 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.7.0@sha256:010d4b66aed389848b0694f91c7aaee9df59a6f20be7f5d12e53663a37bd14e2 AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base From 6e9d421da5b818df856dd143e740b18771308f24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:02:21 +0000 Subject: [PATCH 221/868] build(deps): bump the golang-x group across 1 directory with 6 updates Bumps the golang-x group with 2 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/crypto` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/crypto/compare/v0.41.0...v0.42.0) Updates `golang.org/x/net` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/net/compare/v0.43.0...v0.44.0) Updates `golang.org/x/sync` from 0.16.0 to 0.17.0 - [Commits](https://github.com/golang/sync/compare/v0.16.0...v0.17.0) Updates `golang.org/x/sys` from 0.35.0 to 0.36.0 - [Commits](https://github.com/golang/sys/compare/v0.35.0...v0.36.0) Updates `golang.org/x/term` from 0.34.0 to 0.35.0 - [Commits](https://github.com/golang/term/compare/v0.34.0...v0.35.0) Updates `golang.org/x/text` from 0.28.0 to 0.29.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.28.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.29.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 8d10651f6d0..b060c48ef79 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.41.0 - golang.org/x/net v0.43.0 - golang.org/x/sync v0.16.0 //gomodjail:unconfined - golang.org/x/sys v0.35.0 //gomodjail:unconfined - golang.org/x/term v0.34.0 //gomodjail:unconfined - golang.org/x/text v0.28.0 + golang.org/x/crypto v0.42.0 + golang.org/x/net v0.44.0 + golang.org/x/sync v0.17.0 //gomodjail:unconfined + golang.org/x/sys v0.36.0 //gomodjail:unconfined + golang.org/x/term v0.35.0 //gomodjail:unconfined + golang.org/x/text v0.29.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index 239f1ebb5e5..6d4df08f931 100644 --- a/go.sum +++ b/go.sum @@ -362,8 +362,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -395,8 +395,8 @@ 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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 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= @@ -409,8 +409,8 @@ 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= @@ -435,8 +435,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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= @@ -446,8 +446,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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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= @@ -457,8 +457,8 @@ 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 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= From 2f760f6b42f690c20ae51b4767115f6293d4e4ad Mon Sep 17 00:00:00 2001 From: Swapnanil-Gupta Date: Fri, 12 Sep 2025 17:04:55 +0000 Subject: [PATCH 222/868] Changes: - move blkio code to a separate file - return block device paths instead of major:minor numbers - update blkio device tests to use blk devices in place of char devices Signed-off-by: Swapnanil-Gupta --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-test.yml | 8 +- .github/workflows/workflow-tigron.yml | 2 +- Dockerfile | 14 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 | 2 - Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 | 2 + Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 | 2 - Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 | 2 + .../SHA256SUMS.d/stargz-snapshotter-v0.16.3 | 3 - .../SHA256SUMS.d/stargz-snapshotter-v0.17.0 | 3 + .../container/container_inspect_linux_test.go | 50 +++--- .../container_run_cgroup_linux_test.go | 73 ++++++--- .../container/container_stop_linux_test.go | 20 +++ cmd/nerdctl/image/image_convert.go | 2 + cmd/nerdctl/image/image_convert_linux_test.go | 20 ++- docs/dev/auditing_dockerfile.md | 4 +- go.mod | 16 +- go.sum | 42 ++--- hack/provisioning/kube/kind.sh | 8 +- pkg/api/types/image_types.go | 4 + pkg/cmd/image/convert.go | 2 +- pkg/containerutil/containerutil.go | 41 ++--- pkg/inspecttypes/dockercompat/blkio.go | 155 ++++++++++++++++++ .../dockercompat/blkioutils_linux.go | 98 +++++++++++ .../dockercompat/blkioutils_others.go | 33 ++++ pkg/inspecttypes/dockercompat/dockercompat.go | 86 +--------- .../dockercompat/dockercompat_test.go | 30 ++-- pkg/snapshotterutil/sociutil.go | 8 +- 33 files changed, 507 insertions(+), 235 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 delete mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 delete mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 create mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 create mode 100644 pkg/inspecttypes/dockercompat/blkio.go create mode 100644 pkg/inspecttypes/dockercompat/blkioutils_linux.go create mode 100644 pkg/inspecttypes/dockercompat/blkioutils_others.go diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 938bfd7b351..29556e259de 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 1b5442a698b..65c8bbd3374 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index dbf6d8f3912..6a1309918bc 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 5d944a72dab..8e3b11bdf13 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index e892876eea9..1c7aa9a0069 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f5e98bff47..d74012312e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: "Install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 5016fb21ff1..19ed223761c 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -141,9 +141,9 @@ jobs: go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.1.3 + containerd-version: 2.1.4 # Note: these as for amd64 - containerd-sha: 436cc160c33b37ec25b89fb5c72fc879ab2b3416df5d7af240c3e9c2f4065d3c + containerd-sha: 316d510a0428276d931023f72c09fdff1a6ba81d6cc36f31805fea6a3c88f515 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 - linux-cni-version: v1.7.1 - linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 + linux-cni-version: v1.8.0 + linux-cni-sha: ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 7e505057faf..16cc728e000 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/Dockerfile b/Dockerfile index 1588e7c62d5..7f581d28049 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,14 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.1.3@c787fb98911740dd3ff2d0e45ce88cdf01410486 -ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e -ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY +ARG CONTAINERD_VERSION=v2.1.4@75cb2b7193e4e490e9fbdc236c0e811ccaba3376 +ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e +ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.23.2@BINARY +ARG BUILDKIT_VERSION=v0.24.0@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.17.0@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless @@ -48,14 +48,14 @@ ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.12.3 -ARG NYDUS_VERSION=v2.3.2 +ARG NYDUS_VERSION=v2.3.5 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base COPY --from=xx / / ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 deleted file mode 100644 index e74581e9551..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.23.2 +++ /dev/null @@ -1,2 +0,0 @@ -2771c3403e3a1f75a83cde387a05365794d3b900c355e864772a36c3ce541f82 buildkit-v0.23.2.linux-amd64.tar.gz -6385ff70b2fb4134b50ac3183eea3a0b06c6f6129173940d73178ae0477368f1 buildkit-v0.23.2.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 new file mode 100644 index 00000000000..61d26717528 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 @@ -0,0 +1,2 @@ +af8064eca16077b4d6937745988ba2d2dfa439540874cdcd918318315f3ba1d3 buildkit-v0.24.0.linux-amd64.tar.gz +38dc4433d220bb43c198df2070e49d5dde5ed44ee31fb80d6b13722eec21d4ea buildkit-v0.24.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 deleted file mode 100644 index c9f57e39739..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 +++ /dev/null @@ -1,2 +0,0 @@ -1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 cni-plugins-linux-amd64-v1.7.1.tgz -119fcb508d1ac2149e49a550752f9cd64d023a1d70e189b59c476e4d2bf7c497 cni-plugins-linux-arm64-v1.7.1.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 new file mode 100644 index 00000000000..40b7ebdd7f2 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 @@ -0,0 +1,2 @@ +ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 cni-plugins-linux-amd64-v1.8.0.tgz +57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb cni-plugins-linux-arm64-v1.8.0.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 deleted file mode 100644 index e9b2bfa457c..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 +++ /dev/null @@ -1,3 +0,0 @@ -516984d13e10396f7f6090c51e4e42cc1af9a0d4b16aa81837bcdb1d5a5608d6 stargz-snapshotter-v0.16.3-linux-amd64.tar.gz -d3ac8215603cfd002901c88c568ff5c0685d6953c012fa6ff709deb50f90b023 stargz-snapshotter-v0.16.3-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 new file mode 100644 index 00000000000..785c3b2acec --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 @@ -0,0 +1,3 @@ +e3cd9aed03a0fc82adc2484a3fe94381d21f52d998419e15ca019744d27e18b7 stargz-snapshotter-v0.17.0-linux-amd64.tar.gz +9b3e85729885d7b5c4a3b7b67a8c8048065f60b2098fec17251f256d49bb24bb stargz-snapshotter-v0.17.0-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index ad43f0bf87e..21098c322f2 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -20,7 +20,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "slices" "strings" "testing" @@ -28,6 +27,7 @@ import ( "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" + "github.com/containerd/continuity/testutil/loopback" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -512,45 +512,45 @@ func TestContainerInspectBlkioSettings(t *testing.T) { // For now, disable the test unless on a recent kernel. testutil.RequireKernelVersion(t, ">= 6.0.0-0") - devPath := "/dev/dummy-zero" - // a dummy zero device: mknod /dev/dummy-zero c 1 5 - helperCmd := exec.Command("mknod", []string{devPath, "c", "1", "5"}...) - if out, err := helperCmd.CombinedOutput(); err != nil { - err = fmt.Errorf("cannot create %q: %q: %w", devPath, string(out), err) + lo, err := loopback.New(4096) + if err != nil { + err = fmt.Errorf("cannot find a loop device: %w", err) t.Fatal(err) } - - // ensure the file will be removed in case of failed in the test - defer func() { - if err := exec.Command("rm", "-f", devPath).Run(); err != nil { - t.Logf("failed to remove device %s: %v", devPath, err) - } - }() + defer lo.Close() base := testutil.NewBase(t) defer base.Cmd("rm", "-f", testContainer).AssertOK() + const ( + weight = 500 + readBps = 1048576 + readIops = 1000 + writeBps = 2097152 + writeIops = 2000 + ) base.Cmd("run", "-d", "--name", testContainer, - "--blkio-weight", "500", - "--blkio-weight-device", "/dev/dummy-zero:500", - "--device-read-bps", "/dev/dummy-zero:1048576", - "--device-read-iops", "/dev/dummy-zero:1000", - "--device-write-bps", "/dev/dummy-zero:2097152", - "--device-write-iops", "/dev/dummy-zero:2000", + "--blkio-weight", fmt.Sprintf("%d", weight), + "--blkio-weight-device", fmt.Sprintf("%s:%d", lo.Device, weight), + "--device-read-bps", fmt.Sprintf("%s:%d", lo.Device, readBps), + "--device-read-iops", fmt.Sprintf("%s:%d", lo.Device, readIops), + "--device-write-bps", fmt.Sprintf("%s:%d", lo.Device, writeBps), + "--device-write-iops", fmt.Sprintf("%s:%d", lo.Device, writeIops), testutil.AlpineImage, "sleep", "infinity").AssertOK() inspect := base.InspectContainer(testContainer) - assert.Equal(t, uint16(500), inspect.HostConfig.BlkioWeight) + assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeight) assert.Equal(t, 1, len(inspect.HostConfig.BlkioWeightDevice)) - assert.Equal(t, uint16(500), *inspect.HostConfig.BlkioWeightDevice[0].Weight) + assert.Equal(t, lo.Device, inspect.HostConfig.BlkioWeightDevice[0].Path) + assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeightDevice[0].Weight) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadBps)) - assert.Equal(t, uint64(1048576), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) + assert.Equal(t, uint64(readBps), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteBps)) - assert.Equal(t, uint64(2097152), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) + assert.Equal(t, uint64(writeBps), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadIOps)) - assert.Equal(t, uint64(1000), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) + assert.Equal(t, uint64(readIops), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteIOps)) - assert.Equal(t, uint64(2000), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) + assert.Equal(t, uint64(writeIops), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) } func TestContainerInspectUser(t *testing.T) { diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index e7ea488aa9f..4c03e4ea85e 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -21,7 +21,6 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "strconv" "strings" @@ -495,31 +494,33 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { // For now, disable the test unless on a recent kernel. testutil.RequireKernelVersion(t, ">= 6.0.0-0") - // Create dummy device path - dummyDev := "/dev/dummy-zero" - + const ( + weight = "150" + deviceWeight = "100" + readBps = "1048576" + readIops = "1000" + writeBps = "2097152" + writeIops = "2000" + ) + var lo *loopback.Loopback testCase.Setup = func(data test.Data, helpers test.Helpers) { - // Create dummy device - helperCmd := exec.Command("mknod", dummyDev, "c", "1", "5") - if out, err := helperCmd.CombinedOutput(); err != nil { - t.Fatalf("cannot create %q: %q: %v", dummyDev, string(out), err) - } + var err error + lo, err = loopback.New(4096) + assert.NilError(t, err) + t.Logf("loopback device: %+v", lo) } - testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - // Clean up the dummy device - if err := exec.Command("rm", "-f", dummyDev).Run(); err != nil { - t.Logf("failed to remove device %s: %v", dummyDev, err) + if lo != nil { + _ = lo.Close() } } - testCase.SubTests = []*test.Case{ { Description: "blkio-weight", Require: nerdtest.CGroupV2, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--blkio-weight", "150", + "--blkio-weight", weight, testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -530,7 +531,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { ExitCode: 0, Output: expect.All( func(stdout string, t tig.T) { - assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), "150")) + assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), weight)) }, ), } @@ -541,7 +542,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Require: nerdtest.CGroupV2, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--blkio-weight-device", dummyDev+":100", + "--blkio-weight-device", fmt.Sprintf("%s:%s", lo.Device, deviceWeight), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -553,7 +554,11 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Weight}}{{end}}", data.Identifier()) - assert.Assert(t, strings.Contains(inspectOut, "100")) + assert.Assert(t, strings.Contains(inspectOut, deviceWeight)) + }, + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Path}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } @@ -570,7 +575,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--device-read-bps", dummyDev+":1048576", + "--device-read-bps", fmt.Sprintf("%s:%s", lo.Device, readBps), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -582,7 +587,11 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Rate}}{{end}}", data.Identifier()) - assert.Assert(t, strings.Contains(inspectOut, "1048576")) + assert.Assert(t, strings.Contains(inspectOut, readBps)) + }, + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Path}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } @@ -599,7 +608,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--device-write-bps", dummyDev+":2097152", + "--device-write-bps", fmt.Sprintf("%s:%s", lo.Device, writeBps), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -611,7 +620,11 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Rate}}{{end}}", data.Identifier()) - assert.Assert(t, strings.Contains(inspectOut, "2097152")) + assert.Assert(t, strings.Contains(inspectOut, writeBps)) + }, + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Path}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } @@ -628,7 +641,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--device-read-iops", dummyDev+":1000", + "--device-read-iops", fmt.Sprintf("%s:%s", lo.Device, readIops), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -640,7 +653,11 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Rate}}{{end}}", data.Identifier()) - assert.Assert(t, strings.Contains(inspectOut, "1000")) + assert.Assert(t, strings.Contains(inspectOut, readIops)) + }, + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Path}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } @@ -657,7 +674,7 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { ), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "-d", "--name", data.Identifier(), - "--device-write-iops", dummyDev+":2000", + "--device-write-iops", fmt.Sprintf("%s:%s", lo.Device, writeIops), testutil.AlpineImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -669,7 +686,11 @@ func TestRunBlkioSettingCgroupV2(t *testing.T) { Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Rate}}{{end}}", data.Identifier()) - assert.Assert(t, strings.Contains(inspectOut, "2000")) + assert.Assert(t, strings.Contains(inspectOut, writeIops)) + }, + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Path}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, lo.Device)) }, ), } diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index 19eb58bf9a4..e2f581a1ca8 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -199,3 +199,23 @@ func TestStopWithTimeout(t *testing.T) { // The container should get the SIGKILL before the 10s default timeout assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag") } +func TestStopCleanupFIFOs(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("/run/containerd/fifo/ doesn't exist on rootless") + } + testutil.DockerIncompatible(t) + base := testutil.NewBase(t) + testContainerName := testutil.Identifier(t) + oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + // Stop the container after 2 seconds + go func() { + time.Sleep(2 * time.Second) + base.Cmd("stop", testContainerName).AssertOK() + newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + assert.Equal(t, oldNumFifos, newNumFifos) + }() + // Start a container that is automatically removed after it exits + base.Cmd("run", "--rm", "--name", testContainerName, testutil.NginxAlpineImage).AssertOK() +} diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 49496f09ee7..48a8bed42f9 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -303,6 +303,8 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { SociOptions: types.SociOptions{ SpanSize: sociSpanSize, MinLayerSize: sociMinLayerSize, + Platforms: platforms, + AllPlatforms: allPlatforms, }, }, Stdout: cmd.OutOrStdout(), diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index be24918fb31..07cd2a7003d 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -39,7 +39,7 @@ func TestImageConvert(t *testing.T) { require.Not(nerdtest.Docker), ), Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("pull", "--quiet", testutil.CommonImage) + helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.CommonImage) }, SubTests: []*test.Case{ { @@ -107,6 +107,24 @@ func TestImageConvert(t *testing.T) { }, Expected: test.Expects(0, nil, nil), }, + { + Description: "soci with all-platforms", + Require: require.All( + require.Not(nerdtest.Docker), + nerdtest.Soci, + nerdtest.SociVersion("0.10.0"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--soci", "--all-platforms", + "--soci-span-size", "2097152", + "--soci-min-layer-size", "0", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, }, } diff --git a/docs/dev/auditing_dockerfile.md b/docs/dev/auditing_dockerfile.md index b323caf78fa..81a57592e53 100644 --- a/docs/dev/auditing_dockerfile.md +++ b/docs/dev/auditing_dockerfile.md @@ -34,7 +34,7 @@ is the local ip of the Charles proxy (non-localhost) Add the following stages in the dockerfile: ```dockerfile -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS hack-build-base-debian +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS hack-build-base-debian RUN apt-get update -qq; apt-get -qq install ca-certificates COPY charles-ssl-proxying-certificate.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates @@ -52,7 +52,7 @@ RUN update-ca-certificates Then replace any later "FROM" with our modified bases: ``` -golang:${GO_VERSION}-bookworm => hack-build-base-debian +golang:${GO_VERSION}-trixie => hack-build-base-debian golang:${GO_VERSION}-alpine => hack-build-base ubuntu:${UBUNTU_VERSION} => hack-base ``` diff --git a/go.mod b/go.mod index 6d554b021f3..8d10651f6d0 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.24.0 +go 1.24.2 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -27,13 +27,13 @@ require ( github.com/containerd/stargz-snapshotter/ipfs v0.17.0 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined - github.com/containernetworking/plugins v1.7.1 //gomodjail:unconfined + github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.3.3+incompatible //gomodjail:unconfined - github.com/docker/docker v28.3.3+incompatible //gomodjail:unconfined + github.com/docker/cli v28.4.0+incompatible //gomodjail:unconfined + github.com/docker/docker v28.4.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -56,8 +56,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined - github.com/spf13/cobra v1.9.1 //gomodjail:unconfined - github.com/spf13/pflag v1.0.7 //gomodjail:unconfined + github.com/spf13/cobra v1.10.1 //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 @@ -86,7 +86,7 @@ require ( github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -141,7 +141,7 @@ require ( //gomodjail:unconfined google.golang.org/grpc v1.73.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index eba472458ec..239f1ebb5e5 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= 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.7.1 h1:CNAR0jviDj6FS5Vg85NTgKWLDzZPfi/lj+VJfhMDTIs= -github.com/containernetworking/plugins v1.7.1/go.mod h1:xuMdjuio+a1oVQsHKjr/mgzuZ24leAsqUYRnzGoXHy0= +github.com/containernetworking/plugins v1.8.0 h1:WjGbV/0UQyo8A4qBsAh6GaDAtu1hevxVxsEuqtBqUFk= +github.com/containernetworking/plugins v1.8.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= @@ -88,10 +88,10 @@ 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 v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo= -github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= +github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= @@ -115,8 +115,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= @@ -155,8 +155,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -238,10 +238,10 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= +github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= +github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= +github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -286,11 +286,11 @@ github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -500,8 +500,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index cabaddab967..43b74e4eb8b 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -21,12 +21,12 @@ readonly root . "$root/../../scripts/lib.sh" GO_VERSION=1.25 -KIND_VERSION=v0.27.0 -CNI_PLUGINS_VERSION=v1.7.1 +KIND_VERSION=v0.30.0 +CNI_PLUGINS_VERSION=v1.8.0 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_AMD64=1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 +CNI_PLUGINS_SHA_AMD64=ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_ARM64=119fcb508d1ac2149e49a550752f9cd64d023a1d70e189b59c476e4d2bf7c497 +CNI_PLUGINS_SHA_ARM64=57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb [ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64 diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 5ff507ccc7c..0ceb3148896 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -310,4 +310,8 @@ type SociOptions struct { SpanSize int64 // Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. MinLayerSize int64 + // Platforms convert content for a specific platform + Platforms []string + // AllPlatforms convert content for all platforms + AllPlatforms bool } diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index 12a2040d598..c197755b3a5 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -171,7 +171,7 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa convertType = "nydus" case soci: // Convert image to SOCI format - convertedRef, err := snapshotterutil.ConvertSociIndexV2(ctx, client, srcRef, targetRef, options.GOptions, options.Platforms, options.SociOptions) + convertedRef, err := snapshotterutil.ConvertSociIndexV2(ctx, client, srcRef, targetRef, options.GOptions, options.SociOptions) if err != nil { return fmt.Errorf("failed to convert image to SOCI format: %w", err) } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 60da6f11895..31ded1a3b93 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -383,6 +383,12 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur switch status.Status { case containerd.Created, containerd.Stopped: + // Cleanup the IO after a successful Stop + if io := task.IO(); io != nil { + if cerr := io.Close(); cerr != nil { + log.G(ctx).Warnf("failed to close IO for container %s: %v", container.ID(), cerr) + } + } return nil case containerd.Paused, containerd.Pausing: paused = true @@ -395,6 +401,13 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } + // signal will be sent once resume is finished + if paused { + if err := task.Resume(ctx); err != nil { + log.G(ctx).Errorf("cannot unpause container %s: %s", container.ID(), err) + return err + } + } if *timeout > 0 { sig, err := getSignal(signalValue, l) if err != nil { @@ -405,20 +418,10 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } - // signal will be sent once resume is finished - if paused { - if err := task.Resume(ctx); err != nil { - log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) - } else { - // no need to do it again when send sigkill signal - paused = false - } - } - sigtermCtx, sigtermCtxCancel := context.WithTimeout(ctx, *timeout) defer sigtermCtxCancel() - err = waitContainerStop(sigtermCtx, exitCh, container.ID()) + err = waitContainerStop(sigtermCtx, task, exitCh, container.ID()) if err == nil { return nil } @@ -437,13 +440,7 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return err } - // signal will be sent once resume is finished - if paused { - if err := task.Resume(ctx); err != nil { - log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err) - } - } - return waitContainerStop(ctx, exitCh, container.ID()) + return waitContainerStop(ctx, task, exitCh, container.ID()) } func getSignal(signalValue string, containerLabels map[string]string) (syscall.Signal, error) { @@ -458,7 +455,7 @@ func getSignal(signalValue string, containerLabels map[string]string) (syscall.S return signal.ParseSignal("SIGTERM") } -func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error { +func waitContainerStop(ctx context.Context, task containerd.Task, exitCh <-chan containerd.ExitStatus, id string) error { select { case <-ctx.Done(): if err := ctx.Err(); err != nil { @@ -466,6 +463,12 @@ func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, } return nil case status := <-exitCh: + // Cleanup the IO after a successful Stop + if io := task.IO(); io != nil { + if cerr := io.Close(); cerr != nil { + log.G(ctx).Warnf("failed to close IO for container %s: %v", id, cerr) + } + } return status.Error() } } diff --git a/pkg/inspecttypes/dockercompat/blkio.go b/pkg/inspecttypes/dockercompat/blkio.go new file mode 100644 index 00000000000..3f2275335f0 --- /dev/null +++ b/pkg/inspecttypes/dockercompat/blkio.go @@ -0,0 +1,155 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + Portions from https://github.com/moby/moby/blob/v20.10.1/api/types/blkiodev/blkio.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/v20.10.1/NOTICE +*/ + +package dockercompat + +import ( + "fmt" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +type BlkioSettings struct { + BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) + BlkioWeightDevice []*WeightDevice + BlkioDeviceReadBps []*ThrottleDevice + BlkioDeviceWriteBps []*ThrottleDevice + BlkioDeviceReadIOps []*ThrottleDevice + BlkioDeviceWriteIOps []*ThrottleDevice +} + +// From https://github.com/moby/moby/blob/v20.10.1/api/types/blkiodev/blkio.go +// WeightDevice is a structure that holds device:weight pair +type WeightDevice struct { + Path string + Weight uint16 +} + +func (w *WeightDevice) String() string { + return fmt.Sprintf("%s:%d", w.Path, w.Weight) +} + +// ThrottleDevice is a structure that holds device:rate_per_second pair +type ThrottleDevice struct { + Path string + Rate uint64 +} + +func (t *ThrottleDevice) String() string { + return fmt.Sprintf("%s:%d", t.Path, t.Rate) +} + +func getBlkioSettingsFromSpec(spec *specs.Spec, hostConfig *HostConfig) error { + if spec == nil { + return fmt.Errorf("spec cannot be nil") + } + if hostConfig == nil { + return fmt.Errorf("hostConfig cannot be nil") + } + + // Initialize empty arrays by default + hostConfig.BlkioSettings = getDefaultBlkioSettings() + + if spec.Linux == nil || spec.Linux.Resources == nil || spec.Linux.Resources.BlockIO == nil { + return nil + } + + blockIO := spec.Linux.Resources.BlockIO + + // Set block IO weight + if blockIO.Weight != nil { + hostConfig.BlkioWeight = *blockIO.Weight + } + + // Set weight devices + if len(blockIO.WeightDevice) > 0 { + hostConfig.BlkioWeightDevice = make([]*WeightDevice, len(blockIO.WeightDevice)) + dockerCompatWeightDevices, err := toDockerCompatWeightDevices(blockIO.WeightDevice) + if err != nil { + return fmt.Errorf("failed to convert weight devices to dockercompat format: %w", err) + } + for i, dev := range dockerCompatWeightDevices { + hostConfig.BlkioWeightDevice[i] = &dev + } + } + + // Set throttle devices for read BPS + if len(blockIO.ThrottleReadBpsDevice) > 0 { + hostConfig.BlkioDeviceReadBps = make([]*ThrottleDevice, len(blockIO.ThrottleReadBpsDevice)) + dockerCompatThrottleDevices, err := toDockerCompatThrottleDevices(blockIO.ThrottleReadBpsDevice) + if err != nil { + return fmt.Errorf("failed to convert throttle devices to dockercompat format: %w", err) + } + for i, dev := range dockerCompatThrottleDevices { + hostConfig.BlkioDeviceReadBps[i] = &dev + } + } + + // Set throttle devices for write BPS + if len(blockIO.ThrottleWriteBpsDevice) > 0 { + hostConfig.BlkioDeviceWriteBps = make([]*ThrottleDevice, len(blockIO.ThrottleWriteBpsDevice)) + dockerCompatThrottleDevices, err := toDockerCompatThrottleDevices(blockIO.ThrottleWriteBpsDevice) + if err != nil { + return fmt.Errorf("failed to convert throttle devices to dockercompat format: %w", err) + } + for i, dev := range dockerCompatThrottleDevices { + hostConfig.BlkioDeviceWriteBps[i] = &dev + } + } + + // Set throttle devices for read IOPs + if len(blockIO.ThrottleReadIOPSDevice) > 0 { + hostConfig.BlkioDeviceReadIOps = make([]*ThrottleDevice, len(blockIO.ThrottleReadIOPSDevice)) + dockerCompatThrottleDevices, err := toDockerCompatThrottleDevices(blockIO.ThrottleReadIOPSDevice) + if err != nil { + return fmt.Errorf("failed to convert throttle devices to dockercompat format: %w", err) + } + for i, dev := range dockerCompatThrottleDevices { + hostConfig.BlkioDeviceReadIOps[i] = &dev + } + } + + // Set throttle devices for write IOPs + if len(blockIO.ThrottleWriteIOPSDevice) > 0 { + hostConfig.BlkioDeviceWriteIOps = make([]*ThrottleDevice, len(blockIO.ThrottleWriteIOPSDevice)) + dockerCompatThrottleDevices, err := toDockerCompatThrottleDevices(blockIO.ThrottleWriteIOPSDevice) + if err != nil { + return fmt.Errorf("failed to convert throttle devices to dockercompat format: %w", err) + } + for i, dev := range dockerCompatThrottleDevices { + hostConfig.BlkioDeviceWriteIOps[i] = &dev + } + } + return nil +} + +func getDefaultBlkioSettings() BlkioSettings { + return BlkioSettings{ + BlkioWeight: 0, + BlkioWeightDevice: make([]*WeightDevice, 0), + BlkioDeviceReadBps: make([]*ThrottleDevice, 0), + BlkioDeviceWriteBps: make([]*ThrottleDevice, 0), + BlkioDeviceReadIOps: make([]*ThrottleDevice, 0), + BlkioDeviceWriteIOps: make([]*ThrottleDevice, 0), + } +} diff --git a/pkg/inspecttypes/dockercompat/blkioutils_linux.go b/pkg/inspecttypes/dockercompat/blkioutils_linux.go new file mode 100644 index 00000000000..bac75ced0c2 --- /dev/null +++ b/pkg/inspecttypes/dockercompat/blkioutils_linux.go @@ -0,0 +1,98 @@ +//go:build linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dockercompat + +import ( + "fmt" + "os" + + "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) + +func toDockerCompatWeightDevices(weightDevices []specs.LinuxWeightDevice) ([]WeightDevice, error) { + majorMinorToPathMap, err := getDeviceMajorMinorToPathMap() + if err != nil { + return nil, fmt.Errorf("failed to query device paths from major/minor numbers: %w", err) + } + + devices := []WeightDevice{} + for _, weightDevice := range weightDevices { + key := fmt.Sprintf("%d:%d", weightDevice.Major, weightDevice.Minor) + if _, ok := majorMinorToPathMap[key]; ok { + devices = append(devices, WeightDevice{ + Path: majorMinorToPathMap[key], + Weight: *weightDevice.Weight, + }) + } + } + return devices, nil +} + +func toDockerCompatThrottleDevices(throttleDevices []specs.LinuxThrottleDevice) ([]ThrottleDevice, error) { + majorMinorToPathMap, err := getDeviceMajorMinorToPathMap() + if err != nil { + return nil, fmt.Errorf("failed to query device paths from major/minor numbers: %w", err) + } + + devices := []ThrottleDevice{} + for _, throttleDevice := range throttleDevices { + key := fmt.Sprintf("%d:%d", throttleDevice.Major, throttleDevice.Minor) + if _, ok := majorMinorToPathMap[key]; ok { + devices = append(devices, ThrottleDevice{ + Path: majorMinorToPathMap[key], + Rate: throttleDevice.Rate, + }) + } + } + return devices, nil +} + +func getDeviceMajorMinorToPathMap() (map[string]string, error) { + devDir := "/dev" + entries, err := os.ReadDir(devDir) + if err != nil { + return nil, fmt.Errorf("failed to read %s: %w", devDir, err) + } + + majorMinorToPathMap := make(map[string]string) + for _, ent := range entries { + if ent.IsDir() { + continue + } + devicePath := fmt.Sprintf("%s/%s", devDir, ent.Name()) + osStat, err := os.Stat(devicePath) + if err != nil { + return nil, fmt.Errorf("failed to stat %s: %w", devicePath, err) + } + // skip char devices + if osStat.Mode()&os.ModeCharDevice != 0 { + continue + } + var unixStat unix.Stat_t + if err := unix.Stat(devicePath, &unixStat); err != nil { + return nil, fmt.Errorf("failed to stat %s: %w", devicePath, err) + } + major := int64(unix.Major(uint64(unixStat.Rdev))) //nolint: unconvert + minor := int64(unix.Minor(uint64(unixStat.Rdev))) //nolint: unconvert + key := fmt.Sprintf("%d:%d", major, minor) + majorMinorToPathMap[key] = devicePath + } + return majorMinorToPathMap, nil +} diff --git a/pkg/inspecttypes/dockercompat/blkioutils_others.go b/pkg/inspecttypes/dockercompat/blkioutils_others.go new file mode 100644 index 00000000000..c5560f099e3 --- /dev/null +++ b/pkg/inspecttypes/dockercompat/blkioutils_others.go @@ -0,0 +1,33 @@ +//go:build !linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dockercompat + +import ( + "fmt" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +func toDockerCompatWeightDevices(weightDevices []specs.LinuxWeightDevice) ([]WeightDevice, error) { + return nil, fmt.Errorf("block device weight controls are not supported on this platform") +} + +func toDockerCompatThrottleDevices(throttleDevices []specs.LinuxThrottleDevice) ([]ThrottleDevice, error) { + return nil, fmt.Errorf("block device throttling is not supported on this platform") +} diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 8077d72886a..5ebfb0c2980 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -182,7 +182,7 @@ type HostConfig struct { MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap OomKillDisable bool // specifies whether to disable OOM Killer Devices []DeviceMapping // List of devices to map inside the container - LinuxBlkioSettings + BlkioSettings } // From https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L416-L427 @@ -309,15 +309,6 @@ type NetworkEndpointSettings struct { // TODO DriverOpts map[string]string } -type LinuxBlkioSettings struct { - BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) - BlkioWeightDevice []*specs.LinuxWeightDevice - BlkioDeviceReadBps []*specs.LinuxThrottleDevice - BlkioDeviceWriteBps []*specs.LinuxThrottleDevice - BlkioDeviceReadIOps []*specs.LinuxThrottleDevice - BlkioDeviceWriteIOps []*specs.LinuxThrottleDevice -} - // ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container. func ContainerFromNative(n *native.Container) (*Container, error) { var hostname string @@ -1019,78 +1010,3 @@ func ParseMountProperties(option []string) (rw bool, propagation string) { } return } - -func getDefaultLinuxBlkioSettings() LinuxBlkioSettings { - return LinuxBlkioSettings{ - BlkioWeight: 0, - BlkioWeightDevice: make([]*specs.LinuxWeightDevice, 0), - BlkioDeviceReadBps: make([]*specs.LinuxThrottleDevice, 0), - BlkioDeviceWriteBps: make([]*specs.LinuxThrottleDevice, 0), - BlkioDeviceReadIOps: make([]*specs.LinuxThrottleDevice, 0), - BlkioDeviceWriteIOps: make([]*specs.LinuxThrottleDevice, 0), - } -} - -func getBlkioSettingsFromSpec(spec *specs.Spec, hostConfig *HostConfig) error { - if spec == nil { - return fmt.Errorf("spec cannot be nil") - } - if hostConfig == nil { - return fmt.Errorf("hostConfig cannot be nil") - } - - // Initialize empty arrays by default - hostConfig.LinuxBlkioSettings = getDefaultLinuxBlkioSettings() - - if spec.Linux == nil || spec.Linux.Resources == nil || spec.Linux.Resources.BlockIO == nil { - return nil - } - - blockIO := spec.Linux.Resources.BlockIO - - // Set block IO weight - if blockIO.Weight != nil { - hostConfig.BlkioWeight = *blockIO.Weight - } - - // Set weight devices - if len(blockIO.WeightDevice) > 0 { - hostConfig.BlkioWeightDevice = make([]*specs.LinuxWeightDevice, len(blockIO.WeightDevice)) - for i, dev := range blockIO.WeightDevice { - hostConfig.BlkioWeightDevice[i] = &dev - } - } - - // Set throttle devices for read BPS - if len(blockIO.ThrottleReadBpsDevice) > 0 { - hostConfig.BlkioDeviceReadBps = make([]*specs.LinuxThrottleDevice, len(blockIO.ThrottleReadBpsDevice)) - for i, dev := range blockIO.ThrottleReadBpsDevice { - hostConfig.BlkioDeviceReadBps[i] = &dev - } - } - - // Set throttle devices for write BPS - if len(blockIO.ThrottleWriteBpsDevice) > 0 { - hostConfig.BlkioDeviceWriteBps = make([]*specs.LinuxThrottleDevice, len(blockIO.ThrottleWriteBpsDevice)) - for i, dev := range blockIO.ThrottleWriteBpsDevice { - hostConfig.BlkioDeviceWriteBps[i] = &dev - } - } - - // Set throttle devices for read IOPs - if len(blockIO.ThrottleReadIOPSDevice) > 0 { - hostConfig.BlkioDeviceReadIOps = make([]*specs.LinuxThrottleDevice, len(blockIO.ThrottleReadIOPSDevice)) - for i, dev := range blockIO.ThrottleReadIOPSDevice { - hostConfig.BlkioDeviceReadIOps[i] = &dev - } - } - - // Set throttle devices for write IOPs - if len(blockIO.ThrottleWriteIOPSDevice) > 0 { - hostConfig.BlkioDeviceWriteIOps = make([]*specs.LinuxThrottleDevice, len(blockIO.ThrottleWriteIOPSDevice)) - for i, dev := range blockIO.ThrottleWriteIOPSDevice { - hostConfig.BlkioDeviceWriteIOps[i] = &dev - } - } - return nil -} diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 621e64bf2ff..a4dbec2d4f3 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -105,9 +105,9 @@ func TestContainerFromNative(t *testing.T) { Driver: "json-file", Opts: map[string]string{}, }, - UTSMode: "host", - Tmpfs: map[string]string{}, - LinuxBlkioSettings: getDefaultLinuxBlkioSettings(), + UTSMode: "host", + Tmpfs: map[string]string{}, + BlkioSettings: getDefaultBlkioSettings(), }, Mounts: []MountPoint{ { @@ -201,9 +201,9 @@ func TestContainerFromNative(t *testing.T) { Driver: "json-file", Opts: map[string]string{}, }, - UTSMode: "host", - Tmpfs: map[string]string{}, - LinuxBlkioSettings: getDefaultLinuxBlkioSettings(), + UTSMode: "host", + Tmpfs: map[string]string{}, + BlkioSettings: getDefaultBlkioSettings(), }, Mounts: []MountPoint{ { @@ -292,9 +292,9 @@ func TestContainerFromNative(t *testing.T) { Driver: "json-file", Opts: map[string]string{}, }, - UTSMode: "host", - Tmpfs: map[string]string{}, - LinuxBlkioSettings: getDefaultLinuxBlkioSettings(), + UTSMode: "host", + Tmpfs: map[string]string{}, + BlkioSettings: getDefaultBlkioSettings(), }, Mounts: []MountPoint{ { @@ -342,12 +342,12 @@ func TestContainerFromNative(t *testing.T) { FinishedAt: "", }, HostConfig: &HostConfig{ - LogConfig: loggerLogConfig{Driver: "json-file", Opts: map[string]string{}}, - PortBindings: nat.PortMap{}, - GroupAdd: []string{}, - Tmpfs: map[string]string{}, - UTSMode: "host", - LinuxBlkioSettings: getDefaultLinuxBlkioSettings(), + LogConfig: loggerLogConfig{Driver: "json-file", Opts: map[string]string{}}, + PortBindings: nat.PortMap{}, + GroupAdd: []string{}, + Tmpfs: map[string]string{}, + UTSMode: "host", + BlkioSettings: getDefaultBlkioSettings(), }, NetworkSettings: &NetworkSettings{ Ports: &nat.PortMap{}, diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 240ef54737e..82e8773ce66 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -104,7 +104,7 @@ func CheckSociVersion(requiredVersion string) error { } // ConvertSociIndexV2 converts an image to SOCI format and returns the converted image reference with digest -func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) { +func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, sOpts types.SociOptions) (string, error) { // Check if SOCI version is at least 0.10.0 which is required for the convert operation if err := CheckSociVersion("0.10.0"); err != nil { return "", err @@ -117,10 +117,12 @@ func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef strin sociCmd.Args = append(sociCmd.Args, "convert") - if len(platforms) > 0 { + if sOpts.AllPlatforms { + sociCmd.Args = append(sociCmd.Args, "--all-platforms") + } else if len(sOpts.Platforms) > 0 { // multiple values need to be passed as separate, repeating flags in soci as it uses urfave // https://github.com/urfave/cli/blob/main/docs/v2/examples/flags.md#multiple-values-per-single-flag - for _, p := range platforms { + for _, p := range sOpts.Platforms { sociCmd.Args = append(sociCmd.Args, "--platform", p) } } From 2e888f4ff6a0de0200f3ae0d13c1ceac2df57336 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Mon, 15 Sep 2025 19:09:41 +0800 Subject: [PATCH 223/868] fix:forbid to restart/start container created by kubernetes Signed-off-by: ningmingxiao --- pkg/cmd/container/restart.go | 11 ++++++++++- pkg/containerutil/containerutil.go | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go index 3b376ada5a5..a3e6f0b2d49 100644 --- a/pkg/cmd/container/restart.go +++ b/pkg/cmd/container/restart.go @@ -21,10 +21,12 @@ import ( "fmt" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels" ) // Restart will restart one or more containers. @@ -35,13 +37,20 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } + info, err := found.Container.Info(ctx) + if err != nil { + return fmt.Errorf("can't get container %s info ", found.Container.ID()) + } + if _, ok := info.Labels[k8slabels.ContainerType]; ok { + log.L.Warnf("nerdctl does not support restarting container %s created by Kubernetes", info.ID) + } if err := containerutil.Stop(ctx, found.Container, options.Timeout, options.Signal); err != nil { return err } if err := containerutil.Start(ctx, found.Container, false, false, client, ""); err != nil { return err } - _, err := fmt.Fprintln(options.Stdout, found.Req) + _, err = fmt.Fprintln(options.Stdout, found.Req) return err }, } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 31ded1a3b93..d29a4035f94 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -216,6 +216,9 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i return err } + if _, ok := lab[k8slabels.ContainerType]; ok { + log.L.Warnf("nerdctl does not support starting container %s created by Kubernetes", container.ID()) + } if err := ReconfigNetContainer(ctx, container, client, lab); err != nil { return err } From 8466806c665beb0a1cae19a74f73001469bdc3bd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 17 Sep 2025 17:37:15 +0900 Subject: [PATCH 224/868] TestRunCgroupV2: remove `--cpu-shares` checks The behavior of CPU shares was changed intentionally in runc v1.4.0-rc.1. See runc PR 4896 Fix 4519 Signed-off-by: Akihiro Suda --- .../container_run_cgroup_linux_test.go | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index e7ea488aa9f..bd859e17d4d 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -61,9 +61,6 @@ func TestRunCgroupV2(t *testing.T) { if !info.SwapLimit { t.Skip("test requires SwapLimit") } - if !info.CPUShares { - t.Skip("test requires CPUShares") - } if !info.CPUSet { t.Skip("test requires CPUSet") } @@ -74,7 +71,6 @@ func TestRunCgroupV2(t *testing.T) { 44040192 44040192 42 -77 0-1 0 ` @@ -83,34 +79,32 @@ func TestRunCgroupV2(t *testing.T) { 60817408 6291456 42 -77 0-1 0 ` - // In CgroupV2 CPUWeight replace CPUShares => weight := 1 + ((shares-2)*9999)/262142 base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--pids-limit", "42", - "--cpu-shares", "2000", "--cpuset-cpus", "0-1", + "--cpuset-cpus", "0-1", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "cat", "cpu.max", "memory.max", "memory.swap.max", - "pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) + "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", - "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", + "--pids-limit", "42", "--cpuset-cpus", "0-1", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", - "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) + "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate1", "-w", "/sys/fs/cgroup", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate1").Run() update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", - "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1"} + "--pids-limit", "42", "--cpuset-cpus", "0-1"} if nerdtest.IsDocker() && info.CgroupVersion == "2" && info.SwapLimit { // Workaround for Docker with cgroup v2: // > Error response from daemon: Cannot update container 67c13276a13dd6a091cdfdebb355aa4e1ecb15fbf39c2b5c9abee89053e88fce: @@ -121,7 +115,7 @@ func TestRunCgroupV2(t *testing.T) { base.Cmd(update...).AssertOK() base.Cmd("exec", testutil.Identifier(t)+"-testUpdate1", "cat", "cpu.max", "memory.max", "memory.swap.max", - "pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) + "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate2").Run() base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate2", "-w", "/sys/fs/cgroup", "-d", @@ -130,11 +124,11 @@ func TestRunCgroupV2(t *testing.T) { base.Cmd("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", - "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", + "--pids-limit", "42", "--cpuset-cpus", "0-1", testutil.Identifier(t)+"-testUpdate2").AssertOK() base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2", "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", - "pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) + "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertOK() base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() From 9c75d4b808f5ba6ce4e6e498bed9c2cd725adc0f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 17 Sep 2025 18:06:20 +0900 Subject: [PATCH 225/868] Add TestRunCPUSharesCgroupV2 Signed-off-by: Akihiro Suda --- .../container_run_cgroup_linux_test.go | 29 +++++++++++++++++++ pkg/testutil/nerdtest/requirements.go | 18 ++++++++++++ 2 files changed, 47 insertions(+) diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index bd859e17d4d..797852e31a1 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -23,6 +23,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "testing" @@ -39,6 +40,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/container" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -709,3 +711,30 @@ func TestRunCPURealTimeSettingCgroupV1(t *testing.T) { testCase.Run(t) } + +func TestRunCPUSharesCgroupV2(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.All( + nerdtest.CGroupV2, + nerdtest.Info( + func(info dockercompat.Info) error { + if !info.CPUShares { + return fmt.Errorf("test requires CPUShares") + } + return nil + }, + ), + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--cpu-shares", "2000", + testutil.AlpineImage, "cat", "/sys/fs/cgroup/cpu.weight") + }, + // The value was historically 77, but with runc v1.4.0-rc.1 it became 170. + // https://github.com/opencontainers/runc/issues/4896#issuecomment-3301825811 + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^(77|170)\n$"))), + } + + testCase.Run(t) +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 40e3e08e955..3741b8f9aa5 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -164,6 +164,24 @@ var Rootless = &test.Requirement{ // Rootful marks a test as suitable only for rootful env var Rootful = require.Not(Rootless) +// Info requires that `nerdctl info` satisfies the condition function passed as argument. +func Info(f func(dockercompat.Info) error) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + stdout := helpers.Capture("info", "--format", "{{ json . }}") + var dinf dockercompat.Info + err := json.Unmarshal([]byte(stdout), &dinf) + if err != nil { + return false, fmt.Sprintf("failed to parse docker info: %v", err) + } + if err := f(dinf); err != nil { + return false, err.Error() + } + return true, "" + }, + } +} + // CGroup requires that cgroup is enabled var CGroup = &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { From fb4d705344a08dc8f68f699948753191fdbc5aad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:27:08 +0000 Subject: [PATCH 226/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.2 to 0.15.4. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.2...v0.15.4) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d2c9c733340..7d354011645 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.2 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.4 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.17.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.17.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index b88289fe71f..fe81460cf7b 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlK github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= -github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= +github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE= +github.com/containerd/nydus-snapshotter v0.15.4/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 7216405091fa9cf2a1262892a093cea344d00ff4 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 28 Jul 2025 03:45:55 +0000 Subject: [PATCH 227/868] add healthcheck orchestration logic Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/compose/compose_start.go | 8 +- cmd/nerdctl/container/container_create.go | 4 - ...o => container_health_check_linux_test.go} | 318 ++++++++++++++++++ cmd/nerdctl/container/container_run.go | 11 +- cmd/nerdctl/container/container_run_test.go | 6 + cmd/nerdctl/helpers/flagutil.go | 6 +- docs/healthchecks.md | 81 ++++- pkg/api/types/container_types.go | 13 +- pkg/cmd/compose/compose.go | 3 +- pkg/cmd/container/create.go | 3 - pkg/cmd/container/health_check.go | 1 - pkg/cmd/container/kill.go | 6 + pkg/cmd/container/remove.go | 6 + pkg/cmd/container/restart.go | 3 +- pkg/cmd/container/start.go | 3 +- pkg/cmd/container/stop.go | 4 + pkg/cmd/container/unpause.go | 3 +- pkg/composer/composer.go | 5 +- pkg/composer/pause.go | 2 +- pkg/config/config.go | 2 + pkg/containerutil/containerutil.go | 23 +- pkg/healthcheck/executor.go | 42 ++- pkg/healthcheck/health.go | 13 +- pkg/healthcheck/healthcheck_manager_darwin.go | 47 +++ .../healthcheck_manager_freebsd.go | 47 +++ pkg/healthcheck/healthcheck_manager_linux.go | 265 +++++++++++++++ .../healthcheck_manager_windows.go | 47 +++ 27 files changed, 903 insertions(+), 69 deletions(-) rename cmd/nerdctl/container/{container_health_check_test.go => container_health_check_linux_test.go} (65%) create mode 100644 pkg/healthcheck/healthcheck_manager_darwin.go create mode 100644 pkg/healthcheck/healthcheck_manager_freebsd.go create mode 100644 pkg/healthcheck/healthcheck_manager_linux.go create mode 100644 pkg/healthcheck/healthcheck_manager_windows.go diff --git a/cmd/nerdctl/compose/compose_start.go b/cmd/nerdctl/compose/compose_start.go index c945f52adb7..88e4cc905de 100644 --- a/cmd/nerdctl/compose/compose_start.go +++ b/cmd/nerdctl/compose/compose_start.go @@ -28,8 +28,10 @@ import ( "github.com/containerd/errdefs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/compose" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/labels" ) @@ -86,7 +88,7 @@ func startAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("service %q has no container to start", svcName) } - if err := startContainers(ctx, client, containers); err != nil { + if err := startContainers(ctx, client, containers, &globalOptions); err != nil { return err } } @@ -94,7 +96,7 @@ func startAction(cmd *cobra.Command, args []string) error { return nil } -func startContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container) error { +func startContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, globalOptions *types.GlobalCommandOptions) error { eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { c := c @@ -112,7 +114,7 @@ func startContainers(ctx context.Context, client *containerd.Client, containers } // in compose, always disable attach - if err := containerutil.Start(ctx, c, false, false, client, ""); err != nil { + if err := containerutil.Start(ctx, c, false, false, client, "", (*config.Config)(globalOptions)); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index fc5cbdd97c8..e30d529481a 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -279,10 +279,6 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { if err != nil { return opt, err } - opt.HealthStartInterval, err = cmd.Flags().GetDuration("health-start-interval") - if err != nil { - return opt, err - } opt.NoHealthcheck, err = cmd.Flags().GetBool("no-healthcheck") if err != nil { return opt, err diff --git a/cmd/nerdctl/container/container_health_check_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go similarity index 65% rename from cmd/nerdctl/container/container_health_check_test.go rename to cmd/nerdctl/container/container_health_check_linux_test.go index aa2d1603313..9ce502f1523 100644 --- a/cmd/nerdctl/container/container_health_check_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/healthcheck" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -42,6 +43,11 @@ func TestContainerHealthCheckBasic(t *testing.T) { // Docker CLI does not provide a standalone healthcheck command. testCase.Require = require.Not(nerdtest.Docker) + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + testCase.SubTests = []*test.Case{ { Description: "Container does not exist", @@ -139,6 +145,11 @@ func TestContainerHealthCheckAdvance(t *testing.T) { // Docker CLI does not provide a standalone healthcheck command. testCase.Require = require.Not(nerdtest.Docker) + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + testCase.SubTests = []*test.Case{ { Description: "Health check timeout scenario", @@ -602,3 +613,310 @@ func TestContainerHealthCheckAdvance(t *testing.T) { testCase.Run(t) } + +func TestHealthCheck_SystemdIntegration_Basic(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + + testCase.SubTests = []*test.Case{ + { + Description: "Basic healthy container with systemd-triggered healthcheck", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "2s", + testutil.CommonImage, "sleep", "30") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // Ensure proper cleanup of systemd units + helpers.Anyhow("stop", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout string, t tig.T) { + var h *healthcheck.Health + + // Poll up to 5 times for health status + maxAttempts := 5 + var finalStatus string + + for i := 0; i < maxAttempts; i++ { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + h = inspect.State.Health + + assert.Assert(t, h != nil, "expected health state to be present") + finalStatus = h.Status + + // If healthy, break and pass the test + if finalStatus == "healthy" { + t.Log(fmt.Sprintf("Container became healthy on attempt %d/%d", i+1, maxAttempts)) + break + } + + // If unhealthy, fail immediately + if finalStatus == "unhealthy" { + assert.Assert(t, false, fmt.Sprintf("Container became unhealthy on attempt %d/%d, status: %s", i+1, maxAttempts, finalStatus)) + return + } + + // If not the last attempt, wait before retrying + if i < maxAttempts-1 { + t.Log(fmt.Sprintf("Attempt %d/%d: status is '%s', waiting 1 second before retry", i+1, maxAttempts, finalStatus)) + time.Sleep(1 * time.Second) + } + } + + if finalStatus != "healthy" { + assert.Assert(t, false, fmt.Sprintf("Container did not become healthy after %d attempts, final status: %s", maxAttempts, finalStatus)) + return + } + + assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") + }), + } + }, + }, + { + Description: "Kill stops healthcheck execution and cleans up systemd timer", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "1s", + testutil.CommonImage, "sleep", "30") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // Container is already killed, just remove it + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + // Get container info for verification + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := inspect.ID + h := inspect.State.Health + + // Verify health state and logs exist + assert.Assert(t, h != nil, "expected health state to be present") + assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") + + // Ensure systemd timers are removed + result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, _ tig.T) { + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) + }, + }) + }, + } + }, + }, + { + Description: "Remove cleans up systemd timer", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "1s", + testutil.CommonImage, "sleep", "30") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("rm", "-f", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // Container is already removed, no cleanup needed + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := inspect.ID + + // Check systemd timers to ensure cleanup + result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, _ tig.T) { + // Verify systemd timer has been cleaned up by checking systemctl output + // We check that no timer contains our test identifier + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected nerdctl healthcheck timer for container ID %s to be removed after container removal", containerID) + }, + }) + }, + } + }, + }, + { + Description: "Stop cleans up systemd timer", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "1s", + testutil.CommonImage, "sleep", "30") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("stop", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // Container is already stopped, just remove it + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + // Get container info for verification + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := inspect.ID + + // Ensure systemd timers are removed + result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, _ tig.T) { + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) + }, + }) + }, + } + }, + }, + } + testCase.Run(t) +} + +func TestHealthCheck_SystemdIntegration_Advanced(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + + testCase.SubTests = []*test.Case{ + { + // Tests that CreateTimer() successfully creates systemd timer units and + // RemoveTransientHealthCheckFiles() properly cleans up units when container stops. + Description: "Systemd timer unit creation and cleanup", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "1s", + testutil.CommonImage, "sleep", "30") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout string, t tig.T) { + // Get container ID and check systemd timer + containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := containerInspect.ID + + // Check systemd timer + result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, _ tig.T) { + // Verify that a timer exists for this specific container + assert.Assert(t, strings.Contains(stdout, containerID), + "expected to find nerdctl healthcheck timer containing container ID: %s", containerID) + }, + }) + // Stop container and verify cleanup + helpers.Ensure("stop", data.Identifier()) + + // Check that timer is gone + result = helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, _ tig.T) { + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected nerdctl healthcheck timer for container ID %s to be removed after container stop", containerID) + }, + }) + }), + } + }, + }, + { + Description: "Container restart recreates systemd timer", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo restart-test", + "--health-interval", "2s", + testutil.CommonImage, "sleep", "60") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Get container ID for verification + containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := containerInspect.ID + + // Step 1: Verify timer exists initially + result := helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, containerID), + "expected timer for container %s to exist initially", containerID) + }, + }) + + // Step 2: Stop container + helpers.Ensure("stop", data.Identifier()) + + // Step 3: Verify timer is removed after stop + result = helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + result.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected timer for container %s to be removed after stop", containerID) + }, + }) + + // Step 4: Restart container + helpers.Ensure("start", data.Identifier()) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + // Step 5: Verify timer is recreated after restart - this is our final verification + return helpers.Custom("systemctl", "list-timers", "--all", "--no-pager") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + containerInspect := nerdtest.InspectContainer(helpers, data.Identifier()) + containerID := containerInspect.ID + assert.Assert(t, strings.Contains(stdout, containerID), + "expected timer for container %s to be recreated after restart", containerID) + }, + } + }, + }, + } + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 67b797bdce9..9b44feb19c8 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -33,10 +33,12 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/container" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/consoleutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/errutil" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/logging" "github.com/containerd/nerdctl/v2/pkg/netutil" @@ -240,7 +242,6 @@ func setCreateFlags(cmd *cobra.Command) { 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-start-period", 0, "Start period for the container to initialize before starting health-retries countdown") - cmd.Flags().Duration("health-start-interval", 0, "Time between running the checks during the start period") cmd.Flags().Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK") // #region env flags @@ -445,6 +446,14 @@ func runAction(cmd *cobra.Command, args []string) error { return err } + // Setup container healthchecks. + if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } + if createOpt.Detach { fmt.Fprintln(createOpt.Stdout, id) return nil diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 64692a3ead7..e3d50940f8b 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -841,6 +841,9 @@ func TestRunDomainname(t *testing.T) { } func TestRunHealthcheckFlags(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("healthcheck tests are skipped in rootless environment") + } testCase := nerdtest.Setup() testCases := []struct { @@ -990,6 +993,9 @@ 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 diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 32217ae95c6..22fc1acb1bf 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -52,8 +52,7 @@ func ValidateHealthcheckFlags(options types.ContainerCreateOptions) error { options.HealthInterval != 0 || options.HealthTimeout != 0 || options.HealthRetries != 0 || - options.HealthStartPeriod != 0 || - options.HealthStartInterval != 0 + options.HealthStartPeriod != 0 if options.NoHealthcheck { if options.HealthCmd != "" || healthFlagsSet { @@ -74,9 +73,6 @@ func ValidateHealthcheckFlags(options types.ContainerCreateOptions) error { if options.HealthStartPeriod < 0 { return fmt.Errorf("--health-start-period cannot be negative") } - if options.HealthStartInterval < 0 { - return fmt.Errorf("--health-start-interval cannot be negative") - } return nil } diff --git a/docs/healthchecks.md b/docs/healthchecks.md index b47c92748b5..628a8710a29 100644 --- a/docs/healthchecks.md +++ b/docs/healthchecks.md @@ -2,25 +2,31 @@ `nerdctl` supports Docker-compatible health checks for containers, allowing users to monitor container health via a user-defined command. -Currently, health checks can be triggered manually using the nerdctl container healthcheck command. Automatic orchestration (e.g., periodic checks) will be added in a future update. +## Configuration Options +| :zap: Requirement | nerdctl >= 2.1.5 | +|-------------------|----------------| Health checks can be configured in multiple ways: -1. At container creation time using nerdctl run or nerdctl create with `--health-*` flags +1. At container creation time using `nerdctl run` or `nerdctl create` with these flags: + - `--health-cmd`: Command to run to check health + - `--health-interval`: Time between running the check (default: 30s) + - `--health-timeout`: Maximum time to allow one check to run (default: 30s) + - `--health-retries`: Consecutive failures needed to report unhealthy (default: 3) + - `--health-start-period`: Start period for the container to initialize before starting health-retries countdown + - `--no-healthcheck`: Disable any container-specified HEALTHCHECK + 2. At image build time using HEALTHCHECK in a Dockerfile -3. In docker-compose.yaml files, if using nerdctl compose -When a container is created, nerdctl determines the health check configuration based on the following priority: +**Note:** The `--health-start-interval` option is currently not supported by nerdctl. -1. **CLI flags** take highest precedence (e.g., `--health-cmd`, etc.) -2. If no CLI flags are set, nerdctl will use any health check defined in the image. -3. If neither is present, no health check will be configured +## Configuration Priority -Example: +When a container is created, nerdctl determines the health check configuration based on this priority: -```bash -nerdctl run --name web --health-cmd="curl -f http://localhost || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 nginx -``` +1. CLI flags take highest precedence (e.g., `--health-cmd`, etc.) +2. If no CLI flags are set, nerdctl will use any health check defined in the image +3. If neither is present, no health check will be configured ### Disabling Health Checks @@ -37,15 +43,54 @@ configured health check inside the container and reports the result. It serves a health checks, especially in scenarios where external scheduling is used. Example: - ``` nerdctl container healthcheck ``` -### Future Work (WIP) +## Automatic Health Checks with systemd + +On Linux systems with systemd, nerdctl automatically creates and manages systemd timer units to execute health checks at the configured intervals. This provides reliable scheduling and execution of health checks without requiring a persistent daemon. + +### Requirements for Automatic Health Checks + +- systemd must be available on the system +- Container must not be running in rootless mode +- Configuration property `disable_hc_systemd` must not be set to `true` in nerdctl.toml + +### How It Works -Since nerdctl is daemonless and does not have a persistent background process, we rely on systemd(or external schedulers) -to invoke nerdctl container healthcheck at configured intervals. This allows periodic health checks for containers in a -systemd-based environment. We are actively working on automating health checks, where we will listen to container lifecycle -events and generate appropriate systemd service and timer units. This will enable nerdctl to support automated, -Docker-compatible health checks by leveraging systemd for scheduling and lifecycle integration. \ No newline at end of file +1. When a container with health checks is created, nerdctl: + - Creates a systemd timer unit for the container + - Configures the timer according to the health check interval + - Starts monitoring the container's health status + +2. The health check status can be one of: + - `starting`: During container initialization + - `healthy`: When health checks are passing + - `unhealthy`: After specified number of consecutive failures +## Examples + +1. Basic health check that verifies a web server: +```bash +nerdctl run -d --name web \ + --health-cmd="curl -f http://localhost/ || exit 1" \ + --health-interval=5s \ + --health-retries=3 \ + nginx +``` + +2. Health check with initialization period: +```bash +nerdctl run -d --name app \ + --health-cmd="./health-check.sh" \ + --health-interval=30s \ + --health-timeout=10s \ + --health-retries=3 \ + --health-start-period=60s \ + myapp +``` + +3. Disable health checks: +```bash +nerdctl run --no-healthcheck myapp +``` diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 3e157bb303d..a19fb5aea1f 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -292,13 +292,12 @@ type ContainerCreateOptions struct { ImagePullOpt ImagePullOptions // Healthcheck related fields - HealthCmd string - HealthInterval time.Duration - HealthTimeout time.Duration - HealthRetries int - HealthStartPeriod time.Duration - HealthStartInterval time.Duration - NoHealthcheck bool + HealthCmd string + HealthInterval time.Duration + HealthTimeout time.Duration + HealthRetries int + HealthStartPeriod time.Duration + NoHealthcheck bool // UserNS name for user namespace mapping of container UserNS string diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index 21bed075580..fd4e2cfa466 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/volume" "github.com/containerd/nerdctl/v2/pkg/composer" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/ipfs" @@ -156,7 +157,7 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op return err } - return composer.New(options, client) + return composer.New(options, client, (*config.Config)(&globalOptions)) } func imageVerifyOptionsFromCompose(ps *serviceparser.Service) types.ImageVerifyOptions { diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index a0c8fc2bf9b..232d8a27b77 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -891,9 +891,6 @@ func withHealthcheck(options types.ContainerCreateOptions, ensuredImage *imgutil if options.HealthStartPeriod != 0 { hc.StartPeriod = options.HealthStartPeriod } - if options.HealthStartInterval != 0 { - hc.StartInterval = options.HealthStartInterval - } // If no healthcheck config is set (via CLI or image), return empty string so we skip adding to container config. if reflect.DeepEqual(hc, &healthcheck.Healthcheck{}) { diff --git a/pkg/cmd/container/health_check.go b/pkg/cmd/container/health_check.go index 6fe31c8ebc3..e2646497c3f 100644 --- a/pkg/cmd/container/health_check.go +++ b/pkg/cmd/container/health_check.go @@ -59,7 +59,6 @@ func HealthCheck(ctx context.Context, client *containerd.Client, container conta hcConfig.Interval = timeoutWithDefault(hcConfig.Interval, healthcheck.DefaultProbeInterval) hcConfig.Timeout = timeoutWithDefault(hcConfig.Timeout, healthcheck.DefaultProbeTimeout) hcConfig.StartPeriod = timeoutWithDefault(hcConfig.StartPeriod, healthcheck.DefaultStartPeriod) - hcConfig.StartInterval = timeoutWithDefault(hcConfig.StartInterval, healthcheck.DefaultStartInterval) if hcConfig.Retries == 0 { hcConfig.Retries = healthcheck.DefaultProbeRetries } diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go index 080336d9f87..d42a7cd8c82 100644 --- a/pkg/cmd/container/kill.go +++ b/pkg/cmd/container/kill.go @@ -35,6 +35,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/netutil" @@ -111,6 +112,11 @@ func killContainer(ctx context.Context, container containerd.Container, signal s return err } + // Clean up healthcheck systemd units + if err := healthcheck.RemoveTransientHealthCheckFiles(ctx, container); err != nil { + log.G(ctx).Warnf("failed to clean up healthcheck units for container %s: %s", container.ID(), err) + } + // signal will be sent once resume is finished if paused { if err := task.Resume(ctx); err != nil { diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index 28048a2f6a1..b9df2b2acaf 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -34,6 +34,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" @@ -179,6 +180,11 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions // Otherwise, nil the error so that we do not write the error label on the container retErr = nil + // Clean up healthcheck systemd units + if err := healthcheck.RemoveTransientHealthCheckFiles(ctx, c); err != nil { + log.G(ctx).WithError(err).Warnf("failed to clean up healthcheck units for container %q", id) + } + // Now, delete the actual container var delOpts []containerd.DeleteOpts if _, err := c.Image(ctx); err == nil { diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go index 3b376ada5a5..0a903665275 100644 --- a/pkg/cmd/container/restart.go +++ b/pkg/cmd/container/restart.go @@ -23,6 +23,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" ) @@ -38,7 +39,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string if err := containerutil.Stop(ctx, found.Container, options.Timeout, options.Signal); err != nil { return err } - if err := containerutil.Start(ctx, found.Container, false, false, client, ""); err != nil { + if err := containerutil.Start(ctx, found.Container, false, false, client, "", (*config.Config)(&options.GOption)); err != nil { return err } _, err := fmt.Fprintln(options.Stdout, found.Req) diff --git a/pkg/cmd/container/start.go b/pkg/cmd/container/start.go index b0820d2aa39..14663b81b9d 100644 --- a/pkg/cmd/container/start.go +++ b/pkg/cmd/container/start.go @@ -23,6 +23,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" ) @@ -40,7 +41,7 @@ func Start(ctx context.Context, client *containerd.Client, reqs []string, option if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys); err != nil { + if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, (*config.Config)(&options.GOptions)); err != nil { return err } if !options.Attach { diff --git a/pkg/cmd/container/stop.go b/pkg/cmd/container/stop.go index e1f347b6b96..755686e4bd8 100644 --- a/pkg/cmd/container/stop.go +++ b/pkg/cmd/container/stop.go @@ -25,6 +25,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" ) @@ -39,6 +40,9 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ if err := cleanupNetwork(ctx, found.Container, opt.GOptions); err != nil { return fmt.Errorf("unable to cleanup network for container: %s", found.Req) } + if err := healthcheck.RemoveTransientHealthCheckFiles(ctx, found.Container); err != nil { + return fmt.Errorf("unable to cleanup healthcheck timer for container: %s: %w", found.Req, err) + } if err := containerutil.Stop(ctx, found.Container, opt.Timeout, opt.Signal); err != nil { if errdefs.IsNotFound(err) { fmt.Fprintf(opt.Stderr, "No such container: %s\n", found.Req) diff --git a/pkg/cmd/container/unpause.go b/pkg/cmd/container/unpause.go index cc6f8a5781d..fb0354efe80 100644 --- a/pkg/cmd/container/unpause.go +++ b/pkg/cmd/container/unpause.go @@ -23,6 +23,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" ) @@ -35,7 +36,7 @@ func Unpause(ctx context.Context, client *containerd.Client, reqs []string, opti if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := containerutil.Unpause(ctx, client, found.Container.ID()); err != nil { + if err := containerutil.Unpause(ctx, client, found.Container.ID(), (*config.Config)(&options.GOptions)); err != nil { return err } diff --git a/pkg/composer/composer.go b/pkg/composer/composer.go index 539971d1f7e..a645d07e34a 100644 --- a/pkg/composer/composer.go +++ b/pkg/composer/composer.go @@ -30,6 +30,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/identifiers" "github.com/containerd/nerdctl/v2/pkg/reflectutil" ) @@ -54,7 +55,7 @@ type Options struct { IPFSAddress string } -func New(o Options, client *containerd.Client) (*Composer, error) { +func New(o Options, client *containerd.Client, cfg *config.Config) (*Composer, error) { if o.NerdctlCmd == "" { return nil, errors.New("got empty nerdctl cmd") } @@ -119,6 +120,7 @@ func New(o Options, client *containerd.Client) (*Composer, error) { Options: o, project: project, client: client, + config: cfg, } return c, nil @@ -128,6 +130,7 @@ type Composer struct { Options project *compose.Project client *containerd.Client + config *config.Config } func (c *Composer) createNerdctlCmd(ctx context.Context, args ...string) *exec.Cmd { diff --git a/pkg/composer/pause.go b/pkg/composer/pause.go index d0e7bc5aa77..7e8e331cafb 100644 --- a/pkg/composer/pause.go +++ b/pkg/composer/pause.go @@ -83,7 +83,7 @@ func (c *Composer) Unpause(ctx context.Context, services []string, writer io.Wri for _, container := range containers { container := container eg.Go(func() error { - if err := containerutil.Unpause(ctx, c.client, container.ID()); err != nil { + if err := containerutil.Unpause(ctx, c.client, container.ID(), c.config); err != nil { return err } info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/pkg/config/config.go b/pkg/config/config.go index a2eae17764a..5ecf41b9256 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -46,6 +46,7 @@ type Config struct { DNS []string `toml:"dns,omitempty"` DNSOpts []string `toml:"dns_opts,omitempty"` DNSSearch []string `toml:"dns_search,omitempty"` + DisableHCSystemd bool `toml:"disable_hc_systemd"` } // New creates a default Config object statically, @@ -71,5 +72,6 @@ func New() *Config { DNS: []string{}, DNSOpts: []string{}, DNSSearch: []string{}, + DisableHCSystemd: false, } } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 31ded1a3b93..73ef458a016 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -45,9 +45,11 @@ import ( "github.com/containerd/go-cni" "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/consoleutil" "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels" @@ -204,7 +206,7 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) } // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. -func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string) (err error) { +func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, cfg *config.Config) (err error) { // defer the storage of start error in the dedicated label defer func() { if err != nil { @@ -286,6 +288,15 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i if err := task.Start(ctx); err != nil { return err } + + // If container has health checks configured, create and start systemd timer/service files. + if err := healthcheck.CreateTimer(ctx, container, cfg); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } + if !isAttach { return nil } @@ -501,7 +512,7 @@ func Pause(ctx context.Context, client *containerd.Client, id string) error { } // Unpause unpauses a container by its id. -func Unpause(ctx context.Context, client *containerd.Client, id string) error { +func Unpause(ctx context.Context, client *containerd.Client, id string, cfg *config.Config) error { container, err := client.LoadContainer(ctx, id) if err != nil { return err @@ -517,6 +528,14 @@ func Unpause(ctx context.Context, client *containerd.Client, id string) error { return err } + // Recreate healthcheck related systemd timer/service files. + if err := healthcheck.CreateTimer(ctx, container, cfg); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } + switch status.Status { case containerd.Paused: return task.Resume(ctx) diff --git a/pkg/healthcheck/executor.go b/pkg/healthcheck/executor.go index f5c4216b302..2525ee7c54b 100644 --- a/pkg/healthcheck/executor.go +++ b/pkg/healthcheck/executor.go @@ -125,29 +125,47 @@ func updateHealthStatus(ctx context.Context, container containerd.Container, hcC return fmt.Errorf("failed to read health state from labels: %w", err) } if currentHealth == nil { + // Determine if we should start in the start period workflow + hasStartPeriod := hcConfig.StartPeriod > 0 currentHealth = &HealthState{ Status: Starting, FailingStreak: 0, + InStartPeriod: hasStartPeriod, } } - // Check if still within start period - startPeriod := hcConfig.StartPeriod + // Get container info for start period check info, err := container.Info(ctx) if err != nil { return fmt.Errorf("failed to get container info: %w", err) } containerCreated := info.CreatedAt - stillInStartPeriod := hcResult.Start.Sub(containerCreated) < startPeriod - - // Update health status based on exit code - if hcResult.ExitCode == 0 { - currentHealth.Status = Healthy - currentHealth.FailingStreak = 0 - } else if !stillInStartPeriod { - currentHealth.FailingStreak++ - if currentHealth.FailingStreak >= hcConfig.Retries { - currentHealth.Status = Unhealthy + + // Check if we're in start period workflow + inStartPeriodTime := hcResult.Start.Sub(containerCreated) < hcConfig.StartPeriod + inStartPeriodState := currentHealth.InStartPeriod + + if inStartPeriodTime && inStartPeriodState { + // Start Period Workflow + if hcResult.ExitCode == 0 { + // First healthy result transitions us out of start period + currentHealth.Status = Healthy + currentHealth.FailingStreak = 0 + currentHealth.InStartPeriod = false + } + // Ignore unhealthy results during start period + } else { + // Health Interval Workflow + if hcResult.ExitCode == 0 { + if currentHealth.Status != Healthy { + currentHealth.Status = Healthy + currentHealth.FailingStreak = 0 + } + } else { + currentHealth.FailingStreak++ + if currentHealth.FailingStreak >= hcConfig.Retries && currentHealth.Status != Unhealthy { + currentHealth.Status = Unhealthy + } } } diff --git a/pkg/healthcheck/health.go b/pkg/healthcheck/health.go index 8e0301b492a..c074c15c413 100644 --- a/pkg/healthcheck/health.go +++ b/pkg/healthcheck/health.go @@ -43,7 +43,6 @@ const ( DefaultProbeInterval = 30 * time.Second // Default interval between probe runs. Also applies before the first probe. DefaultProbeTimeout = 30 * time.Second // Max duration a single probe run may take before it's considered failed. DefaultStartPeriod = 0 * time.Second // Grace period for container startup before health checks count as failures. - DefaultStartInterval = 5 * time.Second // Interval between checks during the start period. DefaultProbeRetries = 3 // Number of consecutive failures before marking container as unhealthy. MaxLogEntries = 5 // Maximum number of health check log entries to keep. MaxOutputLenForInspect = 4096 // Max output length (in bytes) stored in health check logs during inspect. Longer outputs are truncated. @@ -70,18 +69,18 @@ type HealthcheckResult struct { // Healthcheck represents the health check configuration type Healthcheck struct { - Test []string `json:"Test,omitempty"` // Test is the check to perform that the container is healthy - Interval time.Duration `json:"Interval,omitempty"` // Interval is the time to wait between checks - Timeout time.Duration `json:"Timeout,omitempty"` // Timeout is the time to wait before considering the check to have hung - Retries int `json:"Retries,omitempty"` // Retries is the number of consecutive failures needed to consider a container as unhealthy - StartPeriod time.Duration `json:"StartPeriod,omitempty"` // StartPeriod is the period for the container to initialize before the health check starts - StartInterval time.Duration `json:"StartInterval,omitempty"` // StartInterval is the time between health checks during the start period + Test []string `json:"Test,omitempty"` // Test is the check to perform that the container is healthy + Interval time.Duration `json:"Interval,omitempty"` // Interval is the time to wait between checks + Timeout time.Duration `json:"Timeout,omitempty"` // Timeout is the time to wait before considering the check to have hung + Retries int `json:"Retries,omitempty"` // Retries is the number of consecutive failures needed to consider a container as unhealthy + StartPeriod time.Duration `json:"StartPeriod,omitempty"` // StartPeriod is the period for the container to initialize before the health check starts } // HealthState stores the current health state of a container type HealthState struct { Status HealthStatus // Status is one of [Starting], [Healthy] or [Unhealthy] FailingStreak int // FailingStreak is the number of consecutive failures + InStartPeriod bool // InStartPeriod indicates if we're in the start period workflow } // ToJSONString serializes HealthState to a JSON string for label storage diff --git a/pkg/healthcheck/healthcheck_manager_darwin.go b/pkg/healthcheck/healthcheck_manager_darwin.go new file mode 100644 index 00000000000..b708b574281 --- /dev/null +++ b/pkg/healthcheck/healthcheck_manager_darwin.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 healthcheck + +import ( + "context" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/config" +) + +// CreateTimer sets up the transient systemd timer and service for healthchecks. +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// StartTimer starts the healthcheck timer unit. +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// RemoveTransientHealthCheckFiles stops and cleans up the transient timer and service. +func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.Container) error { + return nil +} + +// ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service +// using just the container ID. This function is non-blocking and uses timeouts to prevent hanging +// on systemd operations. On Darwin, this is a no-op since systemd is not available. +func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID string) error { + return nil +} diff --git a/pkg/healthcheck/healthcheck_manager_freebsd.go b/pkg/healthcheck/healthcheck_manager_freebsd.go new file mode 100644 index 00000000000..b708b574281 --- /dev/null +++ b/pkg/healthcheck/healthcheck_manager_freebsd.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 healthcheck + +import ( + "context" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/config" +) + +// CreateTimer sets up the transient systemd timer and service for healthchecks. +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// StartTimer starts the healthcheck timer unit. +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// RemoveTransientHealthCheckFiles stops and cleans up the transient timer and service. +func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.Container) error { + return nil +} + +// ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service +// using just the container ID. This function is non-blocking and uses timeouts to prevent hanging +// on systemd operations. On Darwin, this is a no-op since systemd is not available. +func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID string) error { + return nil +} diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go new file mode 100644 index 00000000000..e043b5c2d37 --- /dev/null +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -0,0 +1,265 @@ +/* + 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 healthcheck + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/coreos/go-systemd/v22/dbus" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/containerd/nerdctl/v2/pkg/defaults" + "github.com/containerd/nerdctl/v2/pkg/labels" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" +) + +// CreateTimer sets up the transient systemd timer and service for healthchecks. +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + hc := extractHealthcheck(ctx, container) + if hc == nil { + return nil + } + if shouldSkipHealthCheckSystemd(hc, cfg) { + return nil + } + + containerID := container.ID() + log.G(ctx).Debugf("Creating healthcheck timer unit: %s", containerID) + + cmdOpts := []string{} + if path := os.Getenv("PATH"); path != "" { + cmdOpts = append(cmdOpts, "--setenv=PATH="+path) + } + + // Always use health-interval for timer frequency + cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s") + + cmdOpts = append(cmdOpts, "nerdctl", "container", "healthcheck", containerID) + if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { + cmdOpts = append(cmdOpts, "--debug") + } + + log.G(ctx).Debugf("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) + run := exec.Command("systemd-run", cmdOpts...) + if out, err := run.CombinedOutput(); err != nil { + return fmt.Errorf("systemd-run failed: %w\noutput: %s", err, strings.TrimSpace(string(out))) + } + + return nil +} + +// StartTimer starts the healthcheck timer unit. +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + hc := extractHealthcheck(ctx, container) + if hc == nil { + return nil + } + if shouldSkipHealthCheckSystemd(hc, cfg) { + return nil + } + + containerID := container.ID() + var conn *dbus.Conn + var err error + if rootlessutil.IsRootless() { + conn, err = dbus.NewUserConnectionContext(ctx) + } else { + conn, err = dbus.NewSystemConnectionContext(ctx) + } + if err != nil { + return fmt.Errorf("systemd DBUS connect error: %w", err) + } + defer conn.Close() + + startChan := make(chan string) + unit := containerID + ".service" + if _, err := conn.RestartUnitContext(context.Background(), unit, "fail", startChan); err != nil { + return err + } + if msg := <-startChan; msg != "done" { + return fmt.Errorf("unexpected systemd restart result: %s", msg) + } + return nil +} + +// RemoveTransientHealthCheckFiles stops and cleans up the transient timer and service. +func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.Container) error { + hc := extractHealthcheck(ctx, container) + if hc == nil { + return nil + } + + return ForceRemoveTransientHealthCheckFiles(ctx, container.ID()) +} + +// ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service +// using just the container ID. This function is non-blocking and uses timeouts to prevent hanging +// on systemd operations. It logs errors as warnings but continues cleanup attempts. +func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID string) error { + log.G(ctx).Debugf("Force removing healthcheck timer unit: %s", containerID) + + // Create a timeout context for systemd operations + timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + timer := containerID + ".timer" + service := containerID + ".service" + + // Channel to collect any critical errors (though we'll continue cleanup regardless) + errChan := make(chan error, 3) + + // Goroutine for DBUS connection and cleanup operations + go func() { + defer close(errChan) + + var conn *dbus.Conn + var err error + if rootlessutil.IsRootless() { + conn, err = dbus.NewUserConnectionContext(ctx) + } else { + conn, err = dbus.NewSystemConnectionContext(ctx) + } + if err != nil { + log.G(ctx).Warnf("systemd DBUS connect error during force cleanup: %v", err) + errChan <- fmt.Errorf("systemd DBUS connect error: %w", err) + return + } + defer conn.Close() + + // Stop timer with timeout + go func() { + select { + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout stopping timer %s during force cleanup", timer) + return + default: + tChan := make(chan string, 1) + if _, err := conn.StopUnitContext(timeoutCtx, timer, "ignore-dependencies", tChan); err == nil { + select { + case msg := <-tChan: + if msg != "done" { + log.G(ctx).Warnf("timer stop message during force cleanup: %s", msg) + } + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout waiting for timer stop confirmation: %s", timer) + } + } else { + log.G(ctx).Warnf("failed to stop timer %s during force cleanup: %v", timer, err) + } + } + }() + + // Stop service with timeout + go func() { + select { + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout stopping service %s during force cleanup", service) + return + default: + sChan := make(chan string, 1) + if _, err := conn.StopUnitContext(timeoutCtx, service, "ignore-dependencies", sChan); err == nil { + select { + case msg := <-sChan: + if msg != "done" { + log.G(ctx).Warnf("service stop message during force cleanup: %s", msg) + } + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout waiting for service stop confirmation: %s", service) + } + } else { + log.G(ctx).Warnf("failed to stop service %s during force cleanup: %v", service, err) + } + } + }() + + // Reset failed units (best effort, non-blocking) + go func() { + select { + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout resetting failed unit %s during force cleanup", service) + return + default: + if err := conn.ResetFailedUnitContext(timeoutCtx, service); err != nil { + log.G(ctx).Warnf("failed to reset failed unit %s during force cleanup: %v", service, err) + } + } + }() + + // Wait a short time for operations to complete, but don't block indefinitely + select { + case <-time.After(3 * time.Second): + log.G(ctx).Debugf("force cleanup operations completed for container %s", containerID) + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("force cleanup timed out for container %s", containerID) + } + }() + + // Wait for the cleanup goroutine to finish or timeout + select { + case err := <-errChan: + if err != nil { + log.G(ctx).Warnf("force cleanup encountered errors but continuing: %v", err) + } + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("force cleanup timed out for container %s, but cleanup may continue in background", containerID) + } + + // Always return nil - this function should never block the caller + // even if systemd operations fail or timeout + log.G(ctx).Debugf("force cleanup completed (non-blocking) for container %s", containerID) + return nil +} + +func extractHealthcheck(ctx context.Context, container containerd.Container) *Healthcheck { + l, err := container.Labels(ctx) + if err != nil { + log.G(ctx).WithError(err).Debugf("could not get labels for container %s", container.ID()) + return nil + } + hcStr, ok := l[labels.HealthCheck] + if !ok || hcStr == "" { + return nil + } + hc, err := HealthCheckFromJSON(hcStr) + if err != nil { + log.G(ctx).WithError(err).Debugf("invalid healthcheck config on container %s", container.ID()) + return nil + } + return hc +} + +// shouldSkipHealthCheckSystemd determines if healthcheck timers should be skipped. +func shouldSkipHealthCheckSystemd(hc *Healthcheck, cfg *config.Config) bool { + // Don't proceed if systemd is unavailable or disabled + if !defaults.IsSystemdAvailable() || cfg.DisableHCSystemd || rootlessutil.IsRootless() { + return true + } + + // Don't proceed if health check is nil, empty, explicitly NONE or interval is 0. + if hc == nil || len(hc.Test) == 0 || hc.Test[0] == "NONE" || hc.Interval == 0 { + return true + } + return false +} diff --git a/pkg/healthcheck/healthcheck_manager_windows.go b/pkg/healthcheck/healthcheck_manager_windows.go new file mode 100644 index 00000000000..1da386fe2bc --- /dev/null +++ b/pkg/healthcheck/healthcheck_manager_windows.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 healthcheck + +import ( + "context" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/config" +) + +// CreateTimer sets up the transient systemd timer and service for healthchecks. +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// StartTimer starts the healthcheck timer unit. +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { + return nil +} + +// RemoveTransientHealthCheckFiles stops and cleans up the transient timer and service. +func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.Container) error { + return nil +} + +// ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service +// using just the container ID. This function is non-blocking and uses timeouts to prevent hanging +// on systemd operations. On Windows, this is a no-op since systemd is not available. +func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID string) error { + return nil +} From 5da632340c38e77dd670d9c01fd37232c92ab2ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:01:58 +0000 Subject: [PATCH 228/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.8.2 to 2.9.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.8.2...v2.9.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7d354011645..8f45a5c5a47 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.8.2 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.9.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index fe81460cf7b..49b4fcdce2d 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.8.2 h1:A1iVoZJUex7buGv1CpnC5uwNuyTMBYpDAmBnAQmia9Q= -github.com/compose-spec/compose-go/v2 v2.8.2/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= +github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM= +github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= From 01076c13708217a79cea504410ea0dcef1c78d29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:02:43 +0000 Subject: [PATCH 229/868] build(deps): bump actions/cache from 4.2.4 to 4.3.0 Bumps [actions/cache](https://github.com/actions/cache) from 4.2.4 to 4.3.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/0400d5f644dc74513175e3cd8d07132dd4860809...0057852bfaa89a56745cba8c7296529d2fc39830) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 27d3fae765e..aa160b95fb5 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -35,7 +35,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index ec03d0f182f..6f6f97be75e 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: /root/.vagrant.d key: vagrant From 4b87cfe2b8cdbb0282e5fd1c5a770292ddcc80ff Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 25 Sep 2025 23:26:39 +0900 Subject: [PATCH 230/868] rootful: reserve the ports on the host When running in rootful mode, reserve the ports on the host so that the ports appears on /proc/net/tcp. This also prevents other processes from binding to the same ports. Note that in rootless mode this is not necessary because RootlessKit's port driver already reserves the ports. See lima-vm/lima issue 4085 Similar patterns are used in Docker and Podman. - moby/moby PR 48132 - containers/podman PR 23446 Signed-off-by: Akihiro Suda --- pkg/ocihook/ocihook.go | 117 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 89b6c6b1410..e860f1b1a68 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -26,6 +26,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -420,11 +421,94 @@ func getIP6AddressOpts(opts *handlerOpts) ([]cni.NamespaceOpts, error) { return nil, nil } +func reserveSocket(protocol, hostAddr string) (*os.File, error) { + type filer interface { + File() (*os.File, error) + } + var f filer + switch { + case strings.HasPrefix(protocol, "tcp"): + l, err := net.Listen(protocol, hostAddr) + if err != nil { + return nil, err + } + defer l.Close() + var ok bool + f, ok = l.(filer) + if !ok { + return nil, fmt.Errorf("cannot get file descriptor from the listener of type %T", l) + } + case strings.HasPrefix(protocol, "udp"): + l, err := net.ListenPacket(protocol, hostAddr) + if err != nil { + return nil, err + } + defer l.Close() + var ok bool + f, ok = l.(filer) + if !ok { + return nil, fmt.Errorf("cannot get file descriptor from the listener of type %T", l) + } + default: + return nil, fmt.Errorf("unsupported protocol %q", protocol) + } + return f.File() +} + +// portReserverPidFilePath returns /run/nerdctl///port-reserver.pid +func portReserverPidFilePath(opts *handlerOpts) string { + return filepath.Join("/run/nerdctl/", opts.state.Annotations[labels.Namespace], opts.state.ID, "port-reserver.pid") +} + func applyNetworkSettings(opts *handlerOpts) (err error) { portMapOpts, err := getPortMapOpts(opts) if err != nil { return err } + if !rootlessutil.IsRootlessChild() && len(opts.ports) > 0 { + // When running in rootful mode, reserve the ports on the host + // so that the ports appears on /proc/net/tcp. + // + // This also prevents other processes from binding to the same ports. + // + // Note that in rootless mode this is not necessary because + // RootlessKit's port driver already reserves the ports. + // + // See https://github.com/lima-vm/lima/issues/4085 + // + // Similar patterns are used in Docker and Podman. + // - https://github.com/moby/moby/pull/48132 + // - https://github.com/containers/podman/pull/23446 + reserverCmd := exec.Command("sleep", "infinity") + for _, p := range opts.ports { + protocol := p.Protocol + if !strings.HasSuffix(protocol, "4") && !strings.HasSuffix(protocol, "6") { + // e.g. "tcp" -> "tcp4" + protocol += "4" + } + hostAddr := net.JoinHostPort(p.HostIP, strconv.Itoa(int(p.HostPort))) + f, err := reserveSocket(protocol, hostAddr) + if err != nil { + log.L.WithError(err).Warnf("cannot reserve the port %s/%s", hostAddr, protocol) + continue + } + reserverCmd.ExtraFiles = append(reserverCmd.ExtraFiles, f) + } + if err := reserverCmd.Start(); err != nil { + return fmt.Errorf("cannot start the port reserver process: %w", err) + } + reserverCmdPid := reserverCmd.Process.Pid + log.L.Debugf("started the port reserver process (pid=%d)", reserverCmdPid) + defer func() { + if err != nil { + log.L.Debugf("killing the port reserver process (pid=%d)", reserverCmdPid) + _ = reserverCmd.Process.Kill() + } + }() + if err := writePidFile(portReserverPidFilePath(opts), reserverCmdPid); err != nil { + return fmt.Errorf("cannot write the pid file of the port reserver process: %w", err) + } + } nsPath, err := getNetNSPath(opts.state) if err != nil { return err @@ -659,6 +743,11 @@ func onPostStop(opts *handlerOpts) error { if err := namst.Release(name, opts.state.ID); err != nil && !errors.Is(err, store.ErrNotFound) { return fmt.Errorf("failed to release container name %s: %w", name, err) } + // Kill port-reserver process if any + portReserverPidFile := portReserverPidFilePath(opts) + if err = killProcessByPidFile(portReserverPidFile); err != nil { + log.L.WithError(err).Errorf("failed to kill the port-reserver process") + } return nil } @@ -706,7 +795,11 @@ func writePidFile(path string, pid int) error { if err != nil { return err } - tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path))) + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + tempPath := filepath.Join(dir, fmt.Sprintf(".%s", filepath.Base(path))) f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666) if err != nil { return err @@ -718,3 +811,25 @@ func writePidFile(path string, pid int) error { } return os.Rename(tempPath, path) } + +func killProcessByPidFile(pidFile string) error { + pidData, err := os.ReadFile(pidFile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = nil + } + return err + } + pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) + if err != nil { + return fmt.Errorf("failed to parse pid %q from %q: %w", string(pidData), pidFile, err) + } + proc, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("failed to find process %d: %w", pid, err) + } + if err := proc.Kill(); err != nil { + return fmt.Errorf("failed to kill process %d: %w", pid, err) + } + return nil +} From cebdcbcdebc9a6e08428e0773bd7fd32caf92e74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:13:42 +0000 Subject: [PATCH 231/868] build(deps): bump docker/login-action from 3.5.0 to 3.6.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/184bdaa0721073962dff0199f1fb9940f07167d1...5e57cd118135c172c3672efd75eb46360885c0ef) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 605ac04b250..b0c2801f02a 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From eb3ccbceb62a8925da833314b032f498a0cfdea7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 30 Sep 2025 16:38:46 +0900 Subject: [PATCH 232/868] Add TestReservePorts TestReservePorts tests that a published port appears as a listening port on the host. Follow-up to PR 4526 Signed-off-by: Akihiro Suda --- .../container_run_network_linux_test.go | 55 +++++++++++++++++++ pkg/testutil/images.yaml | 4 ++ pkg/testutil/nerdtest/requirements.go | 18 ++++++ pkg/testutil/testutil_linux.go | 1 + 4 files changed, 78 insertions(+) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 02f01677cee..6d7b353cdea 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -1080,3 +1080,58 @@ dns_search = ["example.com", "test.local"]` } testCase.Run(t) } + +// TestReservePorts tests that a published port appears +// as a listening port on the host. +// See https://github.com/containerd/nerdctl/pull/4526 +func TestReservePorts(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.All( + require.Not(require.Windows), + require.Not(nerdtest.RootlessWithoutDetachNetNS), // RootlessKit v1 + ), + NoParallel: true, + SubTests: []*test.Case{ + { + Description: "TCP", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("nginx"), + "-p", "60080:80", testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("nginx")) + time.Sleep(3 * time.Second) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("nginx")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--network=host", testutil.CommonImage, "netstat", "-lnt") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains(":60080"), + )), + }, + { + Description: "UDP", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("coredns"), + "-p", "60053:53/udp", testutil.CoreDNSImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("coredns")) + time.Sleep(3 * time.Second) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("coredns")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--network=host", testutil.CommonImage, "netstat", "-lnu") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains(":60053"), + )), + }, + }, + } + testCase.Run(t) +} diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 67c0e3a3551..089273231e7 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -75,6 +75,10 @@ ubuntu: ref: "public.ecr.aws/docker/library/ubuntu" tag: "23.10" +coredns: + ref: "public.ecr.aws/eks-distro/coredns/coredns" + tag: "v1.12.2-eks-1-31-latest" + # Future: images to add or update soon. # busybox:1.37.0@sha256:37f7b378a29ceb4c551b1b5582e27747b855bbfaa73fa11914fe0df028dc581f # debian:bookworm-slim@sha256:b1211f6d19afd012477bd34fdcabb6b663d680e0f4b0537da6e6b0fd057a3ec3 diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 3741b8f9aa5..d4af8490339 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -161,6 +161,24 @@ var Rootless = &test.Requirement{ }, } +// RootlessWithDetachNetNS marks a test as suitable only for rootless environment with detached netns support. +var RootlessWithDetachNetNS = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ns, err := rootlessutil.DetachedNetNS() + if err != nil { + return false, fmt.Sprintf("failed to check for detached netns: %+v", err) + } + if ns == "" { + return false, "detached netns is not supported" + } + return true, "detached netns is supported" + }, +} + +// RootlessWithoutDetachNetNS marks a test as suitable only for rootless environment without detached netns support. +// i.e., RootlessKit v1. +var RootlessWithoutDetachNetNS = require.All(Rootless, require.Not(RootlessWithDetachNetNS)) + // Rootful marks a test as suitable only for rootful env var Rootful = require.Not(Rootless) diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index d50d6e30489..3f7f2c85337 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -34,6 +34,7 @@ var ( FedoraESGZImage = GetTestImage("fedora_esgz") // eStargz FfmpegSociImage = GetTestImage("ffmpeg_soci") // SOCI UbuntuImage = GetTestImage("ubuntu") // Large enough for testing soci index creation + CoreDNSImage = GetTestImage("coredns") ) const ( From 3f90997aa805182cdf28904985f1fd929b6ca80e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:02:25 +0000 Subject: [PATCH 233/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.4.0+incompatible to 28.5.0+incompatible - [Commits](https://github.com/docker/cli/compare/v28.4.0...v28.5.0) Updates `github.com/docker/docker` from 28.4.0+incompatible to 28.5.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.4.0...v28.5.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.5.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.5.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8f45a5c5a47..5fe166f9802 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.4.0+incompatible //gomodjail:unconfined - github.com/docker/docker v28.4.0+incompatible //gomodjail:unconfined + github.com/docker/cli v28.5.0+incompatible //gomodjail:unconfined + github.com/docker/docker v28.5.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 49b4fcdce2d..c2c2175d539 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= -github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= -github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk= +github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E= +github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= From 65c395c0cfa52620498f8c779f72b4b77d04d2e8 Mon Sep 17 00:00:00 2001 From: Kay Yan Date: Thu, 9 Oct 2025 06:45:19 +0000 Subject: [PATCH 234/868] Fix namestore directory regression: restore names subdirectory in path Signed-off-by: Kay Yan --- pkg/namestore/namestore.go | 2 +- pkg/namestore/namestore_test.go | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 pkg/namestore/namestore_test.go diff --git a/pkg/namestore/namestore.go b/pkg/namestore/namestore.go index 6ded12d6c95..6b65269ef36 100644 --- a/pkg/namestore/namestore.go +++ b/pkg/namestore/namestore.go @@ -40,7 +40,7 @@ func New(stateDir, namespace string) (NameStore, error) { return nil, errors.Join(ErrNameStore, store.ErrInvalidArgument) } - st, err := store.New(filepath.Join(stateDir, namespace), 0, 0) + st, err := store.New(filepath.Join(stateDir, "names", namespace), 0, 0) if err != nil { return nil, errors.Join(ErrNameStore, err) } diff --git a/pkg/namestore/namestore_test.go b/pkg/namestore/namestore_test.go new file mode 100644 index 00000000000..b426d8d63e3 --- /dev/null +++ b/pkg/namestore/namestore_test.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 namestore + +import ( + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/store" +) + +func TestNamestoreNew(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + namespace string + wantErr bool + errChecks []error + }{ + { + name: "empty namespace", + namespace: "", + wantErr: true, + errChecks: []error{ErrNameStore, store.ErrInvalidArgument}, + }, + { + name: "valid namespace", + namespace: "testnamespace", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns, err := New(tempDir, tt.namespace) + if tt.wantErr { + assert.Assert(t, err != nil, "New should return an error for %s", tt.name) + for _, errCheck := range tt.errChecks { + assert.ErrorIs(t, err, errCheck, "Error should contain %v for %s", errCheck, tt.name) + } + } else { + assert.NilError(t, err, "New should succeed for %s", tt.name) + assert.Assert(t, ns != nil, "New should return a non-nil NameStore for %s", tt.name) + + // Check that the directory is created in the correct path + expectedDir := filepath.Join(tempDir, "names", tt.namespace) + _, err = os.Stat(expectedDir) + assert.NilError(t, err, "Directory should be created at the correct path for %s", tt.name) + } + }) + } +} From 9430f110b66eb779d2ff08167aec0300866847fc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 10 Oct 2025 18:14:31 +0900 Subject: [PATCH 235/868] docs/command-reference.md: remove outdated "Windows enabled" notes The previous information was outdated and misleading, as lots of commands are now cross-platform. It was also slightly ruining the readability. Signed-off-by: Akihiro Suda --- docs/command-reference.md | 124 +++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index a3a8979f8de..ec9bfb530b7 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -4,21 +4,21 @@ :nerd_face: = nerdctl specific -:blue_square: = Windows enabled - -Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. -It does not necessarily mean that the corresponding features are missing in containerd. +> [!NOTE] +- Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. + It does not necessarily mean that the corresponding features are missing in containerd. +- Some commands and flags are only available on Linux. - [Container management](#container-management) - - [:whale: :blue_square: nerdctl run](#whale-blue_square-nerdctl-run) - - [:whale: :blue_square: nerdctl exec](#whale-blue_square-nerdctl-exec) - - [:whale: :blue_square: nerdctl create](#whale-blue_square-nerdctl-create) + - [:whale: nerdctl run](#whale-blue_square-nerdctl-run) + - [:whale: nerdctl exec](#whale-blue_square-nerdctl-exec) + - [:whale: nerdctl create](#whale-blue_square-nerdctl-create) - [:whale: nerdctl cp](#whale-nerdctl-cp) - - [:whale: :blue_square: nerdctl ps](#whale-blue_square-nerdctl-ps) - - [:whale: :blue_square: nerdctl inspect](#whale-blue_square-nerdctl-inspect) + - [:whale: nerdctl ps](#whale-blue_square-nerdctl-ps) + - [:whale: nerdctl inspect](#whale-blue_square-nerdctl-inspect) - [:whale: nerdctl logs](#whale-nerdctl-logs) - [:whale: nerdctl port](#whale-nerdctl-port) - [:whale: nerdctl rm](#whale-nerdctl-rm) @@ -39,8 +39,8 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl build](#whale-nerdctl-build) - [:whale: nerdctl commit](#whale-nerdctl-commit) - [Image management](#image-management) - - [:whale: :blue_square: nerdctl images](#whale-blue_square-nerdctl-images) - - [:whale: :blue_square: nerdctl pull](#whale-blue_square-nerdctl-pull) + - [:whale: nerdctl images](#whale-blue_square-nerdctl-images) + - [:whale: nerdctl pull](#whale-blue_square-nerdctl-pull) - [:whale: nerdctl push](#whale-nerdctl-push) - [:whale: nerdctl load](#whale-nerdctl-load) - [:whale: nerdctl save](#whale-nerdctl-save) @@ -75,11 +75,11 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl volume rm](#whale-nerdctl-volume-rm) - [:whale: nerdctl volume prune](#whale-nerdctl-volume-prune) - [Namespace management](#namespace-management) - - [:nerd_face: :blue_square: nerdctl namespace create](#nerd_face-blue_square-nerdctl-namespace-create) - - [:nerd_face: :blue_square: nerdctl namespace inspect](#nerd_face-blue_square-nerdctl-namespace-inspect) - - [:nerd_face: :blue_square: nerdctl namespace ls](#nerd_face-blue_square-nerdctl-namespace-ls) - - [:nerd_face: :blue_square: nerdctl namespace remove](#nerd_face-blue_square-nerdctl-namespace-remove) - - [:nerd_face: :blue_square: nerdctl namespace update](#nerd_face-blue_square-nerdctl-namespace-update) + - [:nerd_face: nerdctl namespace create](#nerd_face-blue_square-nerdctl-namespace-create) + - [:nerd_face: nerdctl namespace inspect](#nerd_face-blue_square-nerdctl-namespace-inspect) + - [:nerd_face: nerdctl namespace ls](#nerd_face-blue_square-nerdctl-namespace-ls) + - [:nerd_face: nerdctl namespace remove](#nerd_face-blue_square-nerdctl-namespace-remove) + - [:nerd_face: nerdctl namespace update](#nerd_face-blue_square-nerdctl-namespace-update) - [AppArmor profile management](#apparmor-profile-management) - [:nerd_face: nerdctl apparmor inspect](#nerd_face-nerdctl-apparmor-inspect) - [:nerd_face: nerdctl apparmor load](#nerd_face-nerdctl-apparmor-load) @@ -135,7 +135,7 @@ It does not necessarily mean that the corresponding features are missing in cont ## Container management -### :whale: :blue_square: nerdctl run +### :whale: nerdctl run Run a command in a new container. @@ -147,11 +147,11 @@ Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]` Basic flags: - :whale: `-a, --attach`: Attach STDIN, STDOUT, or STDERR -- :whale: :blue_square: `-i, --interactive`: Keep STDIN open even if not attached" -- :whale: :blue_square: `-t, --tty`: Allocate a pseudo-TTY +- :whale: `-i, --interactive`: Keep STDIN open even if not attached" +- :whale: `-t, --tty`: Allocate a pseudo-TTY - :warning: WIP: currently `-t` conflicts with `-d` - :whale: `-sig-proxy`: Proxy received signals to the process (default true) -- :whale: :blue_square: `-d, --detach`: Run container in background and print container ID +- :whale: `-d, --detach`: Run container in background and print container ID - :whale: `--restart=(no|always|on-failure|unless-stopped)`: Restart policy to apply when a container exits - Default: "no" - always: Always restart the container if it stops. @@ -180,7 +180,7 @@ Init process flags: Isolation flags: -- :whale: :blue_square: :nerd_face: `--isolation=(default|process|host|hyperv)`: Used on Windows to change process isolation level. `default` will use the runtime options configured in `default_runtime` in the [containerd configuration](https://github.com/containerd/containerd/blob/master/docs/cri/config.md#cri-plugin-config-guide) which is `process` in containerd by default. `process` runs process isolated containers. `host` runs [Host Process containers](https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/). Host process containers inherit permissions from containerd process unless `--user` is specified then will start with user specified and the user specified must be present on the host. `host` requires Containerd 1.7+. `hyperv` runs Hyper-V hypervisor partition-based isolated containers. Not implemented for Linux. +- :whale: :nerd_face: `--isolation=(default|process|host|hyperv)`: Used on Windows to change process isolation level. `default` will use the runtime options configured in `default_runtime` in the [containerd configuration](https://github.com/containerd/containerd/blob/master/docs/cri/config.md#cri-plugin-config-guide) which is `process` in containerd by default. `process` runs process isolated containers. `host` runs [Host Process containers](https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/). Host process containers inherit permissions from containerd process unless `--user` is specified then will start with user specified and the user specified must be present on the host. `host` requires Containerd 1.7+. `hyperv` runs Hyper-V hypervisor partition-based isolated containers. Not implemented for Linux. Network flags: @@ -231,7 +231,7 @@ Resource flags: - :whale: `--cgroupns=(host|private)`: Cgroup namespace to use - Default: "private" on cgroup v2 hosts, "host" on cgroup v1 hosts - :whale: `--cgroup-parent`: Optional parent cgroup for the container -- :whale: :blue_square: `--device`: Add a host device to the container +- :whale: `--device`: Add a host device to the container Intel RDT flags: @@ -239,7 +239,7 @@ Intel RDT flags: User flags: -- :whale: :blue_square: `-u, --user`: Username or UID (format: [:]) +- :whale: `-u, --user`: Username or UID (format: [:]) - :nerd_face: `--umask`: Set the umask inside the container. Defaults to 0022. Corresponds to Podman CLI. - :whale: `--group-add`: Add additional groups to join @@ -274,7 +274,7 @@ Runtime flags: Volume flags: -- :whale: :blue_square: `-v, --volume :[:]`: Bind mount a volume, e.g., `-v /mnt:/mnt:rro,rprivate` +- :whale: `-v, --volume :[:]`: Bind mount a volume, e.g., `-v /mnt:/mnt:rro,rprivate` - :whale: option `rw` : Read/Write (when writable) - :whale: option `ro` : Non-recursive read-only - :nerd_face: option `rro`: Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too. @@ -315,29 +315,29 @@ Rootfs flags: Env flags: -- :whale: :blue_square: `--entrypoint`: Overwrite the default ENTRYPOINT of the image -- :whale: :blue_square: `-w, --workdir`: Working directory inside the container -- :whale: :blue_square: `-e, --env`: Set environment variables -- :whale: :blue_square: `--env-file`: Set environment variables from file +- :whale: `--entrypoint`: Overwrite the default ENTRYPOINT of the image +- :whale: `-w, --workdir`: Working directory inside the container +- :whale: `-e, --env`: Set environment variables +- :whale: `--env-file`: Set environment variables from file Metadata flags: -- :whale: :blue_square: `--name`: Assign a name to the container -- :whale: :blue_square: `-l, --label`: Set meta data on a container (Not passed through the OCI runtime since nerdctl v2.0, with an exception for `nerdctl/bypass4netns`) -- :whale: :blue_square: `--label-file`: Read in a line delimited file of labels -- :whale: :blue_square: `--annotation`: Add an annotation to the container (passed through to the OCI runtime) -- :whale: :blue_square: `--cidfile`: Write the container ID to the file +- :whale: `--name`: Assign a name to the container +- :whale: `-l, --label`: Set meta data on a container (Not passed through the OCI runtime since nerdctl v2.0, with an exception for `nerdctl/bypass4netns`) +- :whale: `--label-file`: Read in a line delimited file of labels +- :whale: `--annotation`: Add an annotation to the container (passed through to the OCI runtime) +- :whale: `--cidfile`: Write the container ID to the file - :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention. Health check flags: -- :whale: :blue_square: `--health-cmd`: Command to run to check container health -- :whale: :blue_square: `--health-interval`: Time between running the check (e.g., 30s, 1m) -- :whale: :blue_square: `--health-timeout`: Time to wait before considering the check failed (e.g., 5s) -- :whale: :blue_square: `--health-retries`: Number of failures before container is considered unhealthy -- :whale: :blue_square: `--health-start-period`: Start period for the container to initialize before starting health-retries countdown -- :whale: :blue_square: `--health-start-interval`: Interval between checks during the start period -- :whale: :blue_square: `--no-healthcheck`: Disable any health checks defined by image or CLI +- :whale: `--health-cmd`: Command to run to check container health +- :whale: `--health-interval`: Time between running the check (e.g., 30s, 1m) +- :whale: `--health-timeout`: Time to wait before considering the check failed (e.g., 5s) +- :whale: `--health-retries`: Number of failures before container is considered unhealthy +- :whale: `--health-start-period`: Start period for the container to initialize before starting health-retries countdown +- :whale: `--health-start-interval`: Interval between checks during the start period +- :whale: `--no-healthcheck`: Disable any health checks defined by image or CLI Logging flags: @@ -449,7 +449,7 @@ Unimplemented `docker run` flags: `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, `--isolation`, `--link*`, `--publish-all`, `--storage-opt`, `--volume-driver` -### :whale: :blue_square: nerdctl exec +### :whale: nerdctl exec Run a command in a running container. @@ -469,7 +469,7 @@ Flags: Unimplemented `docker exec` flags: `--detach-keys` -### :whale: :blue_square: nerdctl create +### :whale: nerdctl create Create a new container. @@ -498,7 +498,7 @@ Flags: Unimplemented `docker cp` flags: `--archive` -### :whale: :blue_square: nerdctl ps +### :whale: nerdctl ps List containers. @@ -542,7 +542,7 @@ Following arguments for `--filter` are not supported yet: 4. `--filter isolation=` 5. `--filter is-task=` -### :whale: :blue_square: nerdctl inspect +### :whale: nerdctl inspect Display detailed information on one or more containers. @@ -801,7 +801,7 @@ support zstdchunked convert ## Image management -### :whale: :blue_square: nerdctl images +### :whale: nerdctl images List images @@ -828,7 +828,7 @@ Flags: - :nerd_face: `--filter=reference=`: Filter images by reference (Matches both docker compatible wildcard pattern and regexp match) - :nerd_face: `--names`: Show image names -### :whale: :blue_square: nerdctl pull +### :whale: nerdctl pull Pull an image from a registry. @@ -1189,7 +1189,7 @@ Flags: - :whale: `--driver=bridge`: Default driver for unix - :whale: `--driver=macvlan`: Macvlan network driver for unix - :whale: `--driver=ipvlan`: IPvlan network driver for unix - - :whale: :blue_square: `--driver=nat`: Default driver for windows + - :whale: `--driver=nat`: Default driver for windows - :whale: `-o, --opt`: Set driver specific options - :whale: `--opt=com.docker.network.driver.mtu=`: Set the containers network MTU - :nerd_face: `--opt=mtu=`: Alias of `--opt=com.docker.network.driver.mtu=` @@ -1200,7 +1200,7 @@ Flags: - :nerd_face: `--opt=mode=(bridge|l2|l3)`: Alias of `--opt=macvlan_mode=(bridge)` and `--opt=ipvlan_mode=(l2|l3)` - :whale: `--opt=parent=`: Set valid parent interface on host - :whale: `--ipam-driver=(default|host-local|dhcp)`: IP Address Management Driver - - :whale: :blue_square: `--ipam-driver=default`: Default IPAM driver + - :whale: `--ipam-driver=default`: Default IPAM driver - :nerd_face: `--ipam-driver=host-local`: Host-local IPAM driver for unix - :nerd_face: `--ipam-driver=dhcp`: DHCP IPAM driver for unix, requires root - :whale: `--ipam-opt`: Set IPAM driver specific options @@ -1341,7 +1341,7 @@ Unimplemented `docker volume prune` flags: `--filter` ## Namespace management -### :nerd_face: :blue_square: nerdctl namespace create +### :nerd_face: nerdctl namespace create Create a new namespace. @@ -1350,13 +1350,13 @@ Flags: - `--label`: Set labels for a namespace -### :nerd_face: :blue_square: nerdctl namespace inspect +### :nerd_face: nerdctl namespace inspect Inspect a namespace. Usage: `nerdctl namespace inspect NAMESPACE` -### :nerd_face: :blue_square: nerdctl namespace ls +### :nerd_face: nerdctl namespace ls List containerd namespaces such as "default", "moby", or "k8s.io". @@ -1367,7 +1367,7 @@ Flags: - `-q, --quiet`: Only display namespace names - `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}` -### :nerd_face: :blue_square: nerdctl namespace remove +### :nerd_face: nerdctl namespace remove Remove one or more namespaces. @@ -1377,7 +1377,7 @@ Flags: - `-c, --cgroup`: delete the namespace's cgroup -### :nerd_face: :blue_square: nerdctl namespace update +### :nerd_face: nerdctl namespace update Update labels for a namespace. @@ -1908,15 +1908,15 @@ Flags: ## Global flags -- :nerd_face: :blue_square: `--address`: containerd address, optionally with "unix://" prefix -- :nerd_face: :blue_square: `-a`, `--host`, `-H`: deprecated aliases of `--address` -- :nerd_face: :blue_square: `--namespace`: containerd namespace -- :nerd_face: :blue_square: `-n`: deprecated alias of `--namespace` -- :nerd_face: :blue_square: `--snapshotter`: containerd snapshotter -- :nerd_face: :blue_square: `--storage-driver`: deprecated alias of `--snapshotter` -- :nerd_face: :blue_square: `--cni-path`: CNI binary path (default: `/opt/cni/bin`) [`$CNI_PATH`] -- :nerd_face: :blue_square: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`] -- :nerd_face: :blue_square: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl" +- :nerd_face: `--address`: containerd address, optionally with "unix://" prefix +- :nerd_face: `-a`, `--host`, `-H`: deprecated aliases of `--address` +- :nerd_face: `--namespace`: containerd namespace +- :nerd_face: `-n`: deprecated alias of `--namespace` +- :nerd_face: `--snapshotter`: containerd snapshotter +- :nerd_face: `--storage-driver`: deprecated alias of `--snapshotter` +- :nerd_face: `--cni-path`: CNI binary path (default: `/opt/cni/bin`) [`$CNI_PATH`] +- :nerd_face: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`] +- :nerd_face: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl" - :nerd_face: `--cgroup-manager=(cgroupfs|systemd|none)`: cgroup manager - Default: "systemd" on cgroup v2 (rootful & rootless), "cgroupfs" on v1 rootful, "none" on v1 rootless - :nerd_face: `--insecure-registry`: skips verifying HTTPS certs, and allows falling back to plain HTTP From 2f79aa6190b97d16bf4d12cb759038e69f69deda Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 10 Oct 2025 19:33:46 +0900 Subject: [PATCH 236/868] docs/command-reference.md: fix markdown Signed-off-by: Akihiro Suda --- docs/command-reference.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index ec9bfb530b7..21af3a569f4 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -5,9 +5,9 @@ :nerd_face: = nerdctl specific > [!NOTE] -- Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. - It does not necessarily mean that the corresponding features are missing in containerd. -- Some commands and flags are only available on Linux. +> - Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. +> It does not necessarily mean that the corresponding features are missing in containerd. +> - Some commands and flags are only available on Linux. From b90faf66001cdaa0fe34b5d294a7307d83a3eade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:01:47 +0000 Subject: [PATCH 237/868] build(deps): bump the golang-x group across 1 directory with 5 updates Bumps the golang-x group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0) Updates `golang.org/x/net` from 0.44.0 to 0.45.0 - [Commits](https://github.com/golang/net/compare/v0.44.0...v0.45.0) Updates `golang.org/x/sys` from 0.36.0 to 0.37.0 - [Commits](https://github.com/golang/sys/compare/v0.36.0...v0.37.0) Updates `golang.org/x/term` from 0.35.0 to 0.36.0 - [Commits](https://github.com/golang/term/compare/v0.35.0...v0.36.0) Updates `golang.org/x/text` from 0.29.0 to 0.30.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.29.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.45.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.30.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 5fe166f9802..bb97dd84599 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.42.0 - golang.org/x/net v0.44.0 + golang.org/x/crypto v0.43.0 + golang.org/x/net v0.45.0 golang.org/x/sync v0.17.0 //gomodjail:unconfined - golang.org/x/sys v0.36.0 //gomodjail:unconfined - golang.org/x/term v0.35.0 //gomodjail:unconfined - golang.org/x/text v0.29.0 + golang.org/x/sys v0.37.0 //gomodjail:unconfined + golang.org/x/term v0.36.0 //gomodjail:unconfined + golang.org/x/text v0.30.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) @@ -136,7 +136,7 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.27.0 // indirect + golang.org/x/mod v0.28.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect //gomodjail:unconfined google.golang.org/grpc v1.73.0 // indirect diff --git a/go.sum b/go.sum index c2c2175d539..fcfeb43bb69 100644 --- a/go.sum +++ b/go.sum @@ -362,8 +362,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -377,8 +377,8 @@ 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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 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= @@ -395,8 +395,8 @@ 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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 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= @@ -435,8 +435,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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= @@ -446,8 +446,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.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= 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= @@ -457,8 +457,8 @@ 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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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= @@ -471,8 +471,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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= From d658692c69ea5463d2a46a122acc28978cd1a650 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:02:49 +0000 Subject: [PATCH 238/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.4 to 0.15.5. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.4...v0.15.5) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5fe166f9802..43c124f9a4b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.4 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.5 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.17.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.17.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index c2c2175d539..6cc04ae89fe 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlK github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE= -github.com/containerd/nydus-snapshotter v0.15.4/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= +github.com/containerd/nydus-snapshotter v0.15.5 h1:wP17QGv33SGItGQ+CvZIqEnwIMjX26vZ4hs5wFRQm8I= +github.com/containerd/nydus-snapshotter v0.15.5/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From fd9fb6b1c1ef9b9c145f3dc8aa339afca464dd31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:01:49 +0000 Subject: [PATCH 239/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.5.0+incompatible to 28.5.1+incompatible - [Commits](https://github.com/docker/cli/compare/v28.5.0...v28.5.1) Updates `github.com/docker/docker` from 28.5.0+incompatible to 28.5.1+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bb97dd84599..262a2c8f0f3 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.5.0+incompatible //gomodjail:unconfined - github.com/docker/docker v28.5.0+incompatible //gomodjail:unconfined + github.com/docker/cli v28.5.1+incompatible //gomodjail:unconfined + github.com/docker/docker v28.5.1+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index fcfeb43bb69..193cdf832ad 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk= -github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E= -github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= +github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= From 1b37f8bacf8d79079f006dec8d3d89fca4705b27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:01:52 +0000 Subject: [PATCH 240/868] build(deps): bump lima-vm/lima-actions from 1.0.1 to 1.1.0 Bumps [lima-vm/lima-actions](https://github.com/lima-vm/lima-actions) from 1.0.1 to 1.1.0. - [Release notes](https://github.com/lima-vm/lima-actions/releases) - [Commits](https://github.com/lima-vm/lima-actions/compare/03b96d61959e83b2c737e44162c3088e81de0886...55627e31b78637bf254a8b2a14da8ea7d12564e5) --- updated-dependencies: - dependency-name: lima-vm/lima-actions dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index aa160b95fb5..a4f8322ab02 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 1 - name: "Init: lima" - uses: lima-vm/lima-actions/setup@03b96d61959e83b2c737e44162c3088e81de0886 # v1.0.1 + uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 id: lima-actions-setup - name: "Init: Cache" From 513c8b8c12b590acac873b4aad43b2ac0e9a72e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:02:19 +0000 Subject: [PATCH 241/868] build(deps): bump golang.org/x/net in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.45.0 to 0.46.0 - [Commits](https://github.com/golang/net/compare/v0.45.0...v0.46.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bb97dd84599..a914019a0b1 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.43.0 - golang.org/x/net v0.45.0 + golang.org/x/net v0.46.0 golang.org/x/sync v0.17.0 //gomodjail:unconfined golang.org/x/sys v0.37.0 //gomodjail:unconfined golang.org/x/term v0.36.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index fcfeb43bb69..c4e83a64d0b 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,8 @@ 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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 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= From e1de9e71933db7acfd721369524694cd0afb5844 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 14 Oct 2025 23:16:34 +0000 Subject: [PATCH 242/868] healthcheck: fix path issues and add default config values - Fixed PATH resolution by using explicit nerdctl binary path in systemd service files, eliminating 'nerdctl' not found errors - Added default values for unspecified healthcheck flags to prevent silent failures Signed-off-by: Arjun Raja Yogidas --- .../container_health_check_linux_test.go | 133 ++++++++++++++++++ pkg/cmd/container/create.go | 5 + pkg/healthcheck/health.go | 16 +++ pkg/healthcheck/healthcheck_manager_linux.go | 12 +- 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index 9ce502f1523..cda5cec1cb7 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -139,6 +139,139 @@ func TestContainerHealthCheckBasic(t *testing.T) { testCase.Run(t) } +func TestContainerHealthCheckDefaults(t *testing.T) { + testCase := nerdtest.Setup() + + // Docker CLI does not provide a standalone healthcheck command. + testCase.Require = require.Not(nerdtest.Docker) + + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + + testCase.SubTests = []*test.Case{ + { + Description: "Health check applies default values when not explicitly set", + Setup: func(data test.Data, helpers test.Helpers) { + // Create container with only --health-cmd, no other health flags + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + + // Parse the healthcheck config from container labels + hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] + assert.Assert(t, hcLabel != "", "expected healthcheck label to be present") + + var hc healthcheck.Healthcheck + err := json.Unmarshal([]byte(hcLabel), &hc) + assert.NilError(t, err, "failed to parse healthcheck config") + + // Verify default values are applied + assert.Equal(t, hc.Interval, 30*time.Second, "expected default interval of 30s") + assert.Equal(t, hc.Timeout, 30*time.Second, "expected default timeout of 30s") + assert.Equal(t, hc.Retries, 3, "expected default retries of 3") + assert.Equal(t, hc.StartPeriod, 0*time.Second, "expected default start period of 0s") + + // Verify the command was set correctly + assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "echo healthy"}) + }), + } + }, + }, + { + Description: "CLI flags override default values correctly", + Setup: func(data test.Data, helpers test.Helpers) { + // Create container with custom health flags that override defaults + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo custom", + "--health-interval", "45s", + "--health-timeout", "15s", + "--health-retries", "5", + "--health-start-period", "10s", + testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + + // Parse the healthcheck config from container labels + hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] + assert.Assert(t, hcLabel != "", "expected healthcheck label to be present") + + var hc healthcheck.Healthcheck + err := json.Unmarshal([]byte(hcLabel), &hc) + assert.NilError(t, err, "failed to parse healthcheck config") + + // Verify CLI overrides are applied (not defaults) + assert.Equal(t, hc.Interval, 45*time.Second, "expected custom interval of 45s") + assert.Equal(t, hc.Timeout, 15*time.Second, "expected custom timeout of 15s") + assert.Equal(t, hc.Retries, 5, "expected custom retries of 5") + assert.Equal(t, hc.StartPeriod, 10*time.Second, "expected custom start period of 10s") + + // Verify the command was set correctly + assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "echo custom"}) + }), + } + }, + }, + { + Description: "No defaults applied when no healthcheck is configured", + Setup: func(data test.Data, helpers test.Helpers) { + // Create container without any health flags + helpers.Ensure("run", "-d", "--name", data.Identifier(), + testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + + // Verify no healthcheck label is present + hcLabel := inspect.Config.Labels["nerdctl/healthcheck"] + assert.Equal(t, hcLabel, "", "expected no healthcheck label when no healthcheck is configured") + + // Verify no health state + assert.Assert(t, inspect.State.Health == nil, "expected no health state when no healthcheck is configured") + }), + } + }, + }, + } + + testCase.Run(t) +} + func TestContainerHealthCheckAdvance(t *testing.T) { testCase := nerdtest.Setup() diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 232d8a27b77..69e6919ccd3 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -892,6 +892,11 @@ func withHealthcheck(options types.ContainerCreateOptions, ensuredImage *imgutil hc.StartPeriod = options.HealthStartPeriod } + // Apply defaults for any unset values, but only if we have a healthcheck configured + if len(hc.Test) > 0 && hc.Test[0] != "NONE" { + hc.ApplyDefaults() + } + // If no healthcheck config is set (via CLI or image), return empty string so we skip adding to container config. if reflect.DeepEqual(hc, &healthcheck.Healthcheck{}) { return "", nil diff --git a/pkg/healthcheck/health.go b/pkg/healthcheck/health.go index c074c15c413..70104187e29 100644 --- a/pkg/healthcheck/health.go +++ b/pkg/healthcheck/health.go @@ -136,3 +136,19 @@ func HealthcheckResultFromJSON(s string) (*HealthcheckResult, error) { } return &r, nil } + +// ApplyDefaults sets default values for unset healthcheck fields +func (hc *Healthcheck) ApplyDefaults() { + if hc.Interval == 0 { + hc.Interval = DefaultProbeInterval + } + if hc.Timeout == 0 { + hc.Timeout = DefaultProbeTimeout + } + if hc.StartPeriod == 0 { + hc.StartPeriod = DefaultStartPeriod + } + if hc.Retries == 0 { + hc.Retries = DefaultProbeRetries + } +} diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index e043b5c2d37..4cd33b77c01 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -56,7 +56,13 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi // Always use health-interval for timer frequency cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s") - cmdOpts = append(cmdOpts, "nerdctl", "container", "healthcheck", containerID) + // Get the full path to the current nerdctl binary + nerdctlPath, err := os.Executable() + if err != nil { + return fmt.Errorf("could not determine nerdctl executable path: %v", err) + } + + cmdOpts = append(cmdOpts, nerdctlPath, "container", "healthcheck", containerID) if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { cmdOpts = append(cmdOpts, "--debug") } @@ -257,8 +263,8 @@ func shouldSkipHealthCheckSystemd(hc *Healthcheck, cfg *config.Config) bool { return true } - // Don't proceed if health check is nil, empty, explicitly NONE or interval is 0. - if hc == nil || len(hc.Test) == 0 || hc.Test[0] == "NONE" || hc.Interval == 0 { + // Don't proceed if health check is nil, empty or explicitly NONE. + if hc == nil || len(hc.Test) == 0 || hc.Test[0] == "NONE" { return true } return false From 5d99208645a229c5725328b4d2a7f186e9eb0fb1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 21 Oct 2025 19:11:35 +0900 Subject: [PATCH 243/868] CI: skip flaky tests on EL8 and ARM64 Those tests should be enabled again once the flakiness is improved. Signed-off-by: Akihiro Suda --- .github/workflows/job-test-in-container.yml | 5 +++++ .github/workflows/job-test-in-lima.yml | 5 +++++ .github/workflows/workflow-flaky.yml | 1 + .github/workflows/workflow-test.yml | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index be742f8ab00..902f8de5a5b 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -35,6 +35,10 @@ on: required: false default: false type: boolean + skip-flaky: + required: false + default: false + type: boolean env: GOTOOLCHAIN: local @@ -171,6 +175,7 @@ jobs: fi # FIXME: this NEEDS to go away - name: "Run: integration tests (flaky)" + if: ${{ !fromJSON(inputs.skip-flaky) }} run: | . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index a4f8322ab02..7d0c2f922ea 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -16,6 +16,10 @@ on: guest: required: true type: string + skip-flaky: + required: false + default: false + type: boolean jobs: test: @@ -114,6 +118,7 @@ jobs: docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false fi - name: "Run: integration tests (flaky)" + if: ${{ !fromJSON(inputs.skip-flaky) }} run: | set -eux if [ "$TARGET" = "rootless" ]; then diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 9165f372813..a17e19c4c4c 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -29,6 +29,7 @@ jobs: runner: ubuntu-24.04 guest: ${{ matrix.guest }} target: ${{ matrix.target }} + skip-flaky: true # skip the most flaky ones for now test-integration-freebsd: name: "FreeBSD" diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 19ed223761c..54b86450877 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -69,9 +69,11 @@ jobs: # arm64 - runner: ubuntu-24.04-arm target: rootless + skip-flaky: true # port-slirp4netns - runner: ubuntu-24.04 target: rootless-port-slirp4netns + skip-flaky: true # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless @@ -88,6 +90,7 @@ jobs: # arm64 - runner: ubuntu-24.04-arm target: rootful + skip-flaky: true # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful @@ -96,6 +99,7 @@ jobs: - runner: ubuntu-24.04 target: rootful ipv6: true + skip-flaky: true # all canary - runner: ubuntu-24.04 target: rootful @@ -110,6 +114,7 @@ jobs: rootlesskit-version: ${{ matrix.rootlesskit-version }} ipv6: ${{ matrix.ipv6 && true || false }} canary: ${{ matrix.canary && true || false }} + skip-flaky: ${{ matrix.skip-flaky && true || false }} test-integration-host: name: "in-host${{ inputs.hack }}" From 955ed62bfb390de9d5f8f1ab10be34630c90085c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:21:39 +0000 Subject: [PATCH 244/868] build(deps): bump tonistiigi/xx from 1.7.0 to 1.8.0 Bumps tonistiigi/xx from 1.7.0 to 1.8.0. --- updated-dependencies: - dependency-name: tonistiigi/xx dependency-version: 1.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4443b3ffba6..15868503c24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ ARG NYDUS_VERSION=v2.3.5 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.7.0@sha256:010d4b66aed389848b0694f91c7aaee9df59a6f20be7f5d12e53663a37bd14e2 AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0@sha256:add602d55daca18914838a78221f6bbe4284114b452c86a48f96d59aeb00f5c6 AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base From 77cd0209c8f7412aa2497999b0b3c25c5b1d0d91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:22:13 +0000 Subject: [PATCH 245/868] build(deps): bump github.com/klauspost/compress from 1.18.0 to 1.18.1 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.0 to 1.18.1. - [Release notes](https://github.com/klauspost/compress/releases) - [Changelog](https://github.com/klauspost/compress/blob/master/.goreleaser.yml) - [Commits](https://github.com/klauspost/compress/compare/v1.18.0...v1.18.1) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f8460605000..3aeb619a754 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.4.0 github.com/ipfs/go-cid v0.5.0 - github.com/klauspost/compress v1.18.0 + github.com/klauspost/compress v1.18.1 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 diff --git a/go.sum b/go.sum index 185519aedea..4813ef932e8 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From e584314dd262de8828f1c42a261ce66d56e0ed3c Mon Sep 17 00:00:00 2001 From: clarehkli Date: Sat, 18 Oct 2025 12:11:44 +0800 Subject: [PATCH 246/868] bump github.com/containerd/stargz-snapshotter from v0.17.0 to v0.18.0 Signed-off-by: clarehkli --- go.mod | 27 +++++++++++----------- go.sum | 73 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index f8460605000..253559d18f4 100644 --- a/go.mod +++ b/go.mod @@ -22,9 +22,9 @@ require ( github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.5 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter v0.17.0 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/estargz v0.17.0 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/ipfs v0.17.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter v0.18.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/estargz v0.18.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/ipfs v0.18.0 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined @@ -85,7 +85,7 @@ require ( github.com/djherbis/times v1.6.0 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -108,7 +108,7 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.16.0 // indirect + github.com/multiformats/go-multiaddr v0.16.1 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect @@ -127,24 +127,25 @@ require ( //gomodjail:unconfined github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.3.0 // indirect - github.com/vbatts/tar-split v0.12.1 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined - google.golang.org/grpc v1.73.0 // indirect + google.golang.org/grpc v1.76.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 185519aedea..2bac67cbcfc 100644 --- a/go.sum +++ b/go.sum @@ -53,12 +53,12 @@ github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsW github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc= -github.com/containerd/stargz-snapshotter v0.17.0/go.mod h1:ySEul1ck7jCE4jqsuFCo8FFLrHU20UWQeI9g7mdsanI= -github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE= -github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM= -github.com/containerd/stargz-snapshotter/ipfs v0.17.0 h1:Q7UO2U0nKXtQFYVeX8WUmMKXtJR9ZAPgISt/sMEo8Ng= -github.com/containerd/stargz-snapshotter/ipfs v0.17.0/go.mod h1:zRJECfc6IPSr50ljYX36kVmrSd1Wdi3aXLzZhFuhfR4= +github.com/containerd/stargz-snapshotter v0.18.0 h1:C7mqAnH5v+ZE9FK+ZFt8qsb9uHfuRJXlpqQAQpq8PDc= +github.com/containerd/stargz-snapshotter v0.18.0/go.mod h1:BnVpVqp79HpVPtOOiK/O/2HINEoCf/Gz9vzXrtRArnE= +github.com/containerd/stargz-snapshotter/estargz v0.18.0 h1:Ny5yptQgEXSkDFKvlKJGTvf1YJ+4xD8V+hXqoRG0n74= +github.com/containerd/stargz-snapshotter/estargz v0.18.0/go.mod h1:7hfU1BO2KB3axZl0dRQCdnHrIWw7TRDdK6L44Rdeuo0= +github.com/containerd/stargz-snapshotter/ipfs v0.18.0 h1:yDIKLwldoQFS9wpZHaGdqNZCwIkTgsUuV5pNAX9JC9M= +github.com/containerd/stargz-snapshotter/ipfs v0.18.0/go.mod h1:aVNaKOoeNgAKEMphB6YeAowWnVG+7Fa3vvFHltM1zGE= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -112,8 +112,8 @@ github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOe github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -151,7 +151,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -230,8 +229,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= -github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= @@ -266,8 +265,8 @@ 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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= @@ -303,15 +302,15 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= -github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -336,22 +335,24 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 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= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -477,20 +478,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/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-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 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= @@ -500,8 +503,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -516,8 +519,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= tags.cncf.io/container-device-interface/specs-go v1.0.0 h1:8gLw29hH1ZQP9K1YtAzpvkHCjjyIxHZYzBAvlQ+0vD8= From 35cc34f8d443c1dd89eef8f83df1a19f2acbbaa5 Mon Sep 17 00:00:00 2001 From: clarehkli Date: Sat, 18 Oct 2025 12:17:01 +0800 Subject: [PATCH 247/868] add support for the new --estargz-gzip-helper option in stargz-snapshotter add support for the newly added image conversion --estargz-gzip-helper option in stargz-snapshotter Signed-off-by: clarehkli --- cmd/nerdctl/image/image_convert.go | 6 ++++++ docs/stargz.md | 17 +++++++++++++++-- pkg/api/types/image_types.go | 2 ++ pkg/cmd/image/convert.go | 8 ++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 48a8bed42f9..ebe86119e31 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -61,6 +61,7 @@ func convertCommand() *cobra.Command { cmd.Flags().Int("estargz-min-chunk-size", 0, "The minimal number of bytes of data must be written in one gzip stream. (requires stargz-snapshotter >= v0.13.0)") cmd.Flags().Bool("estargz-external-toc", false, "Separate TOC JSON into another image (called \"TOC image\"). The name of TOC image is the original + \"-esgztoc\" suffix. Both eStargz and the TOC image should be pushed to the same registry. (requires stargz-snapshotter >= v0.13.0) (EXPERIMENTAL)") cmd.Flags().Bool("estargz-keep-diff-id", false, "Convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc')") + cmd.Flags().String("estargz-gzip-helper", "", "Helper command for decompressing layers compressed with gzip. Options: pigz, igzip, or gzip.") // #endregion // #region zstd flags @@ -149,6 +150,10 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { if err != nil { return types.ImageConvertOptions{}, err } + estargzGzipHelper, err := cmd.Flags().GetString("estargz-gzip-helper") + if err != nil { + return types.ImageConvertOptions{}, err + } // #endregion // #region zstd flags @@ -275,6 +280,7 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { EstargzMinChunkSize: estargzMinChunkSize, EstargzExternalToc: estargzExternalTOC, EstargzKeepDiffID: estargzKeepDiffID, + EstargzGzipHelper: estargzGzipHelper, }, ZstdOptions: types.ZstdOptions{ Zstd: zstd, diff --git a/docs/stargz.md b/docs/stargz.md index 5a54fe17906..57cd22f303e 100644 --- a/docs/stargz.md +++ b/docs/stargz.md @@ -92,7 +92,20 @@ Stargz Snapshotter is not needed for building stargz images. ## Tips for image conversion -### Tips 1: Creating smaller eStargz images +### Tips 1: Using gzip helper to speed up image conversion + +When converting a traditional overlayfs image encoded as tar.gz to an estargz format image, nerdctl supports specifying an additional command‑line decompression tool to speed up the conversion process. You can set `--estargz-gzip-helper` to choose different CLI gzip tools. Even using the gzip command corresponding to the Go gzip library can achieve approximately 32% speed improvement. For more details, see: [Using decompression commands to improve the layer decompression speed of gzip-formatted images](https://github.com/containerd/stargz-snapshotter/pull/2117). Currently, `--estargz-gzip-helper` supports `pigz`, `igzip`, and `gzip`. The recommended order is `pigz` > `igzip` > `gzip`. + +```console +# nerdctl image convert --oci --estargz --estargz-gzip-helper pigz ghcr.io/stargz-containers/ubuntu:22.04 ghcr.io/stargz-containers/ubuntu:22.04-esgz +sha256:aa6543b9885867b8b485925b6ec69d8e018e8fce40835ea6359cbb573683a014 +# nerdctl image ls +REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE +ghcr.io/stargz-containers/ubuntu 22.04-esgz aa6543b98858 About a minute ago linux/amd64 0B 32.43MB +ghcr.io/stargz-containers/ubuntu 22.04 20fa2d7bb4de 2 minutes ago linux/amd64 87.47MB 30.43MB +``` + +### Tips 2: Creating smaller eStargz images `nerdctl image convert` allows the following flags for optionally creating a smaller eStargz image. The result image requires stargz-snapshotter >= v0.13.0 for lazy pulling. @@ -167,7 +180,7 @@ sha256:7f5cbd8cc787c8d628630756bcc7240e6c96b876c2882e6fc980a8b60cdfa274 sha256:7f5cbd8cc787c8d628630756bcc7240e6c96b876c2882e6fc980a8b60cdfa274 ``` -### Tips 2: Using zstd instead of gzip (a.k.a. zstd:chunked) +### Tips 3: Using zstd instead of gzip (a.k.a. zstd:chunked) You can use zstd compression with lazy pulling support (a.k.a zstd:chunked) instead of gzip. diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 0ceb3148896..8999d659213 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -92,6 +92,8 @@ type EstargzOptions struct { EstargzExternalToc bool // EstargzKeepDiffID convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc') EstargzKeepDiffID bool + // EstargzGzipHelper helper command for decompressing layers compressed with gzip. Options: pigz, igzip, or gzip + EstargzGzipHelper string } // ZstdOptions contains zstd conversion options diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index c197755b3a5..df022b90011 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -41,6 +41,7 @@ import ( estargzexternaltocconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz/externaltoc" zstdchunkedconvert "github.com/containerd/stargz-snapshotter/nativeconverter/zstdchunked" "github.com/containerd/stargz-snapshotter/recorder" + estargzdecompressutil "github.com/containerd/stargz-snapshotter/util/decompressutil" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -285,6 +286,13 @@ func getESGZConvertOpts(options types.ImageConvertOptions) ([]estargz.Option, er var ignored []string esgzOpts = append(esgzOpts, estargz.WithAllowPrioritizeNotFound(&ignored)) } + if options.EstargzGzipHelper != "" { + gzipHelperFunc, err := estargzdecompressutil.GetGzipHelperFunc(options.EstargzGzipHelper) + if err != nil { + return nil, err + } + esgzOpts = append(esgzOpts, estargz.WithGzipHelperFunc(gzipHelperFunc)) + } return esgzOpts, nil } From bdfc7b0f6d0f35eccf8577ed769858201e14fee8 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 21 Oct 2025 22:00:20 +0900 Subject: [PATCH 248/868] CI: mark TestPush flaky See issue 4470 Signed-off-by: Akihiro Suda --- cmd/nerdctl/image/image_push_linux_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index dee14a74660..c547341d012 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -43,6 +43,7 @@ func TestPush(t *testing.T) { Require: require.All( require.Linux, nerdtest.Registry, + nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4470"), ), Setup: func(data test.Data, helpers test.Helpers) { From c8ddcd87dac5e29eb85e6c4433fd374392a18969 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 26 Aug 2025 14:51:09 +0800 Subject: [PATCH 249/868] checkpoint: support nerdctl checkpoint create command - Create checkpoints from running containers using containerd APIs - Support both leave-running and exit modes via --leave-running flag - Configurable checkpoint directory via --checkpoint-dir flag Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/checkpoint/checkpoint.go | 40 ++++++ cmd/nerdctl/checkpoint/checkpoint_create.go | 93 ++++++++++++++ cmd/nerdctl/main.go | 4 + pkg/api/types/checkpoint_types.go | 29 +++++ pkg/checkpointutil/checkpointutil.go | 48 +++++++ pkg/cmd/checkpoint/create.go | 135 ++++++++++++++++++++ 6 files changed, 349 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint.go create mode 100644 cmd/nerdctl/checkpoint/checkpoint_create.go create mode 100644 pkg/api/types/checkpoint_types.go create mode 100644 pkg/checkpointutil/checkpointutil.go create mode 100644 pkg/cmd/checkpoint/create.go diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go new file mode 100644 index 00000000000..10a8c00108f --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint.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 checkpoint + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Annotations: map[string]string{helpers.Category: helpers.Management}, + Use: "checkpoint", + Short: "Manage checkpoints.", + RunE: helpers.UnknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + + cmd.AddCommand( + CreateCommand(), + ) + + return cmd +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_create.go b/cmd/nerdctl/checkpoint/checkpoint_create.go new file mode 100644 index 00000000000..540acd44f26 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_create.go @@ -0,0 +1,93 @@ +/* + 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 checkpoint + +import ( + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" +) + +func CreateCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "create [OPTIONS] CONTAINER CHECKPOINT", + Short: "Create a checkpoint from a running container", + Args: cobra.ExactArgs(2), + RunE: createAction, + ValidArgsFunction: createShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().Bool("leave-running", false, "Leave the container running after checkpointing") + cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") + return cmd +} + +func processCreateFlags(cmd *cobra.Command) (types.CheckpointCreateOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.CheckpointCreateOptions{}, err + } + + leaveRunning, err := cmd.Flags().GetBool("leave-running") + if err != nil { + return types.CheckpointCreateOptions{}, err + } + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.CheckpointCreateOptions{}, err + } + if checkpointDir == "" { + checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints") + } + + return types.CheckpointCreateOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + LeaveRunning: leaveRunning, + CheckpointDir: checkpointDir, + }, nil +} + +func createAction(cmd *cobra.Command, args []string) error { + createOptions, err := processCreateFlags(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), createOptions.GOptions.Namespace, createOptions.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + err = checkpoint.Create(ctx, client, args[0], args[1], createOptions) + if err != nil { + return err + } + + return nil +} + +func createShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index c5abcc60a6c..51dfb26736e 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/checkpoint" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/compose" "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" @@ -350,6 +351,9 @@ Config file ($NERDCTL_TOML): %s // Manifest manifest.Command(), + + // Checkpoint + checkpoint.Command(), ) addApparmorCommand(rootCmd) container.AddCpCommand(rootCmd) diff --git a/pkg/api/types/checkpoint_types.go b/pkg/api/types/checkpoint_types.go new file mode 100644 index 00000000000..46b055105c4 --- /dev/null +++ b/pkg/api/types/checkpoint_types.go @@ -0,0 +1,29 @@ +/* + 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 types + +import "io" + +// CheckpointCreateOptions specifies options for `nerdctl checkpoint create`. +type CheckpointCreateOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Leave the container running after checkpointing + LeaveRunning bool + // Checkpoint directory + CheckpointDir string +} diff --git a/pkg/checkpointutil/checkpointutil.go b/pkg/checkpointutil/checkpointutil.go new file mode 100644 index 00000000000..c3f789af737 --- /dev/null +++ b/pkg/checkpointutil/checkpointutil.go @@ -0,0 +1,48 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpointutil + +import ( + "fmt" + "os" + "path/filepath" +) + +func GetCheckpointDir(checkpointDir, checkpointID, containerID string, create bool) (string, error) { + checkpointAbsDir := filepath.Join(checkpointDir, checkpointID) + stat, err := os.Stat(checkpointAbsDir) + if create { + switch { + case err == nil && stat.IsDir(): + err = fmt.Errorf("checkpoint with name %s already exists for container %s", checkpointID, containerID) + case err != nil && os.IsNotExist(err): + err = os.MkdirAll(checkpointAbsDir, 0o700) + case err != nil: + err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + } + } else { + switch { + case err != nil: + err = fmt.Errorf("checkpoint %s does not exist for container %s", checkpointID, containerID) + case stat.IsDir(): + err = nil + default: + err = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + } + } + return checkpointAbsDir, err +} diff --git a/pkg/cmd/checkpoint/create.go b/pkg/cmd/checkpoint/create.go new file mode 100644 index 00000000000..f9524ae7ec7 --- /dev/null +++ b/pkg/cmd/checkpoint/create.go @@ -0,0 +1,135 @@ +/* + 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 checkpoint + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/api/types/runc/options" + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/pkg/archive" + "github.com/containerd/containerd/v2/plugins" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +func Create(ctx context.Context, client *containerd.Client, containerID string, checkpointName string, options types.CheckpointCreateOptions) error { + var container containerd.Container + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req) + } + container = found.Container + return nil + }, + } + + n, err := walker.Walk(ctx, containerID) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("error creating checkpoint for container: %s, no such container", containerID) + } + + info, err := container.Info(ctx) + if err != nil { + return fmt.Errorf("failed to get info for container %q: %w", containerID, err) + } + + task, err := container.Task(ctx, nil) + if err != nil { + return fmt.Errorf("failed to get task for container %q: %w", containerID, err) + } + + img, err := task.Checkpoint(ctx, withCheckpointOpts(info.Runtime.Name, !options.LeaveRunning)) + if err != nil { + return err + } + + defer client.ImageService().Delete(ctx, img.Name()) + + cs := client.ContentStore() + + rawIndex, err := content.ReadBlob(ctx, cs, img.Target()) + if err != nil { + return fmt.Errorf("failed to retrieve checkpoint data: %w", err) + } + + var index ocispec.Index + if err := json.Unmarshal(rawIndex, &index); err != nil { + return fmt.Errorf("failed to decode checkpoint data: %w", err) + } + + var cpDesc *ocispec.Descriptor + for _, m := range index.Manifests { + if m.MediaType == images.MediaTypeContainerd1Checkpoint { + cpDesc = &m //nolint:gosec + break + } + } + if cpDesc == nil { + return errors.New("invalid checkpoint") + } + + targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), true) + if err != nil { + return err + } + + rat, err := cs.ReaderAt(ctx, *cpDesc) + if err != nil { + return fmt.Errorf("failed to get checkpoint reader: %w", err) + } + defer rat.Close() + + _, err = archive.Apply(ctx, targetPath, content.NewReader(rat)) + if err != nil { + return fmt.Errorf("failed to read checkpoint reader: %w", err) + } + + fmt.Fprintf(options.Stdout, "%s\n", checkpointName) + + return nil +} + +func withCheckpointOpts(rt string, exit bool) containerd.CheckpointTaskOpts { + return func(r *containerd.CheckpointTaskInfo) error { + + switch rt { + case plugins.RuntimeRuncV2: + if r.Options == nil { + r.Options = &options.CheckpointOptions{} + } + opts, _ := r.Options.(*options.CheckpointOptions) + + opts.Exit = exit + } + return nil + } +} From 8f8eaf010d246a4a33b958a281f141ea1b64ef3a Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 13 Sep 2025 20:08:01 +0800 Subject: [PATCH 250/868] checkpoint: add unit tests for checkpoint create command add unit tests for checkpoint create command. Signed-off-by: ChengyuZhu6 --- .../checkpoint_create_linux_test.go | 115 ++++++++++++++++++ cmd/nerdctl/checkpoint/checkpoint_test.go | 27 ++++ 2 files changed, 142 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go create mode 100644 cmd/nerdctl/checkpoint/checkpoint_test.go diff --git a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go new file mode 100644 index 00000000000..7548acdd1de --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go @@ -0,0 +1,115 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestCheckpointCreateErrors(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("checkpoint", "create", "too-few-arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "too-many-arguments", + Command: test.Command("checkpoint", "create", "too", "many", "arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "invalid-container-id", + Command: test.Command("checkpoint", "create", "foo", "bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("error creating checkpoint for container: foo")}, + } + }, + }, + } + + testCase.Run(t) +} + +func TestCheckpointCreate(t *testing.T) { + const ( + checkpointName = "checkpoint-bar" + checkpointDir = "/dir/foo" + ) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) + testCase.SubTests = []*test.Case{ + { + Description: "leave-running=true", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-running"), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container-running")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "create", "--leave-running", "--checkpoint-dir", checkpointDir, data.Identifier("container-running"), checkpointName+"running") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "running\n"), + } + }, + }, + { + Description: "leave-running=false", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-exit"), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container-exit")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-exit"), checkpointName+"exit") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(checkpointName + "exit\n"), + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_test.go b/cmd/nerdctl/checkpoint/checkpoint_test.go new file mode 100644 index 00000000000..e32a997e219 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_test.go @@ -0,0 +1,27 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +func TestMain(m *testing.M) { + testutil.M(m) +} From 9c36d7b05ac481cb7fcc131d3bc264f33425451c Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 12:03:18 +0800 Subject: [PATCH 251/868] docs: add checkpoint create command reference add checkpoint create command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index 21af3a569f4..2aaf792c75b 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -53,6 +53,8 @@ - [:nerd_face: nerdctl image convert](#nerd_face-nerdctl-image-convert) - [:nerd_face: nerdctl image encrypt](#nerd_face-nerdctl-image-encrypt) - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) +- [Checkpoint management](#checkpoint-management) + - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) @@ -1060,6 +1062,18 @@ Flags: - `--platform=` : Convert content for a specific platform - `--all-platforms` : Convert content for all platforms (default: false) +## Checkpoint management + +### :whale: nerdctl checkpoint create + +Create a checkpoint from a running container. + +Usage: `nerdctl checkpoint create [OPTIONS] CONTAINER CHECKPOINT` + +Flags: +- :whale: `--leave-running`: Leave the container running after checkpoint +- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory + ## Manifest management ### :whale: nerdctl manifest annotate From 2c4729c06b969f4156cd6fc0f5943fdc6a7ab8e4 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 9 Oct 2025 16:52:59 +0800 Subject: [PATCH 252/868] container: add checkpoint restore support to container start add checkpoint restore support to container start. e.g.: $ nerdctl run --name cr -d busybox sleep infinity $ nerdctl checkpoint create cr checkpoint1 $ nerdctl start --checkpoint checkpoint cr Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/compose/compose_start.go | 2 +- cmd/nerdctl/container/container_run.go | 2 +- cmd/nerdctl/container/container_start.go | 22 +++++++-- pkg/api/types/container_types.go | 4 ++ pkg/cmd/checkpoint/create.go | 4 ++ pkg/cmd/container/restart.go | 2 +- pkg/cmd/container/start.go | 17 ++++++- pkg/containerutil/containerutil.go | 4 +- pkg/taskutil/taskutil.go | 63 ++++++++++++++++++++++-- 9 files changed, 106 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/compose/compose_start.go b/cmd/nerdctl/compose/compose_start.go index 88e4cc905de..0d34f04919f 100644 --- a/cmd/nerdctl/compose/compose_start.go +++ b/cmd/nerdctl/compose/compose_start.go @@ -114,7 +114,7 @@ func startContainers(ctx context.Context, client *containerd.Client, containers } // in compose, always disable attach - if err := containerutil.Start(ctx, c, false, false, client, "", (*config.Config)(globalOptions)); err != nil { + if err := containerutil.Start(ctx, c, false, false, client, "", "", (*config.Config)(globalOptions)); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 9b44feb19c8..0537f2b49d2 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -432,7 +432,7 @@ func runAction(cmd *cobra.Command, args []string) error { logURI := lab[labels.LogURI] detachC := make(chan struct{}) task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach, - con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC) + con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC, "") if err != nil { return err } diff --git a/cmd/nerdctl/container/container_start.go b/cmd/nerdctl/container/container_start.go index 7b770d9b1e6..a1fddadb09c 100644 --- a/cmd/nerdctl/container/container_start.go +++ b/cmd/nerdctl/container/container_start.go @@ -44,6 +44,8 @@ func StartCommand() *cobra.Command { cmd.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals") cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") cmd.Flags().BoolP("interactive", "i", false, "Attach container's STDIN") + cmd.Flags().String("checkpoint", "", "checkpoint name") + cmd.Flags().String("checkpoint-dir", "", "checkpoint directory") return cmd } @@ -64,12 +66,22 @@ func startOptions(cmd *cobra.Command) (types.ContainerStartOptions, error) { if err != nil { return types.ContainerStartOptions{}, err } + checkpoint, err := cmd.Flags().GetString("checkpoint") + if err != nil { + return types.ContainerStartOptions{}, err + } + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.ContainerStartOptions{}, err + } return types.ContainerStartOptions{ - Stdout: cmd.OutOrStdout(), - GOptions: globalOptions, - Attach: attach, - DetachKeys: detachKeys, - Interactive: interactive, + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Attach: attach, + DetachKeys: detachKeys, + Interactive: interactive, + Checkpoint: checkpoint, + CheckpointDir: checkpointDir, }, nil } diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index a19fb5aea1f..20462661085 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -32,6 +32,10 @@ type ContainerStartOptions struct { DetachKeys string // Attach stdin Interactive bool + // Checkpoint is the name of the checkpoint to restore + Checkpoint string + // CheckpointDir is the directory to store checkpoints + CheckpointDir string } // ContainerKillOptions specifies options for `nerdctl (container) kill`. diff --git a/pkg/cmd/checkpoint/create.go b/pkg/cmd/checkpoint/create.go index f9524ae7ec7..31cd0c8fa31 100644 --- a/pkg/cmd/checkpoint/create.go +++ b/pkg/cmd/checkpoint/create.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "path/filepath" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -97,6 +98,9 @@ func Create(ctx context.Context, client *containerd.Client, containerID string, return errors.New("invalid checkpoint") } + if options.CheckpointDir == "" { + options.CheckpointDir = filepath.Join(options.GOptions.DataRoot, "checkpoints") + } targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), true) if err != nil { return err diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go index bf2b335d390..98c543bc67e 100644 --- a/pkg/cmd/container/restart.go +++ b/pkg/cmd/container/restart.go @@ -48,7 +48,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string if err := containerutil.Stop(ctx, found.Container, options.Timeout, options.Signal); err != nil { return err } - if err := containerutil.Start(ctx, found.Container, false, false, client, "", (*config.Config)(&options.GOption)); err != nil { + if err := containerutil.Start(ctx, found.Container, false, false, client, "", "", (*config.Config)(&options.GOption)); err != nil { return err } _, err = fmt.Fprintln(options.Stdout, found.Req) diff --git a/pkg/cmd/container/start.go b/pkg/cmd/container/start.go index 14663b81b9d..604ce9465f5 100644 --- a/pkg/cmd/container/start.go +++ b/pkg/cmd/container/start.go @@ -19,10 +19,12 @@ package container import ( "context" "fmt" + "path/filepath" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" "github.com/containerd/nerdctl/v2/pkg/config" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" @@ -33,15 +35,28 @@ func Start(ctx context.Context, client *containerd.Client, reqs []string, option if options.Attach && len(reqs) > 1 { return fmt.Errorf("you cannot start and attach multiple containers at once") } + if options.Checkpoint != "" && len(reqs) > 1 { + return fmt.Errorf("you cannot start multiple containers with checkpoint at once") + } walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { var err error + var checkpointDir string if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, (*config.Config)(&options.GOptions)); err != nil { + if options.Checkpoint != "" { + if options.CheckpointDir == "" { + options.CheckpointDir = filepath.Join(options.GOptions.DataRoot, "checkpoints") + } + checkpointDir, err = checkpointutil.GetCheckpointDir(options.CheckpointDir, options.Checkpoint, found.Container.ID(), false) + if err != nil { + return err + } + } + if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, checkpointDir, (*config.Config)(&options.GOptions)); err != nil { return err } if !options.Attach { diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 7805ad10b92..57833e0e3dc 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -206,7 +206,7 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) } // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. -func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, cfg *config.Config) (err error) { +func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, checkpointDir string, cfg *config.Config) (err error) { // defer the storage of start error in the dedicated label defer func() { if err != nil { @@ -280,7 +280,7 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i // source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start attachStreamOpt = []string{"STDOUT", "STDERR"} } - task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC) + task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC, checkpointDir) if err != nil { return err } diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index 67962ac9065..d6d3af8f3f2 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -19,6 +19,7 @@ package taskutil import ( "context" "errors" + "fmt" "io" "net/url" "os" @@ -27,13 +28,20 @@ import ( "strings" "sync" "syscall" + "time" "github.com/Masterminds/semver/v3" + "github.com/opencontainers/go-digest" "golang.org/x/term" "github.com/containerd/console" + "github.com/containerd/containerd/api/types" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/pkg/archive" "github.com/containerd/containerd/v2/pkg/cio" + "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/cioutil" @@ -43,9 +51,50 @@ import ( // NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108 func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, - attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) { + attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}, checkpointDir string) (containerd.Task, error) { + var ( + checkpoint *types.Descriptor + t containerd.Task + err error + ) - var t containerd.Task + if checkpointDir != "" { + tar := archive.Diff(ctx, "", checkpointDir) + cs := client.ContentStore() + writer, err := cs.Writer(ctx, content.WithRef(checkpointDir)) + if err != nil { + return nil, err + } + defer writer.Close() + size, err := io.Copy(writer, tar) + if err != nil { + return nil, err + } + labels := map[string]string{ + "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), + } + if err = writer.Commit(ctx, size, "", content.WithLabels(labels)); err != nil { + if !errors.Is(err, errdefs.ErrAlreadyExists) { + return nil, err + } + } + checkpoint = &types.Descriptor{ + MediaType: images.MediaTypeContainerd1Checkpoint, + Digest: writer.Digest().String(), + Size: size, + } + defer func() { + if checkpoint != nil { + _ = cs.Delete(ctx, digest.Digest(checkpoint.Digest)) + } + }() + if err = tar.Close(); err != nil { + return nil, fmt.Errorf("failed to close checkpoint tar stream: %w", err) + } + if err != nil { + return nil, fmt.Errorf("failed to upload checkpoint to containerd: %w", err) + } + } closer := func() { if detachC != nil { detachC <- struct{}{} @@ -158,7 +207,15 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } ioCreator = cioutil.NewContainerIO(namespace, logURI, false, in, os.Stdout, os.Stderr) } - t, err := container.NewTask(ctx, ioCreator) + + taskOpts := []containerd.NewTaskOpts{ + func(_ context.Context, _ *containerd.Client, info *containerd.TaskInfo) error { + info.Checkpoint = checkpoint + return nil + }, + } + + t, err = container.NewTask(ctx, ioCreator, taskOpts...) if err != nil { return nil, err } From 0e2bd476224eef7eb63bc1e5b31f4b304098c0a3 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 11 Oct 2025 23:05:14 +0800 Subject: [PATCH 253/868] container: add unit test for container start with checkpoint add unit test for container start with checkpoint. Signed-off-by: ChengyuZhu6 --- .../container/container_start_linux_test.go | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index b8b82c2d83d..eb6d849e58b 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -20,12 +20,15 @@ import ( "bytes" "errors" "io" + "strconv" "strings" "testing" + "time" "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/mod/tigron/tig" @@ -77,3 +80,50 @@ func TestStartDetachKeys(t *testing.T) { testCase.Run(t) } + +func TestStartWithCheckpoint(t *testing.T) { + + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Use an in-memory tmpfs to model in-memory state without introducing extra processes + // Single PID 1 shell: continuously increment a counter and write to /state/counter (tmpfs) + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--tmpfs", "/state", testutil.CommonImage, + "sh", "-c", `i=0; while true; do i=$((i+1)); printf "%d\n" "$i" >/state/counter; sleep 0.2; done`) + // Give some time for the counter to increase before checkpoint to validate continuity after restore + time.Sleep(1 * time.Second) + helpers.Ensure("checkpoint", "create", data.Identifier(), data.Identifier()+"-checkpoint") + } + + 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("start", "--checkpoint", data.Identifier()+"-checkpoint", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(_ string, t tig.T) { + // Validate in-memory state continuity via tmpfs: counter should not reset and must keep increasing + // Short delay to allow the container to resume; if the counter had reset to 0, it could not reach >5 this fast + time.Sleep(200 * time.Millisecond) + c1Str := strings.TrimSpace(helpers.Capture("exec", data.Identifier(), "cat", "/state/counter")) + var parseErrs []error + c1, err1 := strconv.Atoi(c1Str) + if err1 != nil { + parseErrs = append(parseErrs, err1) + } + assert.Assert(t, len(parseErrs) == 0, "failed to parse counter values: %v", parseErrs) + assert.Assert(t, c1 > 5, "tmpfs in-memory counter seems reset or too small: %d", c1) + }, + ), + } + } + + testCase.Run(t) +} From 4560704f87ad484a00cef4de8def16b5b0f8e663 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 11 Oct 2025 23:07:45 +0800 Subject: [PATCH 254/868] docs: add nerdctl start with checkpoint command reference add nerdctl start with checkpoint command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 2aaf792c75b..7fba7931258 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -615,8 +615,10 @@ Flags: - :whale: `-a, --attach`: Attach STDOUT/STDERR and forward signals - :whale: `--detach-keys`: Override the default detach keys +- :whale: `--checkpoint`: checkpoint name +- :whale: `--detach-keys`: checkpoint directory -Unimplemented `docker start` flags: `--checkpoint`, `--checkpoint-dir`, `--interactive` +Unimplemented `docker start` flags: `--interactive` ### :whale: nerdctl restart From d12925ed91091080c93064128df9efecc0e9c042 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 17:25:52 +0800 Subject: [PATCH 255/868] ci: install criu dependency install criu in ci to test checkpoint. Signed-off-by: ChengyuZhu6 --- .github/workflows/job-test-in-container.yml | 12 +++++++++++- .github/workflows/job-test-in-host.yml | 8 +++++--- .github/workflows/job-test-unit.yml | 7 +++++-- Dockerfile | 15 +++++++++++---- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 902f8de5a5b..a74ba80137e 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -153,7 +153,17 @@ jobs: sudo sysctl -w net.ipv4.ip_forward=1 # Enable IPv6 for Docker, and configure docker to use containerd for gha sudo mkdir -p /etc/docker - echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json + echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "ip6tables": true}' | sudo tee /etc/docker/daemon.json + - name: "Init: enable Docker experimental features" + run: | + sudo mkdir -p /etc/docker + if [ -f /etc/docker/daemon.json ]; then + tmpfile="$(sudo mktemp)" + sudo jq '.experimental = true' /etc/docker/daemon.json | sudo tee "$tmpfile" >/dev/null + sudo mv "$tmpfile" /etc/docker/daemon.json + else + echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json >/dev/null + fi sudo systemctl restart docker - name: "Run: integration tests" run: | diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 8e3b11bdf13..40e2dc02a66 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -107,9 +107,9 @@ jobs: name: "Init (linux): prepare host" run: | if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then - echo "::group:: configure cdi for docker" + echo "::group:: configure cdi and experimental for docker" sudo mkdir -p /etc/docker - sudo jq '.features.cdi = true' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp && sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json + sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json echo "::endgroup::" echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" sudo apt-get update -qq @@ -122,6 +122,7 @@ jobs: | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} + sudo systemctl restart docker echo "::endgroup::" else # FIXME: this is missing runc (see top level workflow note about the state of this) @@ -153,7 +154,8 @@ jobs: # FIXME: remove expect when we are done removing unbuffer from tests echo "::group:: installing test dependencies" - sudo apt-get install -qq expect + sudo add-apt-repository ppa:criu/ppa -y + sudo apt-get install -qq expect criu echo "::endgroup::" # This ensures that bridged traffic goes through netfilter diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 1c7aa9a0069..a7723d88898 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -68,14 +68,17 @@ jobs: go-version: ${{ env.GO_VERSION }} check-latest: true - # Install CNI + # Install CNI and CRIU - if: ${{ env.GO_VERSION != '' }} - name: "Init: set up CNI" + name: "Init: set up CNI and CRIU" run: | if [ "$RUNNER_OS" == "Windows" ]; then GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh elif [ "$RUNNER_OS" == "Linux" ]; then ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + sudo apt-get update -qq + sudo add-apt-repository ppa:criu/ppa -y + sudo apt-get install -qq criu fi - if: ${{ env.GO_VERSION != '' }} diff --git a/Dockerfile b/Dockerfile index 15868503c24..31c44584821 100644 --- a/Dockerfile +++ b/Dockerfile @@ -309,10 +309,17 @@ ARG DEBIAN_FRONTEND=noninteractive # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing # `jq` is required to generate test summaries RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ - expect \ - jq \ - git \ - make + software-properties-common \ + gnupg \ + gpg-agent \ + ca-certificates && \ + add-apt-repository ppa:criu/ppa && \ + apt-get update -qq && apt-get install -qq --no-install-recommends \ + expect \ + jq \ + git \ + make \ + criu # We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" COPY --from=build-base /usr/local/go /usr/local/go ARG TARGETARCH From a59921c5255f8db0a6f905c42c83d0b21d007faa Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sat, 11 Oct 2025 23:26:07 +0800 Subject: [PATCH 256/868] taskutil: introduce taskoptions to reduce argument numbers introduce taskoptions to reduce argument numbers. Otherwise, ci would be failed by: ``` Error: pkg/taskutil/taskutil.go:51:1: argument-limit: maximum number of arguments per function exceeded; max 12 but got 13 func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}, checkpointDir string) ``` Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_run.go | 14 +++++- pkg/containerutil/containerutil.go | 13 +++++- pkg/taskutil/taskutil.go | 63 ++++++++++++++++---------- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 0537f2b49d2..cd35c735969 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -431,8 +431,18 @@ func runAction(cmd *cobra.Command, args []string) error { } logURI := lab[labels.LogURI] detachC := make(chan struct{}) - task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach, - con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC, "") + task, err := taskutil.NewTask(ctx, client, c, taskutil.TaskOptions{ + AttachStreamOpt: createOpt.Attach, + IsInteractive: createOpt.Interactive, + IsTerminal: createOpt.TTY, + IsDetach: createOpt.Detach, + Con: con, + LogURI: logURI, + DetachKeys: createOpt.DetachKeys, + Namespace: createOpt.GOptions.Namespace, + DetachC: detachC, + CheckpointDir: "", + }) if err != nil { return err } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 57833e0e3dc..32f99b5229e 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -280,7 +280,18 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i // source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start attachStreamOpt = []string{"STDOUT", "STDERR"} } - task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, isInteractive, isTerminal, true, con, logURI, detachKeys, namespace, detachC, checkpointDir) + task, err := taskutil.NewTask(ctx, client, container, taskutil.TaskOptions{ + AttachStreamOpt: attachStreamOpt, + IsInteractive: isInteractive, + IsTerminal: isTerminal, + IsDetach: true, + Con: con, + LogURI: logURI, + DetachKeys: detachKeys, + Namespace: namespace, + DetachC: detachC, + CheckpointDir: checkpointDir, + }) if err != nil { return err } diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index d6d3af8f3f2..ec5f96585d6 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -49,19 +49,32 @@ import ( "github.com/containerd/nerdctl/v2/pkg/infoutil" ) +// TaskOptions contains options for creating a new task +type TaskOptions struct { + AttachStreamOpt []string + IsInteractive bool + IsTerminal bool + IsDetach bool + Con console.Console + LogURI string + DetachKeys string + Namespace string + DetachC chan<- struct{} + CheckpointDir string +} + // NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108 -func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, - attachStreamOpt []string, isInteractive, isTerminal, isDetach bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}, checkpointDir string) (containerd.Task, error) { +func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, opts TaskOptions) (containerd.Task, error) { var ( checkpoint *types.Descriptor t containerd.Task err error ) - if checkpointDir != "" { - tar := archive.Diff(ctx, "", checkpointDir) + if opts.CheckpointDir != "" { + tar := archive.Diff(ctx, "", opts.CheckpointDir) cs := client.ContentStore() - writer, err := cs.Writer(ctx, content.WithRef(checkpointDir)) + writer, err := cs.Writer(ctx, content.WithRef(opts.CheckpointDir)) if err != nil { return nil, err } @@ -96,8 +109,8 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } } closer := func() { - if detachC != nil { - detachC <- struct{}{} + if opts.DetachC != nil { + opts.DetachC <- struct{}{} } // t will be set by container.NewTask at the end of this function. // @@ -113,30 +126,30 @@ func NewTask(ctx context.Context, client *containerd.Client, container container io.Cancel() } var ioCreator cio.Creator - if len(attachStreamOpt) != 0 { + if len(opts.AttachStreamOpt) != 0 { log.G(ctx).Debug("attaching output instead of using the log-uri") // when attaching a TTY we use writee for stdio and binary for log persistence - if isTerminal { + if opts.IsTerminal { var in io.Reader - if isInteractive { + if opts.IsInteractive { // FIXME: check IsTerminal on Windows too if runtime.GOOS != "windows" && !term.IsTerminal(0) { return nil, errors.New("the input device is not a TTY") } var err error - in, err = consoleutil.NewDetachableStdin(con, detachKeys, closer) + in, err = consoleutil.NewDetachableStdin(opts.Con, opts.DetachKeys, closer) if err != nil { return nil, err } } - ioCreator = cioutil.NewContainerIO(namespace, logURI, true, in, con, nil) + ioCreator = cioutil.NewContainerIO(opts.Namespace, opts.LogURI, true, in, opts.Con, nil) } else { - streams := processAttachStreamsOpt(attachStreamOpt) - ioCreator = cioutil.NewContainerIO(namespace, logURI, false, streams.stdIn, streams.stdOut, streams.stdErr) + streams := processAttachStreamsOpt(opts.AttachStreamOpt) + ioCreator = cioutil.NewContainerIO(opts.Namespace, opts.LogURI, false, streams.stdIn, streams.stdOut, streams.stdErr) } - } else if isTerminal && isDetach { - u, err := url.Parse(logURI) + } else if opts.IsTerminal && opts.IsDetach { + u, err := url.Parse(opts.LogURI) if err != nil { return nil, err } @@ -162,32 +175,32 @@ func NewTask(ctx context.Context, client *containerd.Client, container container ioCreator = cio.TerminalBinaryIO(parsedPath, map[string]string{ args[0]: args[1], }) - } else if isTerminal && !isDetach { - if con == nil { + } else if opts.IsTerminal && !opts.IsDetach { + if opts.Con == nil { return nil, errors.New("got nil con with isTerminal=true") } var in io.Reader - if isInteractive { + if opts.IsInteractive { // FIXME: check IsTerminal on Windows too if runtime.GOOS != "windows" && !term.IsTerminal(0) { return nil, errors.New("the input device is not a TTY") } var err error - in, err = consoleutil.NewDetachableStdin(con, detachKeys, closer) + in, err = consoleutil.NewDetachableStdin(opts.Con, opts.DetachKeys, closer) if err != nil { return nil, err } } - ioCreator = cioutil.NewContainerIO(namespace, logURI, true, in, os.Stdout, os.Stderr) - } else if isDetach && logURI != "" && logURI != "none" { - u, err := url.Parse(logURI) + ioCreator = cioutil.NewContainerIO(opts.Namespace, opts.LogURI, true, in, os.Stdout, os.Stderr) + } else if opts.IsDetach && opts.LogURI != "" && opts.LogURI != "none" { + u, err := url.Parse(opts.LogURI) if err != nil { return nil, err } ioCreator = cio.LogURI(u) } else { var in io.Reader - if isInteractive { + if opts.IsInteractive { if sv, err := infoutil.ServerSemVer(ctx, client); err != nil { log.G(ctx).Warn(err) } else if sv.LessThan(semver.MustParse("1.6.0-0")) { @@ -205,7 +218,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } in = stdinC } - ioCreator = cioutil.NewContainerIO(namespace, logURI, false, in, os.Stdout, os.Stderr) + ioCreator = cioutil.NewContainerIO(opts.Namespace, opts.LogURI, false, in, os.Stdout, os.Stderr) } taskOpts := []containerd.NewTaskOpts{ From cabb02f3f7fadff4efcdbf127f44ff54b20eacec Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 21 Oct 2025 19:30:40 +0800 Subject: [PATCH 257/868] compose: align convergence with Docker Compose Write com.docker.compose.config-hash label on create/run. Fixes: #4547 Signed-off-by: ChengyuZhu6 --- pkg/composer/create.go | 5 +++++ pkg/composer/up.go | 6 ++++-- pkg/composer/up_service.go | 27 +++++++++++++++++++++++++++ pkg/labels/labels.go | 3 +++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/pkg/composer/create.go b/pkg/composer/create.go index 0dcbfc69cf9..ba7b58b77a7 100644 --- a/pkg/composer/create.go +++ b/pkg/composer/create.go @@ -188,10 +188,15 @@ func (c *Composer) createServiceContainer(ctx context.Context, service *servicep cidFilename := filepath.Join(tempDir, "cid") //add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels + currentHash, err := ServiceHash(*service.Unparsed) + if err != nil { + return "", fmt.Errorf("failed computing service hash for %s: %w", container.Name, err) + } container.RunArgs = append([]string{ "--cidfile=" + cidFilename, fmt.Sprintf("-l=%s=%s", labels.ComposeProject, c.project.Name), fmt.Sprintf("-l=%s=%s", labels.ComposeService, service.Unparsed.Name), + fmt.Sprintf("-l=%s=%s", labels.ComposeConfigHash, currentHash), }, container.RunArgs...) cmd := c.createNerdctlCmd(ctx, append([]string{"create"}, container.RunArgs...)...) diff --git a/pkg/composer/up.go b/pkg/composer/up.go index 84da4535c0f..3a03a08a8ca 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -85,7 +85,7 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro var parsedServices []*serviceparser.Service // use WithServices to sort the services in dependency order - if err := c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error { + forEachFn := func(name string, svc *types.ServiceConfig) error { if replicas, ok := uo.Scale[svc.Name]; ok { if svc.Deploy == nil { svc.Deploy = &types.DeployConfig{} @@ -98,7 +98,9 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro } parsedServices = append(parsedServices, ps) return nil - }); err != nil { + } + err := c.project.ForEachService(services, forEachFn) + if err != nil { return err } diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index 1af24b6c98b..fb9b1ed9fbb 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -155,6 +155,28 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse // delete container if it already exists if existingCid != "" { + // Default behavior for RecreateDiverged: compare stored hash with current service hash + if recreate == RecreateDiverged { + currentHash, err := ServiceHash(*service.Unparsed) + if err != nil { + return "", fmt.Errorf("failed computing service hash for %s: %w", container.Name, err) + } + con, err := c.client.LoadContainer(ctx, existingCid) + if err != nil { + return "", fmt.Errorf("failed to load container %s: %w", existingCid, err) + } + lbls, err := con.Labels(ctx) + if err != nil { + return "", fmt.Errorf("failed to read labels for %s: %w", existingCid, err) + } + if lbls[labels.ComposeConfigHash] == currentHash { + cmd := c.createNerdctlCmd(ctx, append([]string{"start"}, existingCid)...) + if err := c.executeUpCmd(ctx, cmd, container.Name, runFlagD, service.Unparsed.StdinOpen); err != nil { + return "", fmt.Errorf("error while starting existing container %s: %w", container.Name, err) + } + return existingCid, nil + } + } log.G(ctx).Debugf("Container %q already exists, deleting", container.Name) delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name) if err = delCmd.Run(); err != nil { @@ -184,10 +206,15 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse } //add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels + currentHash, err := ServiceHash(*service.Unparsed) + if err != nil { + return "", fmt.Errorf("failed computing service hash for %s: %w", container.Name, err) + } container.RunArgs = append([]string{ "--cidfile=" + cidFilename, fmt.Sprintf("-l=%s=%s", labels.ComposeProject, c.project.Name), fmt.Sprintf("-l=%s=%s", labels.ComposeService, service.Unparsed.Name), + fmt.Sprintf("-l=%s=%s", labels.ComposeConfigHash, currentHash), }, container.RunArgs...) cmd := c.createNerdctlCmd(ctx, append([]string{"run"}, container.RunArgs...)...) diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 792c74dbf9f..e0838d6ea9c 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -41,6 +41,9 @@ const ( //Compose Volume Name ComposeVolume = "com.docker.compose.volume" + // ComposeConfigHash stores the service configuration hash used for convergence decisions + ComposeConfigHash = "com.docker.compose.config-hash" + // Hostname Hostname = Prefix + "hostname" From a3c783bd83313028ad22800ed84a825e8cb56785 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 21 Oct 2025 19:56:09 +0800 Subject: [PATCH 258/868] compose: add unit tests for config hash and dependency handling - TestComposeCreateWritesConfigHashLabel: verify config-hash label is written - TestComposeUpNoRecreateDependencies: ensure dependencies aren't recreated Signed-off-by: ChengyuZhu6 --- .../compose/compose_create_linux_test.go | 22 ++++++++++ cmd/nerdctl/compose/compose_up_linux_test.go | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 4aa88efec05..a62d9b14104 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -204,3 +205,24 @@ services: base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "svc0").AssertOutContains(imageSvc0) base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created") } + +func TestComposeCreateWritesConfigHashLabel(t *testing.T) { + var dockerComposeYAML = fmt.Sprintf(` +services: + svc0: + image: %s +`, testutil.CommonImage) + + base := testutil.NewBase(t) + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + projectName := comp.ProjectName() + t.Logf("projectName=%q", projectName) + + base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK() + defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + + container := serviceparser.DefaultContainerName(projectName, "svc0", "1") + base.Cmd("inspect", "--format", "{{json .Config.Labels}}", container). + AssertOutContains("com.docker.compose.config-hash") +} diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index d9e50812411..f3a20190115 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -377,6 +377,46 @@ services: base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() } +func TestComposeUpNoRecreateDependencies(t *testing.T) { + base := testutil.NewBase(t) + + var dockerComposeYAML = fmt.Sprintf(` +services: + foo: + image: %s + command: "sleep infinity" + bar: + image: %s + command: "sleep infinity" + depends_on: + - foo +`, testutil.CommonImage, testutil.CommonImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + projectName := comp.ProjectName() + t.Logf("projectName=%q", projectName) + + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "foo").AssertOK() + defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + + fooName := serviceparser.DefaultContainerName(projectName, "foo", "1") + id1Cmd := base.Cmd("inspect", fooName, "--format", "{{.Id}}") + id1Res := id1Cmd.Run() + out1 := strings.TrimSpace(id1Res.Stdout()) + assert.Assert(id1Cmd.Base.T, id1Res.ExitCode == 0, id1Res.Stdout()+id1Res.Stderr()) + + // Bring up dependent service; ensure foo is not recreated (ID unchanged) + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "bar").AssertOK() + + id2Cmd := base.Cmd("inspect", fooName, "--format", "{{.Id}}") + id2Res := id2Cmd.Run() + out2 := strings.TrimSpace(id2Res.Stdout()) + assert.Assert(id2Cmd.Base.T, id2Res.ExitCode == 0, id2Res.Stdout()+id2Res.Stderr()) + + assert.Equal(base.T, out1, out2) +} + func TestComposeUpWithExternalNetwork(t *testing.T) { testCase := nerdtest.Setup() From 060469acb3909d352a73236aa3a690e86f316325 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 12 Oct 2025 10:07:58 +0800 Subject: [PATCH 259/868] Disable checkpoint/restore unit tests for docker Currently, nerdctl CI uses docker 28.0.4, while docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. Signed-off-by: ChengyuZhu6 --- .../checkpoint/checkpoint_create_linux_test.go | 15 +++++++++++++-- .../container/container_start_linux_test.go | 8 ++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go index 7548acdd1de..a2dec881b81 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go +++ b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go @@ -30,7 +30,13 @@ import ( func TestCheckpointCreateErrors(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.Rootless) + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) testCase.SubTests = []*test.Case{ { Description: "too-few-arguments", @@ -71,7 +77,12 @@ func TestCheckpointCreate(t *testing.T) { checkpointDir = "/dir/foo" ) testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.Rootless) + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) testCase.SubTests = []*test.Case{ { Description: "leave-running=true", diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index eb6d849e58b..1a86c3026b2 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -84,8 +84,12 @@ func TestStartDetachKeys(t *testing.T) { func TestStartWithCheckpoint(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.Rootless) - + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) testCase.Setup = func(data test.Data, helpers test.Helpers) { // Use an in-memory tmpfs to model in-memory state without introducing extra processes // Single PID 1 shell: continuously increment a counter and write to /state/counter (tmpfs) From ecace2712a43f1c0562ea0bf9cc7d40476eac2bf Mon Sep 17 00:00:00 2001 From: Sadique Azmi Date: Sun, 26 Oct 2025 04:48:26 +0530 Subject: [PATCH 260/868] fix: don't symlink buildkit-cni documentation files to bin/ The nerdctl-full tarball was incorrectly creating symlinks for all files in libexec/cni/, including documentation files like README.md and LICENSE. This resulted in non-executable files appearing in bin/ as buildkit-cni-README.md and buildkit-cni-LICENSE. Add executable and regular file checks to the symlink creation loop to filter out non-executable files. The fix uses [ -x "$f" ] to check for execute permission and [ -f "$f" ] to ensure it's a regular file, so only actual CNI plugin binaries are symlinked. Tested: bin/ file count reduced from 46 to 44 files (removed 2 doc symlinks). All 18 CNI plugin executables still correctly symlinked. Fixes #4553 Signed-off-by: Sadique Azmi --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 15868503c24..2a4a8337cd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -167,7 +167,7 @@ RUN BUILDKIT_VERSION=${BUILDKIT_VERSION%%@*}; \ grep "${fname}" "/SHA256SUMS.d/buildkit-${BUILDKIT_VERSION}" | sha256sum -c && \ tar xzf "${fname}" -C /out && \ rm -f "${fname}" /out/bin/buildkit-qemu-* /out/bin/buildkit-cni-* /out/bin/buildkit-runc && \ - for f in /out/libexec/cni/*; do ln -s ../libexec/cni/$(basename $f) /out/bin/buildkit-cni-$(basename $f); done && \ + for f in /out/libexec/cni/*; do [ -x "$f" ] && [ -f "$f" ] && ln -s ../libexec/cni/$(basename $f) /out/bin/buildkit-cni-$(basename $f); done && \ echo "- BuildKit: ${BUILDKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md # NOTE: github.com/moby/buildkit/examples/systemd is not included in BuildKit v0.8.x, will be included in v0.9.x RUN cd /out/lib/systemd/system && \ From 627f63def44728cb0e00c5e17c3eb69a7135d3f6 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 24 Oct 2025 15:10:21 +0800 Subject: [PATCH 261/868] fix ci failures about soci Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_run_soci_linux_test.go | 1 + cmd/nerdctl/image/image_convert_linux_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/cmd/nerdctl/container/container_run_soci_linux_test.go b/cmd/nerdctl/container/container_run_soci_linux_test.go index 07ad11a0f50..111db5dddc5 100644 --- a/cmd/nerdctl/container/container_run_soci_linux_test.go +++ b/cmd/nerdctl/container/container_run_soci_linux_test.go @@ -36,6 +36,7 @@ func TestRunSoci(t *testing.T) { testCase.Require = require.All( require.Not(nerdtest.Docker), + require.Amd64, nerdtest.Soci, ) diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 07cd2a7003d..a13fc08d595 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -38,6 +38,7 @@ func TestImageConvert(t *testing.T) { require.Not(require.Windows), require.Not(nerdtest.Docker), ), + NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.CommonImage) }, From 2fe22d0c6235ebc87a2a604b8aed0b7bc6b53a69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:37:32 +0000 Subject: [PATCH 262/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.5 to 0.15.6. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.5...v0.15.6) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b48b9f8f2ff..6555f7e8ad0 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.5 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.6 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4539db7b49d..05e7af8f269 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlK github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.5 h1:wP17QGv33SGItGQ+CvZIqEnwIMjX26vZ4hs5wFRQm8I= -github.com/containerd/nydus-snapshotter v0.15.5/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= +github.com/containerd/nydus-snapshotter v0.15.6 h1:WiJ1Ln0fcmCLfAoCMPOH+hBrM1kHF0Ydl2Ies+EoLW0= +github.com/containerd/nydus-snapshotter v0.15.6/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 063a07eb40a23fe89c02e6167e033937a5038d5e Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Sun, 14 Sep 2025 15:48:29 +0800 Subject: [PATCH 263/868] checkpoint: support checkpoint ls command Implement `nerdctl checkpoint ls` command to list checkpoints for a container, matching Docker's output format with "CHECKPOINT NAME" header. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/checkpoint/checkpoint.go | 8 ++ cmd/nerdctl/checkpoint/checkpoint_list.go | 95 +++++++++++++++++++++++ pkg/api/types/checkpoint_types.go | 12 +++ pkg/cmd/checkpoint/list.go | 71 +++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_list.go create mode 100644 pkg/cmd/checkpoint/list.go diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go index 10a8c00108f..a0eb4fa125a 100644 --- a/cmd/nerdctl/checkpoint/checkpoint.go +++ b/cmd/nerdctl/checkpoint/checkpoint.go @@ -34,7 +34,15 @@ func Command() *cobra.Command { cmd.AddCommand( CreateCommand(), + checkpointLsCommand(), ) return cmd } + +func checkpointLsCommand() *cobra.Command { + x := ListCommand() + x.Use = "ls" + x.Aliases = []string{"list"} + return x +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_list.go b/cmd/nerdctl/checkpoint/checkpoint_list.go new file mode 100644 index 00000000000..75fa986fe33 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_list.go @@ -0,0 +1,95 @@ +/* + 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 checkpoint + +import ( + "fmt" + "text/tabwriter" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" +) + +func ListCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list [OPTIONS] CONTAINER", + Short: "List checkpoints for a container", + Args: cobra.ExactArgs(1), + RunE: listAction, + ValidArgsFunction: listShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") + return cmd +} + +func processListFlags(cmd *cobra.Command) (types.CheckpointListOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.CheckpointListOptions{}, err + } + + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.CheckpointListOptions{}, err + } + if checkpointDir == "" { + checkpointDir = globalOptions.DataRoot + "/checkpoints" + } + + return types.CheckpointListOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + CheckpointDir: checkpointDir, + }, nil +} + +func listAction(cmd *cobra.Command, args []string) error { + listOptions, err := processListFlags(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), listOptions.GOptions.Namespace, listOptions.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + checkpoints, err := checkpoint.List(ctx, client, args[0], listOptions) + if err != nil { + return err + } + + w := tabwriter.NewWriter(listOptions.Stdout, 4, 8, 4, ' ', 0) + fmt.Fprintln(w, "CHECKPOINT NAME") + + for _, cp := range checkpoints { + fmt.Fprintln(w, cp.Name) + } + + return w.Flush() +} + +func listShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/checkpoint_types.go b/pkg/api/types/checkpoint_types.go index 46b055105c4..61b5c3ead47 100644 --- a/pkg/api/types/checkpoint_types.go +++ b/pkg/api/types/checkpoint_types.go @@ -27,3 +27,15 @@ type CheckpointCreateOptions struct { // Checkpoint directory CheckpointDir string } + +type CheckpointListOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Checkpoint directory + CheckpointDir string +} + +type CheckpointSummary struct { + // Name is the name of the checkpoint. + Name string +} diff --git a/pkg/cmd/checkpoint/list.go b/pkg/cmd/checkpoint/list.go new file mode 100644 index 00000000000..b8007d0f5ab --- /dev/null +++ b/pkg/cmd/checkpoint/list.go @@ -0,0 +1,71 @@ +/* + 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 checkpoint + +import ( + "context" + "fmt" + "os" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +func List(ctx context.Context, client *containerd.Client, containerID string, options types.CheckpointListOptions) ([]types.CheckpointSummary, error) { + var container containerd.Container + var out []types.CheckpointSummary + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req) + } + container = found.Container + return nil + }, + } + + n, err := walker.Walk(ctx, containerID) + if err != nil { + return nil, err + } else if n == 0 { + return nil, fmt.Errorf("error list checkpoint for container: %s, no such container", containerID) + } + + checkpointDir, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, "", container.ID(), false) + if err != nil { + return nil, err + } + + dirs, err := os.ReadDir(checkpointDir) + if err != nil { + return nil, err + } + + for _, d := range dirs { + if !d.IsDir() { + continue + } + out = append(out, types.CheckpointSummary{Name: d.Name()}) + } + + return out, nil +} From 1a0e7e9259bbec4d8b34820b329c9fa498e06391 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 28 Oct 2025 09:56:09 +0800 Subject: [PATCH 264/868] checkpoint: add unit test for checkpoint ls add unit test for checkpoint ls. Signed-off-by: ChengyuZhu6 --- .../checkpoint/checkpoint_list_linux_test.go | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go diff --git a/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go new file mode 100644 index 00000000000..bb1aec502c4 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go @@ -0,0 +1,107 @@ +//go:build linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestCheckpointListErrors(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("checkpoint", "list"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ExitCode: 1} + }, + }, + { + Description: "too-many-arguments", + Command: test.Command("checkpoint", "list", "too", "many", "arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ExitCode: 1} + }, + }, + { + Description: "invalid-container-id", + Command: test.Command("checkpoint", "list", "no-such-container"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("error list checkpoint for container: no-such-container")}, + } + }, + }, + } + + testCase.Run(t) +} + +func TestCheckpointList(t *testing.T) { + const checkpointName = "checkpoint-list" + + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("checkpoint", "create", data.Identifier(), checkpointName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "list", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + // First line is header, second should include the checkpoint name + Output: expect.Contains("CHECKPOINT NAME\n" + checkpointName + "\n"), + } + } + + testCase.Run(t) +} From 2233f3fbb9d0bb0b0c4bc0cb3c573de2cd4a3a42 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 28 Oct 2025 10:01:29 +0800 Subject: [PATCH 265/868] docs: add checkpoint list command reference add checkpoint list command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/command-reference.md b/docs/command-reference.md index 7fba7931258..aef4ea5080a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -55,6 +55,7 @@ - [:nerd_face: nerdctl image decrypt](#nerd_face-nerdctl-image-decrypt) - [Checkpoint management](#checkpoint-management) - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) + - [:whale: nerdctl checkpoint list](#whale-nerdctl-checkpoint-list) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) @@ -1076,6 +1077,15 @@ Flags: - :whale: `--leave-running`: Leave the container running after checkpoint - :whale: `checkpoint-dir`: Use a custom checkpoint storage directory +### :whale: nerdctl checkpoint list + +List checkpoints for a container + +Usage: `nerdctl checkpoint list/ls [OPTIONS] CONTAINER` + +Flags: +- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory + ## Manifest management ### :whale: nerdctl manifest annotate From 10e3213103a4268e5655438a2919ee55f44b41d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:01:53 +0000 Subject: [PATCH 266/868] build(deps): bump github.com/containerd/cgroups/v3 from 3.0.5 to 3.1.0 Bumps [github.com/containerd/cgroups/v3](https://github.com/containerd/cgroups) from 3.0.5 to 3.1.0. - [Release notes](https://github.com/containerd/cgroups/releases) - [Commits](https://github.com/containerd/cgroups/compare/v3.0.5...v3.1.0) --- updated-dependencies: - dependency-name: github.com/containerd/cgroups/v3 dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6555f7e8ad0..484a2b4e818 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Microsoft/hcsshim v0.13.0 github.com/compose-spec/compose-go/v2 v2.9.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 - github.com/containerd/cgroups/v3 v3.0.5 //gomodjail:unconfined + github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.9.0 github.com/containerd/containerd/v2 v2.1.4 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 05e7af8f269..16f2a0248cf 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1g github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= -github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY= +github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= From 717c54d8b9031aa0122880a3f870653859c148a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:01:59 +0000 Subject: [PATCH 267/868] build(deps): bump github.com/ipfs/go-cid from 0.5.0 to 0.6.0 Bumps [github.com/ipfs/go-cid](https://github.com/ipfs/go-cid) from 0.5.0 to 0.6.0. - [Release notes](https://github.com/ipfs/go-cid/releases) - [Commits](https://github.com/ipfs/go-cid/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: github.com/ipfs/go-cid dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6555f7e8ad0..e33f3107bb6 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.4.0 - github.com/ipfs/go-cid v0.5.0 + github.com/ipfs/go-cid v0.6.0 github.com/klauspost/compress v1.18.1 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 @@ -111,7 +111,7 @@ require ( github.com/multiformats/go-multiaddr v0.16.1 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect diff --git a/go.sum b/go.sum index 05e7af8f269..33cd2567487 100644 --- a/go.sum +++ b/go.sum @@ -166,8 +166,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= -github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= +github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= @@ -235,8 +235,8 @@ github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivnc github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= -github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= From 613cf0b3ef74b65a6f90e99721037b1c9049a88e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:02:03 +0000 Subject: [PATCH 268/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.6 to 0.15.7. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.6...v0.15.7) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6555f7e8ad0..5e5724b409e 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.6 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.7 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 05e7af8f269..bf003a01c21 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,8 @@ github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlK github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.6 h1:WiJ1Ln0fcmCLfAoCMPOH+hBrM1kHF0Ydl2Ies+EoLW0= -github.com/containerd/nydus-snapshotter v0.15.6/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= +github.com/containerd/nydus-snapshotter v0.15.7 h1:0/IVOpqM3TClKjzGRhmT3nq38IIZ62eACxGxTKcDk/0= +github.com/containerd/nydus-snapshotter v0.15.7/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 447d4cc0ca973ee5d33ada044d01b884fb9ee173 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 28 Oct 2025 10:24:11 +0800 Subject: [PATCH 269/868] checkpoint: support checkpoint rm command support checkpoint rm command. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/checkpoint/checkpoint.go | 7 ++ cmd/nerdctl/checkpoint/checkpoint_remove.go | 87 +++++++++++++++++++++ pkg/api/types/checkpoint_types.go | 7 ++ pkg/cmd/checkpoint/remove.go | 58 ++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_remove.go create mode 100644 pkg/cmd/checkpoint/remove.go diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go index a0eb4fa125a..db7a6232c1b 100644 --- a/cmd/nerdctl/checkpoint/checkpoint.go +++ b/cmd/nerdctl/checkpoint/checkpoint.go @@ -35,6 +35,7 @@ func Command() *cobra.Command { cmd.AddCommand( CreateCommand(), checkpointLsCommand(), + checkpointRmCommand(), ) return cmd @@ -46,3 +47,9 @@ func checkpointLsCommand() *cobra.Command { x.Aliases = []string{"list"} return x } +func checkpointRmCommand() *cobra.Command { + x := RemoveCommand() + x.Use = "rm" + x.Aliases = []string{"remove"} + return x +} diff --git a/cmd/nerdctl/checkpoint/checkpoint_remove.go b/cmd/nerdctl/checkpoint/checkpoint_remove.go new file mode 100644 index 00000000000..c45dee4cc06 --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_remove.go @@ -0,0 +1,87 @@ +/* + 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 checkpoint + +import ( + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" +) + +func RemoveCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "rm [OPTIONS] CONTAINER CHECKPOINT", + Short: "Remove a checkpoint", + Args: cobra.ExactArgs(2), + RunE: removeAction, + ValidArgsFunction: removeShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().String("checkpoint-dir", "", "Checkpoint directory") + return cmd +} + +func processRemoveFlags(cmd *cobra.Command) (types.CheckpointRemoveOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.CheckpointRemoveOptions{}, err + } + + checkpointDir, err := cmd.Flags().GetString("checkpoint-dir") + if err != nil { + return types.CheckpointRemoveOptions{}, err + } + if checkpointDir == "" { + checkpointDir = filepath.Join(globalOptions.DataRoot, "checkpoints") + } + + return types.CheckpointRemoveOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + CheckpointDir: checkpointDir, + }, nil +} + +func removeAction(cmd *cobra.Command, args []string) error { + removeOptions, err := processRemoveFlags(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), removeOptions.GOptions.Namespace, removeOptions.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + err = checkpoint.Remove(ctx, client, args[0], args[1], removeOptions) + if err != nil { + return err + } + + return nil +} + +func removeShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ImageNames(cmd) +} diff --git a/pkg/api/types/checkpoint_types.go b/pkg/api/types/checkpoint_types.go index 61b5c3ead47..1cc9f2aea29 100644 --- a/pkg/api/types/checkpoint_types.go +++ b/pkg/api/types/checkpoint_types.go @@ -35,6 +35,13 @@ type CheckpointListOptions struct { CheckpointDir string } +// CheckpointRemoveOptions specifies options for `nerdctl checkpoint rm`. +type CheckpointRemoveOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // Checkpoint directory + CheckpointDir string +} type CheckpointSummary struct { // Name is the name of the checkpoint. Name string diff --git a/pkg/cmd/checkpoint/remove.go b/pkg/cmd/checkpoint/remove.go new file mode 100644 index 00000000000..e8ae857f258 --- /dev/null +++ b/pkg/cmd/checkpoint/remove.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package checkpoint + +import ( + "context" + "fmt" + "os" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/checkpointutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" +) + +func Remove(ctx context.Context, client *containerd.Client, containerID string, checkpointName string, options types.CheckpointRemoveOptions) error { + var container containerd.Container + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple containers found with provided prefix: %s", found.Req) + } + container = found.Container + return nil + }, + } + + n, err := walker.Walk(ctx, containerID) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("error removing checkpoint for container: %s, no such container", containerID) + } + + targetPath, err := checkpointutil.GetCheckpointDir(options.CheckpointDir, checkpointName, container.ID(), false) + if err != nil { + return err + } + + return os.RemoveAll(targetPath) +} From cf142159492befeae488feb5bc3a6b0987403006 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 28 Oct 2025 11:10:33 +0800 Subject: [PATCH 270/868] checkpoint: add unit test for checkpoint rm add unit test for checkpoint rm Signed-off-by: ChengyuZhu6 --- .../checkpoint_remove_linux_test.go | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go diff --git a/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go new file mode 100644 index 00000000000..c00eb34da4b --- /dev/null +++ b/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go @@ -0,0 +1,129 @@ +/* + 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 checkpoint + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +func TestCheckpointRemoveErrors(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + testCase.SubTests = []*test.Case{ + { + Description: "too-few-arguments", + Command: test.Command("checkpoint", "rm", "too-few-arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "too-many-arguments", + Command: test.Command("checkpoint", "rm", "too", "many", "arguments"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "invalid-container-id", + Command: test.Command("checkpoint", "rm", "foo", "bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("error removing checkpoint for container: foo")}, + } + }, + }, + } + + testCase.Run(t) +} + +func TestCheckpointRemove(t *testing.T) { + const ( + checkpointName = "checkpoint-remove" + checkpointDir = "/dir/remove" + ) + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker version 28.x has a known regression that breaks Checkpoint/Restore functionality. + // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. + require.Not(nerdtest.Docker), + ) + testCase.SubTests = []*test.Case{ + { + Description: "remove-existing", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-running-remove"), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("checkpoint", "create", "--checkpoint-dir", checkpointDir, data.Identifier("container-running-remove"), checkpointName) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container-running-remove")) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-running-remove"), checkpointName) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals(""), + } + }, + }, + { + Description: "remove-nonexistent-checkpoint", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("container-clean-remove"), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container-clean-remove")) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-clean-remove"), checkpointName) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("checkpoint " + checkpointName + " does not exist for container")}, + } + }, + }, + } + + testCase.Run(t) +} From 1b2458dae08f1fb63bdc78a329e8144a7765d3a8 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Tue, 28 Oct 2025 12:16:03 +0800 Subject: [PATCH 271/868] docs: add checkpoint remove command reference add checkpoint remove command reference. Signed-off-by: ChengyuZhu6 --- docs/command-reference.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index aef4ea5080a..4bd051fa86a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -56,6 +56,7 @@ - [Checkpoint management](#checkpoint-management) - [:whale: nerdctl checkpoint create](#whale-nerdctl-checkpoint-create) - [:whale: nerdctl checkpoint list](#whale-nerdctl-checkpoint-list) + - [:whale: nerdctl checkpoint remove](#whale-nerdctl-checkpoint-remove) - [Manifest management](#manifest-management) - [:whale: nerdctl manifest annotate](#whale-nerdctl-manifest-annotate) - [:whale: nerdctl manifest create](#whale-nerdctl-manifest-create) @@ -1086,6 +1087,15 @@ Usage: `nerdctl checkpoint list/ls [OPTIONS] CONTAINER` Flags: - :whale: `checkpoint-dir`: Use a custom checkpoint storage directory +### :whale: nerdctl checkpoint remove + +Remove a checkpoint for a container + +Usage: `nerdctl checkpoint remove/rm [OPTIONS] CONTAINER CHECKPOINT` + +Flags: +- :whale: `checkpoint-dir`: Use a custom checkpoint storage directory + ## Manifest management ### :whale: nerdctl manifest annotate @@ -1958,7 +1968,6 @@ See [`./config.md`](./config.md). Container management: - `docker diff` -- `docker checkpoint *` Image: From 7d0091cd61bcef80f67e524b658ad172c06db7a1 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 29 Oct 2025 12:09:21 +0800 Subject: [PATCH 272/868] checkpoint: unexport subcommand unexport subcommand Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/checkpoint/checkpoint.go | 14 +++++++------- cmd/nerdctl/checkpoint/checkpoint_create.go | 2 +- .../checkpoint/checkpoint_create_linux_test.go | 1 + cmd/nerdctl/checkpoint/checkpoint_list.go | 2 +- .../checkpoint/checkpoint_list_linux_test.go | 3 +-- cmd/nerdctl/checkpoint/checkpoint_remove.go | 2 +- .../checkpoint/checkpoint_remove_linux_test.go | 3 +-- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/checkpoint/checkpoint.go b/cmd/nerdctl/checkpoint/checkpoint.go index db7a6232c1b..a17a29aeb71 100644 --- a/cmd/nerdctl/checkpoint/checkpoint.go +++ b/cmd/nerdctl/checkpoint/checkpoint.go @@ -33,22 +33,22 @@ func Command() *cobra.Command { } cmd.AddCommand( - CreateCommand(), - checkpointLsCommand(), - checkpointRmCommand(), + createCommand(), + lsCommand(), + rmCommand(), ) return cmd } -func checkpointLsCommand() *cobra.Command { - x := ListCommand() +func lsCommand() *cobra.Command { + x := listCommand() x.Use = "ls" x.Aliases = []string{"list"} return x } -func checkpointRmCommand() *cobra.Command { - x := RemoveCommand() +func rmCommand() *cobra.Command { + x := removeCommand() x.Use = "rm" x.Aliases = []string{"remove"} return x diff --git a/cmd/nerdctl/checkpoint/checkpoint_create.go b/cmd/nerdctl/checkpoint/checkpoint_create.go index 540acd44f26..39e8d9c3ec3 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_create.go +++ b/cmd/nerdctl/checkpoint/checkpoint_create.go @@ -28,7 +28,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) -func CreateCommand() *cobra.Command { +func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create [OPTIONS] CONTAINER CHECKPOINT", Short: "Create a checkpoint from a running container", diff --git a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go index a2dec881b81..cb2f0c2da1c 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go +++ b/cmd/nerdctl/checkpoint/checkpoint_create_linux_test.go @@ -83,6 +83,7 @@ func TestCheckpointCreate(t *testing.T) { // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) + testCase.NoParallel = true testCase.SubTests = []*test.Case{ { Description: "leave-running=true", diff --git a/cmd/nerdctl/checkpoint/checkpoint_list.go b/cmd/nerdctl/checkpoint/checkpoint_list.go index 75fa986fe33..ed49bbf6e12 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_list.go +++ b/cmd/nerdctl/checkpoint/checkpoint_list.go @@ -29,7 +29,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) -func ListCommand() *cobra.Command { +func listCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "list [OPTIONS] CONTAINER", Short: "List checkpoints for a container", diff --git a/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go index bb1aec502c4..05eb176e122 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go +++ b/cmd/nerdctl/checkpoint/checkpoint_list_linux_test.go @@ -80,7 +80,7 @@ func TestCheckpointList(t *testing.T) { // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) - + testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") helpers.Ensure("checkpoint", "create", data.Identifier(), checkpointName) @@ -88,7 +88,6 @@ func TestCheckpointList(t *testing.T) { testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("rmi", "-f", testutil.CommonImage) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { diff --git a/cmd/nerdctl/checkpoint/checkpoint_remove.go b/cmd/nerdctl/checkpoint/checkpoint_remove.go index c45dee4cc06..2149076f58c 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_remove.go +++ b/cmd/nerdctl/checkpoint/checkpoint_remove.go @@ -28,7 +28,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/checkpoint" ) -func RemoveCommand() *cobra.Command { +func removeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm [OPTIONS] CONTAINER CHECKPOINT", Short: "Remove a checkpoint", diff --git a/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go b/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go index c00eb34da4b..e43e0b5500a 100644 --- a/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go +++ b/cmd/nerdctl/checkpoint/checkpoint_remove_linux_test.go @@ -83,6 +83,7 @@ func TestCheckpointRemove(t *testing.T) { // The issue is tracked in the moby/moby project as https://github.com/moby/moby/issues/50750. require.Not(nerdtest.Docker), ) + testCase.NoParallel = true testCase.SubTests = []*test.Case{ { Description: "remove-existing", @@ -92,7 +93,6 @@ func TestCheckpointRemove(t *testing.T) { }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-running-remove")) - helpers.Anyhow("rmi", "-f", testutil.CommonImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-running-remove"), checkpointName) @@ -111,7 +111,6 @@ func TestCheckpointRemove(t *testing.T) { }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier("container-clean-remove")) - helpers.Anyhow("rmi", "-f", testutil.CommonImage) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("checkpoint", "rm", "--checkpoint-dir", checkpointDir, data.Identifier("container-clean-remove"), checkpointName) From 3d8307ecc48afd24173cbb2478e1cd30836abb78 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 29 Oct 2025 16:55:41 +0800 Subject: [PATCH 273/868] manifest: unexport subcommand unexport manifest subcommand. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/manifest/manifest.go | 10 +++++----- cmd/nerdctl/manifest/manifest_annotate.go | 2 +- cmd/nerdctl/manifest/manifest_create.go | 2 +- cmd/nerdctl/manifest/manifest_inspect.go | 2 +- cmd/nerdctl/manifest/manifest_push.go | 2 +- cmd/nerdctl/manifest/manifest_remove.go | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/nerdctl/manifest/manifest.go b/cmd/nerdctl/manifest/manifest.go index 50cecfd97e2..a504c3a4cf0 100644 --- a/cmd/nerdctl/manifest/manifest.go +++ b/cmd/nerdctl/manifest/manifest.go @@ -33,11 +33,11 @@ func Command() *cobra.Command { } cmd.AddCommand( - InspectCommand(), - CreateCommand(), - AnnotateCommand(), - RemoveCommand(), - PushCommand(), + inspectCommand(), + createCommand(), + annotateCommand(), + removeCommand(), + pushCommand(), ) return cmd diff --git a/cmd/nerdctl/manifest/manifest_annotate.go b/cmd/nerdctl/manifest/manifest_annotate.go index 8649bf41dfa..12c20a47e26 100644 --- a/cmd/nerdctl/manifest/manifest_annotate.go +++ b/cmd/nerdctl/manifest/manifest_annotate.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) -func AnnotateCommand() *cobra.Command { +func annotateCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "annotate INDEX/MANIFESTLIST MANIFEST", Short: "Add additional information to a local image manifest", diff --git a/cmd/nerdctl/manifest/manifest_create.go b/cmd/nerdctl/manifest/manifest_create.go index 962a406a09e..0a8a9f586dc 100644 --- a/cmd/nerdctl/manifest/manifest_create.go +++ b/cmd/nerdctl/manifest/manifest_create.go @@ -27,7 +27,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) -func CreateCommand() *cobra.Command { +func createCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "create INDEX/MANIFESTLIST MANIFEST [MANIFEST...]", Short: "Create a local index/manifest list for annotating and pushing to a registry", diff --git a/cmd/nerdctl/manifest/manifest_inspect.go b/cmd/nerdctl/manifest/manifest_inspect.go index 2572883a4c6..fa0393e4245 100644 --- a/cmd/nerdctl/manifest/manifest_inspect.go +++ b/cmd/nerdctl/manifest/manifest_inspect.go @@ -28,7 +28,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/formatter" ) -func InspectCommand() *cobra.Command { +func inspectCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "inspect MANIFEST", Short: "Display the contents of a manifest or image index/manifest list", diff --git a/cmd/nerdctl/manifest/manifest_push.go b/cmd/nerdctl/manifest/manifest_push.go index abb94f98007..be135dbffbb 100644 --- a/cmd/nerdctl/manifest/manifest_push.go +++ b/cmd/nerdctl/manifest/manifest_push.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) -func PushCommand() *cobra.Command { +func pushCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "push [OPTIONS] INDEX/MANIFESTLIST", Short: "Push a manifest list to a registry", diff --git a/cmd/nerdctl/manifest/manifest_remove.go b/cmd/nerdctl/manifest/manifest_remove.go index 0aabdbd3a26..822aeec0ed4 100644 --- a/cmd/nerdctl/manifest/manifest_remove.go +++ b/cmd/nerdctl/manifest/manifest_remove.go @@ -26,7 +26,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cmd/manifest" ) -func RemoveCommand() *cobra.Command { +func removeCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "rm INDEX/MANIFESTLIST [INDEX/MANIFESTLIST...]", Short: "Remove one or more index/manifest lists", From c0eb24cee906f55baff7673ec3f6b8a7c2024575 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:02:00 +0000 Subject: [PATCH 274/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.9.0 to 2.9.1. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.9.0...v2.9.1) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.9.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cc373224aa1..b59093ea53f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.13.0 - github.com/compose-spec/compose-go/v2 v2.9.0 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.9.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index b72060b9751..126c1f07be0 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.9.0 h1:UHSv/QHlo6QJtrT4igF1rdORgIUhDo1gWuyJUoiNNIM= -github.com/compose-spec/compose-go/v2 v2.9.0/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= +github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= +github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY= From 3ebc5b27a1e51e11c8392ae00b0dff5bdfa46dbc Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 31 Oct 2025 12:02:50 +0800 Subject: [PATCH 275/868] bump containerd to v2.2.0-rc.0 bump containerd to v2.2.0-rc.0 to enable mount manager support. Signed-off-by: ChengyuZhu6 --- go.mod | 20 ++++++++++---------- go.sum | 48 ++++++++++++++++++------------------------------ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index b59093ea53f..945ddf8bc30 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.24.2 +go 1.24.3 require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 - github.com/Microsoft/hcsshim v0.13.0 + github.com/Microsoft/hcsshim v0.14.0-rc.1 github.com/compose-spec/compose-go/v2 v2.9.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined - github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.4 //gomodjail:unconfined + github.com/containerd/containerd/api v1.10.0-rc.0 + github.com/containerd/containerd/v2 v2.2.0-rc.0 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -90,7 +90,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -112,7 +112,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect - github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect + github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect github.com/opencontainers/selinux v1.12.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -124,8 +124,6 @@ require ( github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - //gomodjail:unconfined - github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.3.0 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect @@ -137,16 +135,18 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.28.0 // indirect + golang.org/x/mod v0.29.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined google.golang.org/grpc v1.76.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) +require github.com/moby/sys/capability v0.4.0 // indirect + replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 126c1f07be0..e9c0d3bd9c9 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= -github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= +github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -27,10 +27,10 @@ github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9v github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= -github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ= -github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd/api v1.10.0-rc.0 h1:PEaPRT4atfXLlbr3HaZH/i7/2PZK/+sUnp210HkhXmY= +github.com/containerd/containerd/api v1.10.0-rc.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= +github.com/containerd/containerd/v2 v2.2.0-rc.0 h1:GKx770lifsFnFHyemV++6DgCMlZVrPtgW7C+WtlwfQw= +github.com/containerd/containerd/v2 v2.2.0-rc.0/go.mod h1:X7H17UATRidzfNzb5vS/l30qEJk0ktoTEU1thMh0Nbk= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -131,8 +131,8 @@ 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-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -157,11 +157,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -199,11 +198,12 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/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/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= @@ -222,7 +222,6 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -245,12 +244,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= -github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -278,7 +275,6 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+x github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= @@ -296,26 +292,20 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -378,8 +368,8 @@ 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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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= @@ -415,8 +405,6 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -503,8 +491,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From ee2f1fadd155a93290d6661eb0f2cec2f92b3ec7 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 31 Oct 2025 20:36:27 +0800 Subject: [PATCH 276/868] bump to containerd v2.2.0-rc.0 Signed-off-by: ChengyuZhu6 --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- hack/generate-release-note.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 54b86450877..ffe980d0a9b 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,9 +146,9 @@ jobs: go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.1.4 + containerd-version: 2.2.0-rc.0 # Note: these as for amd64 - containerd-sha: 316d510a0428276d931023f72c09fdff1a6ba81d6cc36f31805fea6a3c88f515 + containerd-sha: 8a7956e91a33bca10b13b196df00e73acebb7f3931e0d1456c0209139f96251b containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.8.0 linux-cni-sha: ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 diff --git a/Dockerfile b/Dockerfile index d67ce6a8b04..13277e48394 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.1.4@75cb2b7193e4e490e9fbdc236c0e811ccaba3376 +ARG CONTAINERD_VERSION=v2.2.0-rc.0@870bb7c80de5f4f69952b0188154ce61dbd4115a ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY diff --git a/hack/generate-release-note.sh b/hack/generate-release-note.sh index b4e75493397..54124b79500 100755 --- a/hack/generate-release-note.sh +++ b/hack/generate-release-note.sh @@ -25,7 +25,7 @@ cat <<-EOX (To be documented) ## Compatible containerd versions -This release of nerdctl is expected to be used with containerd v1.7, v2.0, or v2.1. +This release of nerdctl is expected to be used with containerd v1.7, v2.0, v2.1, or v2.2. Some features may not work with other releases of containerd. ## About the binaries From 80489669cd9faa41cdcf16b9b630a11c3a51960c Mon Sep 17 00:00:00 2001 From: Gao Xiang Date: Fri, 31 Oct 2025 22:13:00 +0800 Subject: [PATCH 277/868] Should use mount.UnmountMounts for unmounting submounts See PR https://github.com/containerd/containerd/issues/7839 and commit 34d587818596 ("Use mount.Target to specify subdirectory of rootfs mount") for more details. Signed-off-by: Gao Xiang --- pkg/cmd/container/run_mount.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 92900a21d59..75ccb4e1bb6 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -175,8 +175,8 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm // windows has additional steps for mounting see // https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b - unmounter := func(mountPath string) { - if uerr := mount.Unmount(mountPath, 0); uerr != nil { + unmounter := func(tempDir string) { + if uerr := mount.UnmountMounts(mounts, tempDir, 0); uerr != nil { log.G(ctx).Debugf("Failed to unmount snapshot %q", tempDir) if err == nil { err = uerr From 2ba655afa163627159f053663f2fd002dd100824 Mon Sep 17 00:00:00 2001 From: Gao Xiang Date: Fri, 31 Oct 2025 22:27:45 +0800 Subject: [PATCH 278/868] Add support for mount manager So that $ nerdctl run --snapshotter=erofs -it --rm docker.1ms.run/library/nginx:latest /bin/sh can work now instead of erroring out as: ``` FATA[0000] failed to mount {Type:format/mkdir/overlay Source:overlay Target: Options:[lowerdir={{ overlay 0 6 }}]} on "/tmp/initialC2844197147": mount source: "overlay", target: "/tmp/initialC2844197147", fstype: format/mkdir/overlay, flags: 0, data: "lowerdir={{ overlay 0 6 }}", err: no such device ``` Signed-off-by: Gao Xiang --- pkg/cmd/container/run_mount.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 75ccb4e1bb6..bd0c4a08645 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -173,6 +173,16 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm return nil, nil, nil, err } + mm := client.MountManager() + + active, err := mm.Activate(ctx, tempDir, mounts) + if err == nil { + defer mm.Deactivate(ctx, tempDir) + mounts = active.System + } else if !errors.Is(err, errdefs.ErrNotImplemented) { + return nil, nil, nil, fmt.Errorf("failed to activate mounts: %w", err) + } + // windows has additional steps for mounting see // https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b unmounter := func(tempDir string) { From 4f099fb1d7ca7b939c456c44842b5122c368968c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:38:26 +0900 Subject: [PATCH 279/868] update runc (1.3.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 13277e48394..cd77bc6303f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.0-rc.0@870bb7c80de5f4f69952b0188154ce61dbd4115a -ARG RUNC_VERSION=v1.3.1@e6457afc48eff1ce22dece664932395026a7105e +ARG RUNC_VERSION=v1.3.2@aeabe4e711d903ef0ea86a4155da0f9e00eabd29 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build From 7c8021b888ea80d5649c9af549c513c2cd6c4c51 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:40:01 +0900 Subject: [PATCH 280/868] update BuildKit (0.25.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 diff --git a/Dockerfile b/Dockerfile index cd77bc6303f..3be020373f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.3.2@aeabe4e711d903ef0ea86a4155da0f9e00eabd29 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.24.0@BINARY +ARG BUILDKIT_VERSION=v0.25.1@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.17.0@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 deleted file mode 100644 index 61d26717528..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.24.0 +++ /dev/null @@ -1,2 +0,0 @@ -af8064eca16077b4d6937745988ba2d2dfa439540874cdcd918318315f3ba1d3 buildkit-v0.24.0.linux-amd64.tar.gz -38dc4433d220bb43c198df2070e49d5dde5ed44ee31fb80d6b13722eec21d4ea buildkit-v0.24.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 new file mode 100644 index 00000000000..09f346a9477 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 @@ -0,0 +1,2 @@ +85aa27c480fc9159705d21a3e66abe305f8dc708b8aeecd1e87bbc5c3de98642 buildkit-v0.25.1.linux-amd64.tar.gz +b7ad3db5e886ef40a28974dc94217666054c147bd0e06e5f71420210c4b72fba buildkit-v0.25.1.linux-arm64.tar.gz From 846bf8478e2816a5fa4aed119ca6b4acc4fab87c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:41:35 +0900 Subject: [PATCH 281/868] update stargz-snapshotter (0.18.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 | 3 --- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 diff --git a/Dockerfile b/Dockerfile index 3be020373f3..6f8cff732ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.25.1@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.17.0@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.0@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 deleted file mode 100644 index 785c3b2acec..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.17.0 +++ /dev/null @@ -1,3 +0,0 @@ -e3cd9aed03a0fc82adc2484a3fe94381d21f52d998419e15ca019744d27e18b7 stargz-snapshotter-v0.17.0-linux-amd64.tar.gz -9b3e85729885d7b5c4a3b7b67a8c8048065f60b2098fec17251f256d49bb24bb stargz-snapshotter-v0.17.0-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 new file mode 100644 index 00000000000..17ed33fc0fa --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 @@ -0,0 +1,3 @@ +1baf2a04a0291187a0c3f5b99eed67172553dc65b9683736d3c9f3e5f7b179d9 stargz-snapshotter-v0.18.0-linux-amd64.tar.gz +8d678d65dcfb73781af62dcb2c17ed80f045147b905d13e557396451e9d45eb4 stargz-snapshotter-v0.18.0-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service From df7fda218c50e07a73d44b4722874e9d842b2dbd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:42:53 +0900 Subject: [PATCH 282/868] update gotestsum (1.13.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6f8cff732ff..2b938ea3d92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 -ARG GOTESTSUM_VERSION=v1.12.3 +ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.5 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 From e5213a41b29a9787edcf537f071d2a590be4681b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:43:11 +0900 Subject: [PATCH 283/868] update Nydus (2.3.9) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2b938ea3d92..7d2b2f1365d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG GO_VERSION=1.25 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 -ARG NYDUS_VERSION=v2.3.5 +ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.37.0 From d0989241315986cebf28199b9cc149f2faad5527 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 1 Nov 2025 01:43:36 +0900 Subject: [PATCH 284/868] update Kubo (0.38.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7d2b2f1365d..3c68ee9838c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 -ARG KUBO_VERSION=v0.37.0 +ARG KUBO_VERSION=v0.38.2 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0@sha256:add602d55daca18914838a78221f6bbe4284114b452c86a48f96d59aeb00f5c6 AS xx From d34eb13bde26f848d073da68f97d94f7f5920681 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:17:11 +0000 Subject: [PATCH 285/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.2.0-rc.0 to 2.2.0-rc.1. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.2.0-rc.0...v2.2.0-rc.1) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.2.0-rc.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 945ddf8bc30..a64ce030ff8 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.10.0-rc.0 - github.com/containerd/containerd/v2 v2.2.0-rc.0 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.2.0-rc.1 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index e9c0d3bd9c9..187343b1692 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.10.0-rc.0 h1:PEaPRT4atfXLlbr3HaZH/i7/2PZK/+sUnp210HkhXmY= github.com/containerd/containerd/api v1.10.0-rc.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.2.0-rc.0 h1:GKx770lifsFnFHyemV++6DgCMlZVrPtgW7C+WtlwfQw= -github.com/containerd/containerd/v2 v2.2.0-rc.0/go.mod h1:X7H17UATRidzfNzb5vS/l30qEJk0ktoTEU1thMh0Nbk= +github.com/containerd/containerd/v2 v2.2.0-rc.1 h1:806y9qsFiZkwl90DJhtAtedG43OcmGd57sJCFyvfxpo= +github.com/containerd/containerd/v2 v2.2.0-rc.1/go.mod h1:X7H17UATRidzfNzb5vS/l30qEJk0ktoTEU1thMh0Nbk= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From 7f48540d50d64721ee73540456cb159e61963718 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:02:47 +0000 Subject: [PATCH 286/868] build(deps): bump docker/metadata-action from 5.8.0 to 5.9.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.8.0 to 5.9.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/c1e51972afc2121e065aed6d45c65596fe445f3f...318604b99e75e41977312d83839a89be02ca4893) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 5.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index b0c2801f02a..90baf17f45a 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -54,7 +54,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From bf8a30b2ceb5fbb2af93678239c5c07afae2e9f7 Mon Sep 17 00:00:00 2001 From: Shubhranshu Mahapatra Date: Wed, 5 Nov 2025 01:32:54 -0800 Subject: [PATCH 287/868] Upgrade runc to v1.3.3 Signed-off-by: Shubhranshu Mahapatra --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3c68ee9838c..87d1a8d781d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.0-rc.0@870bb7c80de5f4f69952b0188154ce61dbd4115a -ARG RUNC_VERSION=v1.3.2@aeabe4e711d903ef0ea86a4155da0f9e00eabd29 +ARG RUNC_VERSION=v1.3.3@d842d7719497cc3b774fd71620278ac9e17710e0 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build From 351dd6d2386ec871a393f150cc6190ce18329d36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:02:20 +0000 Subject: [PATCH 288/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/docker/docker](https://github.com/docker/docker). Updates `github.com/docker/cli` from 28.5.1+incompatible to 28.5.2+incompatible - [Commits](https://github.com/docker/cli/compare/v28.5.1...v28.5.2) Updates `github.com/docker/docker` from 28.5.1+incompatible to 28.5.2+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.5.1...v28.5.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.5.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/docker/docker dependency-version: 28.5.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a64ce030ff8..de1a9f5518e 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.5.1+incompatible //gomodjail:unconfined - github.com/docker/docker v28.5.1+incompatible //gomodjail:unconfined + github.com/docker/cli v28.5.2+incompatible //gomodjail:unconfined + github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 187343b1692..55b0602e368 100644 --- a/go.sum +++ b/go.sum @@ -88,10 +88,10 @@ 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 v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= -github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= -github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.5.2+incompatible h1:XmG99IHcBmIAoC1PPg9eLBZPlTrNUAijsHLm8PjhBlg= +github.com/docker/cli v28.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= From 93fe937416ccf88b5822158957edb9057739b69f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:02:48 +0000 Subject: [PATCH 289/868] build(deps): bump docker/setup-qemu-action from 3.6.0 to 3.7.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.6.0 to 3.7.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/29109295f81e9208d7d86ff1c6c12d2833863392...c7c53464625b32c7a7e944ae62b3e17d2b600130) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 3.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 90baf17f45a..eb263c6c256 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -35,7 +35,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d74012312e2..921d7736afa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: "Install go" uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: From cc70bc3fccea5429b6b250ed035c18c24799e0be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 01:41:22 +0000 Subject: [PATCH 290/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.2.0-rc.1 to 2.2.0. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.2.0-rc.1...v2.2.0) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a64ce030ff8..c0fd7f7d09e 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined - github.com/containerd/containerd/api v1.10.0-rc.0 - github.com/containerd/containerd/v2 v2.2.0-rc.1 //gomodjail:unconfined + github.com/containerd/containerd/api v1.10.0 + github.com/containerd/containerd/v2 v2.2.0 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -21,7 +21,7 @@ require ( github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.7 //gomodjail:unconfined - github.com/containerd/platforms v1.0.0-rc.1 //gomodjail:unconfined + github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.0 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/ipfs v0.18.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 187343b1692..ce2ee7f1570 100644 --- a/go.sum +++ b/go.sum @@ -27,10 +27,10 @@ github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9v github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.10.0-rc.0 h1:PEaPRT4atfXLlbr3HaZH/i7/2PZK/+sUnp210HkhXmY= -github.com/containerd/containerd/api v1.10.0-rc.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.2.0-rc.1 h1:806y9qsFiZkwl90DJhtAtedG43OcmGd57sJCFyvfxpo= -github.com/containerd/containerd/v2 v2.2.0-rc.1/go.mod h1:X7H17UATRidzfNzb5vS/l30qEJk0ktoTEU1thMh0Nbk= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8= +github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -49,8 +49,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.7 h1:0/IVOpqM3TClKjzGRhmT3nq38IIZ62eACxGxTKcDk/0= github.com/containerd/nydus-snapshotter v0.15.7/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= -github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= -github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/stargz-snapshotter v0.18.0 h1:C7mqAnH5v+ZE9FK+ZFt8qsb9uHfuRJXlpqQAQpq8PDc= From a99db0e30fc0a1884612dbb5e0e4651776a49cca Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 6 Nov 2025 10:55:49 +0900 Subject: [PATCH 291/868] update containerd (2.2.0) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index ffe980d0a9b..189b92b4749 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,7 +146,7 @@ jobs: go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.2.0-rc.0 + containerd-version: 2.2.0 # Note: these as for amd64 containerd-sha: 8a7956e91a33bca10b13b196df00e73acebb7f3931e0d1456c0209139f96251b containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 diff --git a/Dockerfile b/Dockerfile index 87d1a8d781d..85e545304d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.2.0-rc.0@870bb7c80de5f4f69952b0188154ce61dbd4115a +ARG CONTAINERD_VERSION=v2.2.0@1c4457e00facac03ce1d75f7b6777a7a851e5c41 ARG RUNC_VERSION=v1.3.3@d842d7719497cc3b774fd71620278ac9e17710e0 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY From dbeb9e1a1476de3920004f0cfb8db8af06b97b07 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 6 Nov 2025 10:58:23 +0900 Subject: [PATCH 292/868] update BuildKit (0.25.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 diff --git a/Dockerfile b/Dockerfile index 85e545304d3..18b3adba46d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.3.3@d842d7719497cc3b774fd71620278ac9e17710e0 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.25.1@BINARY +ARG BUILDKIT_VERSION=v0.25.2@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.0@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 deleted file mode 100644 index 09f346a9477..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.1 +++ /dev/null @@ -1,2 +0,0 @@ -85aa27c480fc9159705d21a3e66abe305f8dc708b8aeecd1e87bbc5c3de98642 buildkit-v0.25.1.linux-amd64.tar.gz -b7ad3db5e886ef40a28974dc94217666054c147bd0e06e5f71420210c4b72fba buildkit-v0.25.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 new file mode 100644 index 00000000000..fcea42d1add --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 @@ -0,0 +1,2 @@ +d18b8188b600e201a1b3d08c20e2a1e439cf51d2c2285f132dd9bd51c666f6d6 buildkit-v0.25.2.linux-amd64.tar.gz +f6fd69c40bec1788d650430ede40545a47bd765922ad23456a463e88b47039cf buildkit-v0.25.2.linux-arm64.tar.gz From 9c0f0ff3738217097a26875b4126c76bb88ec822 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 6 Nov 2025 11:00:18 +0900 Subject: [PATCH 293/868] update containerd-fuse-overlayfs (2.1.7) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 | 6 ------ Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 create mode 100644 Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 diff --git a/Dockerfile b/Dockerfile index 18b3adba46d..1b58625f7f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS ARG FUSE_OVERLAYFS_VERSION=v1.15@BINARY -ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.6@BINARY +ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 deleted file mode 100644 index b76b93d4d62..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 +++ /dev/null @@ -1,6 +0,0 @@ -8a768e4c953251d32b5e5d748d17593f7150834caaba403b483cf83f5856fea3 containerd-fuse-overlayfs-2.1.6-linux-amd64.tar.gz -a3af866a12e913cd1d4dda8e41c08345eca928a15ac1d466fdb2b00b013e14ee containerd-fuse-overlayfs-2.1.6-linux-arm-v7.tar.gz -417ca0c838e43e446f498b384d73f7caaeb00dc4c1c0fe4b0ecfdd36fd355daa containerd-fuse-overlayfs-2.1.6-linux-arm64.tar.gz -5fdebd9fb7b50473318f0410bc3ab46f3388ac8aa586b45c91a314af9ce6569c containerd-fuse-overlayfs-2.1.6-linux-ppc64le.tar.gz -7e1a9d2ba68ff31a8dfb53bf6e71b2879063b13c759922c8cff3013893829bca containerd-fuse-overlayfs-2.1.6-linux-riscv64.tar.gz -3c022651cdaff666e88996d5d9c7e776bf59419a03d7d718a28aa708036419f9 containerd-fuse-overlayfs-2.1.6-linux-s390x.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 new file mode 100644 index 00000000000..e29367cf0d9 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.7 @@ -0,0 +1,6 @@ +d54148043c22381af89cec2a167431e40668716404a1eb682ca69dfb890376f3 containerd-fuse-overlayfs-2.1.7-linux-amd64.tar.gz +a301030391d51356065f628b5e6e5a5a8c55f1978289eb71d8f5284af7a81eda containerd-fuse-overlayfs-2.1.7-linux-arm-v7.tar.gz +94ed6c2c3bece42e0c789ea056565b64fe487de4644121ee0dfb8acd8ef9369c containerd-fuse-overlayfs-2.1.7-linux-arm64.tar.gz +1bfb1f86894b640781d837ec0f66997222b419532fae730579140dbc1c7ea858 containerd-fuse-overlayfs-2.1.7-linux-ppc64le.tar.gz +9f2ef69b06229f5357f3fc23524922cea6616663ff220979a110a7742aaffee6 containerd-fuse-overlayfs-2.1.7-linux-riscv64.tar.gz +03f61035cef5fff33c5084c55f133d0340597520d8d12112970609dff0bd1e7a containerd-fuse-overlayfs-2.1.7-linux-s390x.tar.gz From f77c125465522a92c04069c486d63504893dd470 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 6 Nov 2025 13:40:33 +0900 Subject: [PATCH 294/868] update fuse-overlayfs (1.16) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 | 6 ------ Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 create mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 diff --git a/Dockerfile b/Dockerfile index 1b58625f7f1..a7681310f4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS -ARG FUSE_OVERLAYFS_VERSION=v1.15@BINARY +ARG FUSE_OVERLAYFS_VERSION=v1.16@BINARY ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 deleted file mode 100644 index f3eea29017e..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 +++ /dev/null @@ -1,6 +0,0 @@ -a62829baa7a7d39d0a9a784d51ebd528efe226192c0a86ba6667d0fcae9129c3 fuse-overlayfs-aarch64 -7ad67a810100bebf63c41fbb621df3d552531db94d600a94f5f701b1e9f8aa5a fuse-overlayfs-armv7l -9778e1f0da1429469bcc65ea90a7504e63f0a258089b9bb1ae65105330e61808 fuse-overlayfs-ppc64le -f7a2852983b3d0a8f15c31084c215b4965d5b62b9ce1014708283dd2dd909b28 fuse-overlayfs-riscv64 -89a410a67822002c20ff21d8a9e5353ebda00d3a2f79fd99f26fb47533e253a5 fuse-overlayfs-s390x -1cd97f5ca7ac52fa192c94c1e605713cfb27d3dc417c0bef4dcfb9fb20e01e81 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 new file mode 100644 index 00000000000..edf43283f18 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 @@ -0,0 +1,6 @@ +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 From c77b104e3a8f38959d95afcab160c1a89f9a0bef Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 6 Nov 2025 14:46:35 +0900 Subject: [PATCH 295/868] update stargz-snapshotter (0.18.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 | 3 --- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 diff --git a/Dockerfile b/Dockerfile index a7681310f4e..ddf99183bb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.25.2@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.0@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 deleted file mode 100644 index 17ed33fc0fa..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.0 +++ /dev/null @@ -1,3 +0,0 @@ -1baf2a04a0291187a0c3f5b99eed67172553dc65b9683736d3c9f3e5f7b179d9 stargz-snapshotter-v0.18.0-linux-amd64.tar.gz -8d678d65dcfb73781af62dcb2c17ed80f045147b905d13e557396451e9d45eb4 stargz-snapshotter-v0.18.0-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 new file mode 100644 index 00000000000..831e77f35a2 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 @@ -0,0 +1,3 @@ +f8f106a61b9fc797a6336d6c06435cdbf8b896f3f49fdc5288e08e87dff6bbdf stargz-snapshotter-v0.18.1-linux-amd64.tar.gz +643d04f5e97e83606b9ee129c2c33513df13a091dbc1dc084256d13a1034b749 stargz-snapshotter-v0.18.1-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service From 7e7d36a2cf7a78b692f460b39433335dfcb636d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:45:57 +0000 Subject: [PATCH 296/868] build(deps): bump github.com/containerd/imgcrypt/v2 from 2.0.1 to 2.0.2 Bumps [github.com/containerd/imgcrypt/v2](https://github.com/containerd/imgcrypt) from 2.0.1 to 2.0.2. - [Release notes](https://github.com/containerd/imgcrypt/releases) - [Changelog](https://github.com/containerd/imgcrypt/blob/main/CHANGES) - [Commits](https://github.com/containerd/imgcrypt/compare/v2.0.1...v2.0.2) --- updated-dependencies: - dependency-name: github.com/containerd/imgcrypt/v2 dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6fd456c452a..68b30f8e577 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined github.com/containerd/go-cni v1.1.13 //gomodjail:unconfined - github.com/containerd/imgcrypt/v2 v2.0.1 //gomodjail:unconfined + github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.7 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 2292f8912c1..fa041c45c89 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7H github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA= github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= -github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlKUy6kOio= -github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= +github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtqtALqfuM= +github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.7 h1:0/IVOpqM3TClKjzGRhmT3nq38IIZ62eACxGxTKcDk/0= From bc542706727d20bb1c5c20bc93b65653f9909bcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:01:49 +0000 Subject: [PATCH 297/868] build(deps): bump the stargz group with 3 updates Bumps the stargz group with 3 updates: [github.com/containerd/stargz-snapshotter](https://github.com/containerd/stargz-snapshotter), [github.com/containerd/stargz-snapshotter/estargz](https://github.com/containerd/stargz-snapshotter) and [github.com/containerd/stargz-snapshotter/ipfs](https://github.com/containerd/stargz-snapshotter). Updates `github.com/containerd/stargz-snapshotter` from 0.18.0 to 0.18.1 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.0...v0.18.1) Updates `github.com/containerd/stargz-snapshotter/estargz` from 0.18.0 to 0.18.1 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.0...v0.18.1) Updates `github.com/containerd/stargz-snapshotter/ipfs` from 0.18.0 to 0.18.1 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.0...v0.18.1) --- updated-dependencies: - dependency-name: github.com/containerd/stargz-snapshotter dependency-version: 0.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/estargz dependency-version: 0.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/ipfs dependency-version: 0.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 68b30f8e577..c5b3515f08c 100644 --- a/go.mod +++ b/go.mod @@ -22,9 +22,9 @@ require ( github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.7 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter v0.18.0 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/estargz v0.18.0 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/ipfs v0.18.0 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/ipfs v0.18.1 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index fa041c45c89..82a84dd9d82 100644 --- a/go.sum +++ b/go.sum @@ -53,12 +53,12 @@ github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6a github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter v0.18.0 h1:C7mqAnH5v+ZE9FK+ZFt8qsb9uHfuRJXlpqQAQpq8PDc= -github.com/containerd/stargz-snapshotter v0.18.0/go.mod h1:BnVpVqp79HpVPtOOiK/O/2HINEoCf/Gz9vzXrtRArnE= -github.com/containerd/stargz-snapshotter/estargz v0.18.0 h1:Ny5yptQgEXSkDFKvlKJGTvf1YJ+4xD8V+hXqoRG0n74= -github.com/containerd/stargz-snapshotter/estargz v0.18.0/go.mod h1:7hfU1BO2KB3axZl0dRQCdnHrIWw7TRDdK6L44Rdeuo0= -github.com/containerd/stargz-snapshotter/ipfs v0.18.0 h1:yDIKLwldoQFS9wpZHaGdqNZCwIkTgsUuV5pNAX9JC9M= -github.com/containerd/stargz-snapshotter/ipfs v0.18.0/go.mod h1:aVNaKOoeNgAKEMphB6YeAowWnVG+7Fa3vvFHltM1zGE= +github.com/containerd/stargz-snapshotter v0.18.1 h1:eIkwsafohSWas5YmhxoumrI7elmb2EZJcW8eu7goyOY= +github.com/containerd/stargz-snapshotter v0.18.1/go.mod h1:HPC+XHGIxkjWfAONMvXepQyOs8iGApP2e5A3fOv2TCU= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/stargz-snapshotter/ipfs v0.18.1 h1:v0kozDNJCW1iVy/1MgD5uZImW87CvkHLzb9L9JwfOco= +github.com/containerd/stargz-snapshotter/ipfs v0.18.1/go.mod h1:qgy0jrKhqtLxn6J5rb9BXZ1Xj1Xeimd0vOi25Zyhpds= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= From 067381fe9a97d89c219bef9f56bb8e7bc86b868d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:02:03 +0000 Subject: [PATCH 298/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 28.5.2+incompatible to 29.0.0+incompatible - [Commits](https://github.com/docker/cli/compare/v28.5.2...v29.0.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.0.0+incompatible dependency-type: direct:production update-type: version-update:semver-major dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++++-- go.sum | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c5b3515f08c..1c7eff8e1be 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.5.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.0.0+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 @@ -147,6 +147,10 @@ require ( tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) -require github.com/moby/sys/capability v0.4.0 // indirect +require ( + github.com/moby/moby/api v1.52.0 // indirect + github.com/moby/moby/client v0.1.0 // indirect + github.com/moby/sys/capability v0.4.0 // indirect +) replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 82a84dd9d82..5d2dd9bb198 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,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 v28.5.2+incompatible h1:XmG99IHcBmIAoC1PPg9eLBZPlTrNUAijsHLm8PjhBlg= -github.com/docker/cli v28.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.0.0+incompatible h1:KgsN2RUFMNM8wChxryicn4p46BdQWpXOA1XLGBGPGAw= +github.com/docker/cli v29.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= @@ -202,6 +202,10 @@ 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.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= +github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8= +github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= 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= @@ -507,6 +511,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= From 77a07509a475ffe4db2358b6b906edfae855c631 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 12 Nov 2025 10:55:00 +0000 Subject: [PATCH 299/868] fix: make PORTS in nerdctl ps or nerdctl compose ps easier to view Details are described in the following issue. - https://github.com/containerd/nerdctl/issues/4338 Signed-off-by: Hayato Kiwata --- pkg/formatter/formatter.go | 53 ++++++++++++++-- pkg/formatter/formatter_test.go | 105 ++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index b72952ce68a..c5b8a8be305 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "strconv" "strings" "time" @@ -110,15 +111,59 @@ func Ellipsis(str string, maxDisplayWidth int) string { return str[:maxDisplayWidth-1] + "…" } +func formatRange(startHost, endHost, startContainer, endContainer int32) string { + if startHost == endHost && startContainer == endContainer { + return fmt.Sprintf("%d->%d", startHost, startContainer) + } + return fmt.Sprintf("%d-%d->%d-%d", startHost, endHost, startContainer, endContainer) +} + func FormatPorts(ports []cni.PortMapping) string { if len(ports) == 0 { return "" } - strs := make([]string, len(ports)) - for i, p := range ports { - strs[i] = fmt.Sprintf("%s:%d->%d/%s", p.HostIP, p.HostPort, p.ContainerPort, p.Protocol) + + type key struct { + HostIP string + Protocol string + } + grouped := make(map[key][]cni.PortMapping) + + for _, p := range ports { + k := key{HostIP: p.HostIP, Protocol: p.Protocol} + grouped[k] = append(grouped[k], p) } - return strings.Join(strs, ", ") + + var displayPorts []string + for k, pms := range grouped { + sort.Slice(pms, func(i, j int) bool { + return pms[i].HostPort < pms[j].HostPort + }) + + var i int + var ranges []string + for i = 0; i < len(pms); { + start, end := pms[i], pms[i] + for i+1 < len(pms) && + pms[i+1].HostPort == end.HostPort+1 && + pms[i+1].ContainerPort == end.ContainerPort+1 { + i++ + end = pms[i] + } + + ranges = append( + ranges, + formatRange(start.HostPort, end.HostPort, start.ContainerPort, end.ContainerPort), + ) + i++ + } + displayPorts = append( + displayPorts, + fmt.Sprintf("%s:%s/%s", k.HostIP, strings.Join(ranges, ", "), k.Protocol), + ) + } + + return strings.Join(displayPorts, ", ") } func TimeSinceInHuman(since time.Time) string { diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 6e039039e11..9da5e6fcf11 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -21,6 +21,8 @@ import ( "time" "gotest.tools/v3/assert" + + "github.com/containerd/go-cni" ) func TestTimeSinceInHuman(t *testing.T) { @@ -87,3 +89,106 @@ func TestTimeSinceInHuman(t *testing.T) { }) } } + +func TestFormatPorts(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []cni.PortMapping + expected string + }{ + { + name: "a single tcp port on localhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + }, + expected: "127.0.0.1:3000->8080/tcp", + }, + { + name: "consecutive tcp ports on localhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + { + HostPort: 3001, + ContainerPort: 8081, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + }, + expected: "127.0.0.1:3000-3001->8080-8081/tcp", + }, + { + name: "a single tcp port on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000->8080/tcp", + }, + { + name: "a single udp port on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000->8080/udp", + }, + { + name: "mixed tcp and udp with consecutive ports on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3001, + ContainerPort: 8081, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3002, + ContainerPort: 8082, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3003, + ContainerPort: 8083, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := FormatPorts(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} From 7c84728192fe122ae6834ab5b3eeaeb4c69f7ab7 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 30 Oct 2025 22:01:42 +0000 Subject: [PATCH 300/868] add global options to healthcheck command Signed-off-by: Arjun Raja Yogidas --- pkg/healthcheck/healthcheck_manager_linux.go | 42 +++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index 4cd33b77c01..aba30401842 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -62,7 +62,47 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi return fmt.Errorf("could not determine nerdctl executable path: %v", err) } - cmdOpts = append(cmdOpts, nerdctlPath, "container", "healthcheck", containerID) + cmdOpts = append( + cmdOpts, nerdctlPath, + "--namespace", cfg.Namespace, + "--address", cfg.Address, + "--data-root", cfg.DataRoot, + "--cni-path", cfg.CNIPath, + "--cni-netconfpath", cfg.CNINetConfPath, + "--cgroup-manager", cfg.CgroupManager, + "--host-gateway-ip", cfg.HostGatewayIP, + ) + + // Add boolean flags + if cfg.InsecureRegistry { + cmdOpts = append(cmdOpts, "--insecure-registry") + } + if cfg.Experimental { + cmdOpts = append(cmdOpts, "--experimental") + } + if cfg.DebugFull { + cmdOpts = append(cmdOpts, "--debug-full") + } + + // Add array flags + for _, dir := range cfg.HostsDir { + cmdOpts = append(cmdOpts, "--hosts-dir", dir) + } + for _, dir := range cfg.CDISpecDirs { + cmdOpts = append(cmdOpts, "--cdi-spec-dirs", dir) + } + + // Add userns-remap if set + if cfg.UsernsRemap != "" { + cmdOpts = append(cmdOpts, "--userns-remap", cfg.UsernsRemap) + } + + cmdOpts = append(cmdOpts, "container", "healthcheck", containerID) + + if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { + cmdOpts = append(cmdOpts, "--debug") + } + if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { cmdOpts = append(cmdOpts, "--debug") } From 1456a97bf9c55a9c74abac6f05345fce7f5eeab6 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 3 Nov 2025 18:43:37 +0000 Subject: [PATCH 301/868] add all global options to healthcheck command Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/compose/compose_start.go | 8 ++- .../container_health_check_linux_test.go | 8 +-- cmd/nerdctl/container/container_restart.go | 13 +++-- cmd/nerdctl/container/container_run.go | 2 +- cmd/nerdctl/container/container_start.go | 2 + cmd/nerdctl/container/container_unpause.go | 7 ++- pkg/api/types/container_types.go | 17 +++++- pkg/cmd/container/restart.go | 3 +- pkg/cmd/container/start.go | 2 +- pkg/cmd/container/unpause.go | 2 +- pkg/composer/pause.go | 2 +- pkg/containerutil/containerutil.go | 8 +-- pkg/healthcheck/healthcheck_manager_darwin.go | 2 +- .../healthcheck_manager_freebsd.go | 2 +- pkg/healthcheck/healthcheck_manager_linux.go | 55 ++----------------- .../healthcheck_manager_windows.go | 2 +- 16 files changed, 58 insertions(+), 77 deletions(-) diff --git a/cmd/nerdctl/compose/compose_start.go b/cmd/nerdctl/compose/compose_start.go index 0d34f04919f..08a64d2f91d 100644 --- a/cmd/nerdctl/compose/compose_start.go +++ b/cmd/nerdctl/compose/compose_start.go @@ -54,6 +54,8 @@ func startAction(cmd *cobra.Command, args []string) error { return err } + nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { return err @@ -88,7 +90,7 @@ func startAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("service %q has no container to start", svcName) } - if err := startContainers(ctx, client, containers, &globalOptions); err != nil { + if err := startContainers(ctx, client, containers, &globalOptions, nerdctlCmd, nerdctlArgs); err != nil { return err } } @@ -96,7 +98,7 @@ func startAction(cmd *cobra.Command, args []string) error { return nil } -func startContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, globalOptions *types.GlobalCommandOptions) error { +func startContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, globalOptions *types.GlobalCommandOptions, nerdctlCmd string, nerdctlArgs []string) error { eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { c := c @@ -114,7 +116,7 @@ func startContainers(ctx context.Context, client *containerd.Client, containers } // in compose, always disable attach - if err := containerutil.Start(ctx, c, false, false, client, "", "", (*config.Config)(globalOptions)); err != nil { + if err := containerutil.Start(ctx, c, false, false, client, "", "", (*config.Config)(globalOptions), nerdctlCmd, nerdctlArgs); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index cda5cec1cb7..1f9be28568e 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -309,7 +309,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { debug, _ := json.MarshalIndent(h, "", " ") t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") - assert.Equal(t, h.FailingStreak, 1) + assert.Assert(t, h.FailingStreak >= 1, "expected at least one failing streak") assert.Assert(t, len(inspect.State.Health.Log) > 0, "expected health log to have entries") last := inspect.State.Health.Log[0] assert.Equal(t, -1, last.ExitCode) @@ -348,7 +348,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) - assert.Equal(t, h.FailingStreak, 2) + assert.Assert(t, h.FailingStreak >= 1, "expected atleast one FailingStreak") }), } }, @@ -411,7 +411,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { t.Log(string(debug)) assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Unhealthy) - assert.Equal(t, h.FailingStreak, 1) + assert.Assert(t, h.FailingStreak >= 1, "expected at least one failing streak") }), } }, @@ -633,7 +633,7 @@ func TestContainerHealthCheckAdvance(t *testing.T) { assert.Assert(t, h != nil, "expected health state") assert.Equal(t, h.Status, healthcheck.Healthy) assert.Equal(t, h.FailingStreak, 0) - assert.Assert(t, len(h.Log) == 1, "expected one log entry") + assert.Assert(t, len(h.Log) >= 1, "expected at least one log entry") output := h.Log[0].Output assert.Assert(t, strings.HasSuffix(output, "[truncated]"), "expected output to be truncated with '[truncated]'") }), diff --git a/cmd/nerdctl/container/container_restart.go b/cmd/nerdctl/container/container_restart.go index cbb4b28aeda..f4ca608f451 100644 --- a/cmd/nerdctl/container/container_restart.go +++ b/cmd/nerdctl/container/container_restart.go @@ -48,6 +48,9 @@ func restartOptions(cmd *cobra.Command) (types.ContainerRestartOptions, error) { return types.ContainerRestartOptions{}, err } + // Call GlobalFlags function here + nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) + var timeout *time.Duration if cmd.Flags().Changed("time") { // Seconds to wait for stop before killing it @@ -70,10 +73,12 @@ func restartOptions(cmd *cobra.Command) (types.ContainerRestartOptions, error) { } return types.ContainerRestartOptions{ - Stdout: cmd.OutOrStdout(), - GOption: globalOptions, - Timeout: timeout, - Signal: signal, + Stdout: cmd.OutOrStdout(), + GOption: globalOptions, + Timeout: timeout, + Signal: signal, + NerdctlCmd: nerdctlCmd, + NerdctlArgs: nerdctlArgs, }, err } diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index cd35c735969..81904491256 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -457,7 +457,7 @@ func runAction(cmd *cobra.Command, args []string) error { } // Setup container healthchecks. - if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { + 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) } if err := healthcheck.StartTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { diff --git a/cmd/nerdctl/container/container_start.go b/cmd/nerdctl/container/container_start.go index a1fddadb09c..089a871d515 100644 --- a/cmd/nerdctl/container/container_start.go +++ b/cmd/nerdctl/container/container_start.go @@ -91,6 +91,8 @@ func startAction(cmd *cobra.Command, args []string) error { return err } + options.NerdctlCmd, options.NerdctlArgs = helpers.GlobalFlags(cmd) + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) if err != nil { return err diff --git a/cmd/nerdctl/container/container_unpause.go b/cmd/nerdctl/container/container_unpause.go index 24e0b43e737..cb5a9b3cb44 100644 --- a/cmd/nerdctl/container/container_unpause.go +++ b/cmd/nerdctl/container/container_unpause.go @@ -46,9 +46,12 @@ func unpauseOptions(cmd *cobra.Command) (types.ContainerUnpauseOptions, error) { if err != nil { return types.ContainerUnpauseOptions{}, err } + nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd) return types.ContainerUnpauseOptions{ - GOptions: globalOptions, - Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Stdout: cmd.OutOrStdout(), + NerdctlCmd: nerdctlCmd, + NerdctlArgs: nerdctlArgs, }, nil } diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 20462661085..ac893c1b080 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -36,6 +36,10 @@ type ContainerStartOptions struct { Checkpoint string // CheckpointDir is the directory to store checkpoints CheckpointDir string + // NerdctlCmd is the command name of nerdctl + NerdctlCmd string + // NerdctlArgs is the arguments of nerdctl + NerdctlArgs []string } // ContainerKillOptions specifies options for `nerdctl (container) kill`. @@ -329,6 +333,10 @@ type ContainerRestartOptions struct { Timeout *time.Duration // Signal to send to stop the container, before sending SIGKILL Signal string + // NerdctlCmd is the command name of nerdctl + NerdctlCmd string + // NerdctlArgs is the arguments of nerdctl + NerdctlArgs []string } // ContainerPauseOptions specifies options for `nerdctl (container) pause`. @@ -346,7 +354,14 @@ type ContainerPruneOptions struct { } // ContainerUnpauseOptions specifies options for `nerdctl (container) unpause`. -type ContainerUnpauseOptions ContainerPauseOptions +type ContainerUnpauseOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + // NerdctlCmd is the command name of nerdctl + NerdctlCmd string + // NerdctlArgs is the arguments of nerdctl + NerdctlArgs []string +} // ContainerRemoveOptions specifies options for `nerdctl (container) rm`. type ContainerRemoveOptions struct { diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go index 98c543bc67e..17a7bec99e1 100644 --- a/pkg/cmd/container/restart.go +++ b/pkg/cmd/container/restart.go @@ -48,7 +48,8 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string if err := containerutil.Stop(ctx, found.Container, options.Timeout, options.Signal); err != nil { return err } - if err := containerutil.Start(ctx, found.Container, false, false, client, "", "", (*config.Config)(&options.GOption)); err != nil { + + if err := containerutil.Start(ctx, found.Container, false, false, client, "", "", (*config.Config)(&options.GOption), options.NerdctlCmd, options.NerdctlArgs); err != nil { return err } _, err = fmt.Fprintln(options.Stdout, found.Req) diff --git a/pkg/cmd/container/start.go b/pkg/cmd/container/start.go index 604ce9465f5..7360e258c6a 100644 --- a/pkg/cmd/container/start.go +++ b/pkg/cmd/container/start.go @@ -56,7 +56,7 @@ func Start(ctx context.Context, client *containerd.Client, reqs []string, option return err } } - if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, checkpointDir, (*config.Config)(&options.GOptions)); err != nil { + if err := containerutil.Start(ctx, found.Container, options.Attach, options.Interactive, client, options.DetachKeys, checkpointDir, (*config.Config)(&options.GOptions), options.NerdctlCmd, options.NerdctlArgs); err != nil { return err } if !options.Attach { diff --git a/pkg/cmd/container/unpause.go b/pkg/cmd/container/unpause.go index fb0354efe80..10f8d48e3f5 100644 --- a/pkg/cmd/container/unpause.go +++ b/pkg/cmd/container/unpause.go @@ -36,7 +36,7 @@ func Unpause(ctx context.Context, client *containerd.Client, reqs []string, opti if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := containerutil.Unpause(ctx, client, found.Container.ID(), (*config.Config)(&options.GOptions)); err != nil { + if err := containerutil.Unpause(ctx, client, found.Container.ID(), (*config.Config)(&options.GOptions), options.NerdctlCmd, options.NerdctlArgs); err != nil { return err } diff --git a/pkg/composer/pause.go b/pkg/composer/pause.go index 7e8e331cafb..3c26d50acb7 100644 --- a/pkg/composer/pause.go +++ b/pkg/composer/pause.go @@ -83,7 +83,7 @@ func (c *Composer) Unpause(ctx context.Context, services []string, writer io.Wri for _, container := range containers { container := container eg.Go(func() error { - if err := containerutil.Unpause(ctx, c.client, container.ID(), c.config); err != nil { + if err := containerutil.Unpause(ctx, c.client, container.ID(), c.config, c.NerdctlCmd, c.NerdctlArgs); err != nil { return err } info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 32f99b5229e..875f202fbda 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -206,7 +206,7 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) } // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. -func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, checkpointDir string, cfg *config.Config) (err error) { +func Start(ctx context.Context, container containerd.Container, isAttach bool, isInteractive bool, client *containerd.Client, detachKeys string, checkpointDir string, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) (err error) { // defer the storage of start error in the dedicated label defer func() { if err != nil { @@ -304,7 +304,7 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i } // If container has health checks configured, create and start systemd timer/service files. - if err := healthcheck.CreateTimer(ctx, container, cfg); err != nil { + if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) } if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { @@ -526,7 +526,7 @@ func Pause(ctx context.Context, client *containerd.Client, id string) error { } // Unpause unpauses a container by its id. -func Unpause(ctx context.Context, client *containerd.Client, id string, cfg *config.Config) error { +func Unpause(ctx context.Context, client *containerd.Client, id string, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { container, err := client.LoadContainer(ctx, id) if err != nil { return err @@ -543,7 +543,7 @@ func Unpause(ctx context.Context, client *containerd.Client, id string, cfg *con } // Recreate healthcheck related systemd timer/service files. - if err := healthcheck.CreateTimer(ctx, container, cfg); err != nil { + if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) } if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { diff --git a/pkg/healthcheck/healthcheck_manager_darwin.go b/pkg/healthcheck/healthcheck_manager_darwin.go index b708b574281..289d0c16704 100644 --- a/pkg/healthcheck/healthcheck_manager_darwin.go +++ b/pkg/healthcheck/healthcheck_manager_darwin.go @@ -25,7 +25,7 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { return nil } diff --git a/pkg/healthcheck/healthcheck_manager_freebsd.go b/pkg/healthcheck/healthcheck_manager_freebsd.go index b708b574281..289d0c16704 100644 --- a/pkg/healthcheck/healthcheck_manager_freebsd.go +++ b/pkg/healthcheck/healthcheck_manager_freebsd.go @@ -25,7 +25,7 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { return nil } diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index aba30401842..4cb5d0c3878 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -36,7 +36,7 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { hc := extractHealthcheck(ctx, container) if hc == nil { return nil @@ -56,58 +56,11 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi // Always use health-interval for timer frequency cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s") - // Get the full path to the current nerdctl binary - nerdctlPath, err := os.Executable() - if err != nil { - return fmt.Errorf("could not determine nerdctl executable path: %v", err) - } - - cmdOpts = append( - cmdOpts, nerdctlPath, - "--namespace", cfg.Namespace, - "--address", cfg.Address, - "--data-root", cfg.DataRoot, - "--cni-path", cfg.CNIPath, - "--cni-netconfpath", cfg.CNINetConfPath, - "--cgroup-manager", cfg.CgroupManager, - "--host-gateway-ip", cfg.HostGatewayIP, - ) - - // Add boolean flags - if cfg.InsecureRegistry { - cmdOpts = append(cmdOpts, "--insecure-registry") - } - if cfg.Experimental { - cmdOpts = append(cmdOpts, "--experimental") - } - if cfg.DebugFull { - cmdOpts = append(cmdOpts, "--debug-full") - } - - // Add array flags - for _, dir := range cfg.HostsDir { - cmdOpts = append(cmdOpts, "--hosts-dir", dir) - } - for _, dir := range cfg.CDISpecDirs { - cmdOpts = append(cmdOpts, "--cdi-spec-dirs", dir) - } - - // Add userns-remap if set - if cfg.UsernsRemap != "" { - cmdOpts = append(cmdOpts, "--userns-remap", cfg.UsernsRemap) - } - + cmdOpts = append(cmdOpts, nerdctlCmd) + cmdOpts = append(cmdOpts, nerdctlArgs...) cmdOpts = append(cmdOpts, "container", "healthcheck", containerID) - if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { - cmdOpts = append(cmdOpts, "--debug") - } - - if log.G(ctx).Logger.IsLevelEnabled(log.DebugLevel) { - cmdOpts = append(cmdOpts, "--debug") - } - - log.G(ctx).Debugf("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) + log.G(ctx).Infof("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) run := exec.Command("systemd-run", cmdOpts...) if out, err := run.CombinedOutput(); err != nil { return fmt.Errorf("systemd-run failed: %w\noutput: %s", err, strings.TrimSpace(string(out))) diff --git a/pkg/healthcheck/healthcheck_manager_windows.go b/pkg/healthcheck/healthcheck_manager_windows.go index 1da386fe2bc..e5fa58a4a08 100644 --- a/pkg/healthcheck/healthcheck_manager_windows.go +++ b/pkg/healthcheck/healthcheck_manager_windows.go @@ -25,7 +25,7 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { return nil } From 88a8373ec5f64827099803538ffe6a9d36bcf21b Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 4 Nov 2025 01:11:13 +0000 Subject: [PATCH 302/868] add config parsing to globalFlags and testing Signed-off-by: Arjun Raja Yogidas --- .../container_health_check_linux_test.go | 102 ++++++++++++++++++ cmd/nerdctl/helpers/cobra.go | 6 +- pkg/healthcheck/healthcheck_manager_linux.go | 2 +- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index 1f9be28568e..1217045a3ed 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/healthcheck" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -931,6 +932,107 @@ func TestHealthCheck_SystemdIntegration_Basic(t *testing.T) { testCase.Run(t) } +func TestHealthCheck_GlobalFlags(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + // Skip systemd tests in rootless environment to bypass dbus permission issues + if rootlessutil.IsRootless() { + t.Skip("systemd healthcheck tests are skipped in rootless environment") + } + + testCase.SubTests = []*test.Case{ + { + Description: "Healthcheck works with custom namespace flag", + Setup: func(data test.Data, helpers test.Helpers) { + // Create container in custom namespace with healthcheck + helpers.Ensure("--namespace=healthcheck-test", "run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "2s", + testutil.CommonImage, "sleep", "30") + // Wait a bit to ensure container is running (can't use EnsureContainerStarted with custom namespace) + time.Sleep(1 * time.Second) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("--namespace=healthcheck-test", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Wait a bit for healthcheck to run + time.Sleep(3 * time.Second) + // Verify container is accessible in the custom namespace + return helpers.Command("--namespace=healthcheck-test", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + var inspectResults []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &inspectResults) + assert.NilError(t, err, "failed to parse inspect output") + assert.Assert(t, len(inspectResults) > 0, "expected at least one container in inspect results") + + inspect := inspectResults[0] + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state to be present") + assert.Assert(t, h.Status == healthcheck.Healthy || h.Status == healthcheck.Starting, + "expected health status to be healthy or starting, got: %s", h.Status) + assert.Assert(t, len(h.Log) > 0, "expected at least one health check log entry") + }, + } + }, + }, + { + Description: "Healthcheck works correctly with namespace after container restart", + Setup: func(data test.Data, helpers test.Helpers) { + // Create container in custom namespace + helpers.Ensure("--namespace=restart-test", "run", "-d", "--name", data.Identifier(), + "--health-cmd", "echo healthy", + "--health-interval", "2s", + testutil.CommonImage, "sleep", "60") + // Wait a bit to ensure container is running (can't use EnsureContainerStarted with custom namespace) + time.Sleep(1 * time.Second) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("--namespace=restart-test", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Wait for initial healthcheck + time.Sleep(3 * time.Second) + + // Stop and restart the container + helpers.Ensure("--namespace=restart-test", "stop", data.Identifier()) + helpers.Ensure("--namespace=restart-test", "start", data.Identifier()) + // Wait a bit to ensure container is running after restart + time.Sleep(1 * time.Second) + + // Wait for healthcheck to run after restart + time.Sleep(3 * time.Second) + + return helpers.Command("--namespace=restart-test", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + // Parse the inspect JSON output directly since we're in a custom namespace + var inspectResults []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &inspectResults) + assert.NilError(t, err, "failed to parse inspect output") + assert.Assert(t, len(inspectResults) > 0, "expected at least one container in inspect results") + + inspect := inspectResults[0] + h := inspect.State.Health + assert.Assert(t, h != nil, "expected health state after restart") + assert.Assert(t, h.Status == healthcheck.Healthy || h.Status == healthcheck.Starting, + "expected health status to be healthy or starting after restart, got: %s", h.Status) + assert.Assert(t, len(h.Log) > 0, "expected health check logs after restart") + }, + } + }, + }, + } + testCase.Run(t) +} + func TestHealthCheck_SystemdIntegration_Advanced(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) diff --git a/cmd/nerdctl/helpers/cobra.go b/cmd/nerdctl/helpers/cobra.go index 58eb9ac5f51..8ee5baeb260 100644 --- a/cmd/nerdctl/helpers/cobra.go +++ b/cmd/nerdctl/helpers/cobra.go @@ -156,7 +156,11 @@ func GlobalFlags(cmd *cobra.Command) (string, []string) { flagSet.VisitAll(func(f *pflag.Flag) { key := f.Name val := f.Value.String() - if f.Changed { + // Include flag if: + // 1. It was explicitly changed via CLI (highest priority), OR + // 2. It has a non-default value (from TOML config) + // This ensures both CLI flags and TOML config values are propagated + if f.Changed || (val != f.DefValue && val != "") { args = append(args, "--"+key+"="+val) } }) diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index 4cb5d0c3878..ee618b5cb9f 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -60,7 +60,7 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi cmdOpts = append(cmdOpts, nerdctlArgs...) cmdOpts = append(cmdOpts, "container", "healthcheck", containerID) - log.G(ctx).Infof("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) + log.G(ctx).Debugf("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) run := exec.Command("systemd-run", cmdOpts...) if out, err := run.CombinedOutput(); err != nil { return fmt.Errorf("systemd-run failed: %w\noutput: %s", err, strings.TrimSpace(string(out))) From 89fc0354dfe07efab66f2ca58bfacd303ecb54c8 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 11 Nov 2025 11:06:23 +0000 Subject: [PATCH 303/868] add env variable parsing for healthcheck command Signed-off-by: Arjun Raja Yogidas --- pkg/healthcheck/healthcheck_manager_linux.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index ee618b5cb9f..92b49bd0cc4 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -48,11 +48,20 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi containerID := container.ID() log.G(ctx).Debugf("Creating healthcheck timer unit: %s", containerID) + // Set all environment variables so that they are available for the nerdctl commands run via the systemd service file cmdOpts := []string{} if path := os.Getenv("PATH"); path != "" { cmdOpts = append(cmdOpts, "--setenv=PATH="+path) } + if nerdctlToml := os.Getenv("NERDCTL_TOML"); nerdctlToml != "" { + cmdOpts = append(cmdOpts, "--setenv=NERDCTL_TOML="+nerdctlToml) + } + + if buildKitHost := os.Getenv("BUILDKIT_HOST"); buildKitHost != "" { + cmdOpts = append(cmdOpts, "--setenv=BUILDKIT_HOST="+buildKitHost) + } + // Always use health-interval for timer frequency cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s") From 5bf7e0acec39d501fd3e901735580e86b574c32e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:53:30 +0000 Subject: [PATCH 304/868] build(deps): bump the golang-x group across 1 directory with 6 updates Bumps the golang-x group with 2 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/crypto` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.44.0) Updates `golang.org/x/net` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/net/compare/v0.46.0...v0.47.0) Updates `golang.org/x/sync` from 0.17.0 to 0.18.0 - [Commits](https://github.com/golang/sync/compare/v0.17.0...v0.18.0) Updates `golang.org/x/sys` from 0.37.0 to 0.38.0 - [Commits](https://github.com/golang/sys/compare/v0.37.0...v0.38.0) Updates `golang.org/x/term` from 0.36.0 to 0.37.0 - [Commits](https://github.com/golang/term/compare/v0.36.0...v0.37.0) Updates `golang.org/x/text` from 0.30.0 to 0.31.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.31.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 1c7eff8e1be..42a32dd2f07 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.43.0 - golang.org/x/net v0.46.0 - golang.org/x/sync v0.17.0 //gomodjail:unconfined - golang.org/x/sys v0.37.0 //gomodjail:unconfined - golang.org/x/term v0.36.0 //gomodjail:unconfined - golang.org/x/text v0.30.0 + golang.org/x/crypto v0.44.0 + golang.org/x/net v0.47.0 + golang.org/x/sync v0.18.0 //gomodjail:unconfined + golang.org/x/sys v0.38.0 //gomodjail:unconfined + golang.org/x/term v0.37.0 //gomodjail:unconfined + golang.org/x/text v0.31.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index 5d2dd9bb198..0cd05281b05 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -390,8 +390,8 @@ 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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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= @@ -404,8 +404,8 @@ 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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= @@ -428,8 +428,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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= @@ -439,8 +439,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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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= @@ -450,8 +450,8 @@ 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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= @@ -464,8 +464,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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= From 47e7883da0e11673f1d136fdc0fd30cd33ff6bb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:03:13 +0000 Subject: [PATCH 305/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.0.0+incompatible to 29.0.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.0.0...v29.0.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.0.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 42a32dd2f07..093845fa33b 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.0.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.0.2+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 0cd05281b05..5724447410c 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,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.0.0+incompatible h1:KgsN2RUFMNM8wChxryicn4p46BdQWpXOA1XLGBGPGAw= -github.com/docker/cli v29.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.0.2+incompatible h1:iLuKy2GWOSLXGp8feLYBJQVDv7m/8xoofz6lPq41x6A= +github.com/docker/cli v29.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 27c2d6dff36bdd7f7ad38c057673d6112f277fa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:03:52 +0000 Subject: [PATCH 306/868] build(deps): bump actions/checkout from 5.0.0 to 5.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...93cb6efe18208431cddfb8368fd83d5badbf9bfd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index eb263c6c256..6b966d6beb0 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 29556e259de..f0c52f9ce08 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 65c8bbd3374..9e5b8a00d63 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 509f2bbf950..76baaa84b30 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 6a1309918bc..61a8f4d721f 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 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 9a025c1ac27..44b82b38421 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index a74ba80137e..b3c86cabb01 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 40e2dc02a66..c2dea62fe98 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -71,7 +71,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 7d0c2f922ea..1084c8db497 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -30,7 +30,7 @@ jobs: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 6f6f97be75e..f61bc8b38da 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index a7723d88898..8545007240c 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 921d7736afa..67a4d62c697 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index a17e19c4c4c..dd069f4b50d 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -46,7 +46,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 1 - name: "Run" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 16cc728e000..5a3abb08f81 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -32,7 +32,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 100 - if: ${{ matrix.canary }} From c96692706ab1fb4d0dea21fcf29ab9ed41eefd38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:02:06 +0000 Subject: [PATCH 307/868] build(deps): bump golang.org/x/crypto in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.44.0 to 0.45.0 - [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 093845fa33b..99138c012a7 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.44.0 + golang.org/x/crypto v0.45.0 golang.org/x/net v0.47.0 golang.org/x/sync v0.18.0 //gomodjail:unconfined golang.org/x/sys v0.38.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 5724447410c..042742cf068 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= From 01762bad6eb2af6bfc39b8b559013eb512b4f259 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:02:07 +0000 Subject: [PATCH 308/868] build(deps): bump actions/checkout from 5.0.1 to 6.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93cb6efe18208431cddfb8368fd83d5badbf9bfd...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 6b966d6beb0..c171280e596 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index f0c52f9ce08..14f61411f8c 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 9e5b8a00d63..e24127f2a2a 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 76baaa84b30..ff00d9f9d02 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 61a8f4d721f..115ec117e22 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.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 44b82b38421..ee4e21daf71 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index b3c86cabb01..e82a96e8eff 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index c2dea62fe98..210e1a7cf0b 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -71,7 +71,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 1084c8db497..a0f9ed58000 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -30,7 +30,7 @@ jobs: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index f61bc8b38da..2840a4e6527 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 8545007240c..7bf650985a5 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67a4d62c697..0ef58898e9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index dd069f4b50d..cb74fb27401 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -46,7 +46,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 1 - name: "Run" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 5a3abb08f81..5bb881325dd 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -32,7 +32,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 100 - if: ${{ matrix.canary }} From c018be9f3a818d580c69ff7fa9dce6ff9d476c31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:02:16 +0000 Subject: [PATCH 309/868] build(deps): bump actions/setup-go from 6.0.0 to 6.1.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/44694675825211faa026b3c33043df3e48a5fa00...4dc6199c7b1a012772edbd06daecab0f50c9053c) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index f0c52f9ce08..32de227b741 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 9e5b8a00d63..340a765bda0 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 61a8f4d721f..5f708b96a77 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index c2dea62fe98..9dd639ce8bc 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 8545007240c..03d58ec7442 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67a4d62c697..314bba2add6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: "Install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 5a3abb08f81..a5ce2f62da4 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From ec16a4d5546b4e38fb930ff4a41aff335fb7f1b4 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sun, 23 Nov 2025 15:53:02 +0900 Subject: [PATCH 310/868] fix: split else-if to avoid identical-branches lint error Signed-off-by: Park jungtae --- pkg/cmd/container/create_userns_opts_linux.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/container/create_userns_opts_linux.go b/pkg/cmd/container/create_userns_opts_linux.go index 13f9275801c..1702c8c8d45 100644 --- a/pkg/cmd/container/create_userns_opts_linux.go +++ b/pkg/cmd/container/create_userns_opts_linux.go @@ -324,7 +324,8 @@ func getUserAndGroup(spec string) (user.User, user.Group, error) { parts := strings.Split(spec, ":") if len(parts) > 2 { return user.User{}, user.Group{}, fmt.Errorf("invalid identity mapping format: %s", spec) - } else if len(parts) == 2 && (parts[0] == "" || parts[1] == "") { + } + if len(parts) == 2 && (parts[0] == "" || parts[1] == "") { return user.User{}, user.Group{}, fmt.Errorf("invalid identity mapping format: %s", spec) } From 452c62b1d33c0db3d85423ede7947df718116818 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:10:08 +0000 Subject: [PATCH 311/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.0.2+incompatible to 29.0.3+incompatible - [Commits](https://github.com/docker/cli/compare/v29.0.2...v29.0.3) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.0.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 99138c012a7..d218a932772 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.0.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.0.3+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 042742cf068..a36d8718464 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,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.0.2+incompatible h1:iLuKy2GWOSLXGp8feLYBJQVDv7m/8xoofz6lPq41x6A= -github.com/docker/cli v29.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= +github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From accc2f38793f5f1bfb53c9f5b3b96e2a047ccdc4 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Thu, 27 Nov 2025 05:38:22 +0000 Subject: [PATCH 312/868] Fix SOCI image convertion regression for 0.12.0 release Signed-off-by: Henry Wang --- pkg/snapshotterutil/sociutil.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 82e8773ce66..4f55c917eb1 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -117,6 +117,13 @@ func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef strin sociCmd.Args = append(sociCmd.Args, "convert") + // The following option temporarily fix the image conversion regression in SOCI v0.12.0 + // https://github.com/awslabs/soci-snapshotter/issues/1789 + // TODO: remove after the bug is fixed in SOCI + if err := CheckSociVersion("0.12.0"); err == nil { + sociCmd.Args = append(sociCmd.Args, "--force") + } + if sOpts.AllPlatforms { sociCmd.Args = append(sociCmd.Args, "--all-platforms") } else if len(sOpts.Platforms) > 0 { From 730ac4998c2d92515b798d1ad927308ef2a92a72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:53:33 +0000 Subject: [PATCH 313/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.0.3+incompatible to 29.0.4+incompatible - [Commits](https://github.com/docker/cli/compare/v29.0.3...v29.0.4) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.0.4+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d218a932772..2527845d7a3 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.0.3+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.0+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index a36d8718464..afc89f5247c 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,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.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= -github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.0+incompatible h1:Ru/t9JgWbrwr8pIqh8XULT3CdoFMsg9CMXtaE+4Zymk= +github.com/docker/cli v29.1.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From b2c5d8faed1c993e330b565a9b2ab3cea1524b92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:01:40 +0000 Subject: [PATCH 314/868] build(deps): bump docker/metadata-action from 5.9.0 to 5.10.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.9.0 to 5.10.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/318604b99e75e41977312d83839a89be02ca4893...c299e40c65443455700f0fdfc63efafe5b349051) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 5.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index c171280e596..f60446202e3 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -54,7 +54,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From c40fda58c6abde9540a94547566e7a372bd19f9a Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 5 Nov 2025 22:41:13 +0800 Subject: [PATCH 315/868] Refactor image management to use transfer service Switch image operations to the transfer API with structured progress reporting and improved TLS/HTTP fallback behavior. Introduce shared helpers for credentials, error classification, progress rendering, and transfer-based import/tag/save flows, updating tests to reflect the new UX. Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/container/container_run_test.go | 2 +- cmd/nerdctl/image/image_load_test.go | 5 +- pkg/cmd/image/import.go | 318 ++++++++++++------ pkg/cmd/image/push.go | 106 +++--- pkg/cmd/image/save.go | 105 ++++-- pkg/cmd/image/tag.go | 58 +--- pkg/errutil/errors_check.go | 15 + .../dockerconfigresolver.go | 73 ++++ pkg/imgutil/imgutil.go | 35 +- pkg/imgutil/load/load.go | 118 +++---- pkg/imgutil/transfer.go | 232 +++++++++++++ pkg/transferutil/progress.go | 231 +++++++++++++ 12 files changed, 958 insertions(+), 340 deletions(-) create mode 100644 pkg/imgutil/transfer.go create mode 100644 pkg/transferutil/progress.go diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index e3d50940f8b..ea424a2c889 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -786,7 +786,7 @@ func TestRunFromOCIArchive(t *testing.T) { tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() - base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(fmt.Sprintf("Loaded image: %s", tag), sentinel) + base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(tag, sentinel) } func TestRunDomainname(t *testing.T) { diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 2618b81c64f..90e8c970d1a 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -17,7 +17,6 @@ package image import ( - "fmt" "os" "path/filepath" "strings" @@ -61,7 +60,7 @@ func TestLoadStdinFromPipe(t *testing.T) { identifier := data.Identifier() return &test.Expected{ Output: expect.All( - expect.Contains(fmt.Sprintf("Loaded image: %s:latest", identifier)), + expect.Contains(identifier), func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(helpers.Capture("images"), identifier)) }, @@ -107,7 +106,7 @@ func TestLoadQuiet(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(fmt.Sprintf("Loaded image: %s:latest", data.Identifier())), + expect.Contains(data.Identifier()), expect.DoesNotContain("Loading layer"), ), } diff --git a/pkg/cmd/image/import.go b/pkg/cmd/image/import.go index a8e61eb6944..432d5665a90 100644 --- a/pkg/cmd/image/import.go +++ b/pkg/cmd/image/import.go @@ -17,6 +17,7 @@ package image import ( + "archive/tar" "bytes" "compress/gzip" "context" @@ -25,73 +26,190 @@ import ( "encoding/json" "fmt" "io" + "os" + pathpkg "path" "time" "github.com/opencontainers/go-digest" - "github.com/opencontainers/image-spec/identity" - "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/content" - "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/leases" + "github.com/containerd/containerd/v2/core/transfer" + tarchive "github.com/containerd/containerd/v2/core/transfer/archive" + transferimage "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/containerd/v2/pkg/archive/compression" "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/transferutil" ) func Import(ctx context.Context, client *containerd.Client, options types.ImageImportOptions) (string, error) { - img, err := importRootfs(ctx, client, options.GOptions.Snapshotter, options) + prefix := options.Reference + if prefix == "" { + prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")) + } + + parsed, err := referenceutil.Parse(prefix) if err != nil { return "", err } - return img.Name, nil + imageName := parsed.String() + + platUnpack := platforms.DefaultSpec() + var opts []transferimage.StoreOpt + if options.Platform != "" { + p, err := platforms.Parse(options.Platform) + if err != nil { + return "", err + } + platUnpack = p + opts = append(opts, transferimage.WithPlatforms(platUnpack)) + } + + opts = append(opts, transferimage.WithUnpack(platUnpack, options.GOptions.Snapshotter)) + opts = append(opts, transferimage.WithDigestRef(imageName, true, true)) + + var r io.ReadCloser + if rc, ok := options.Stdin.(io.ReadCloser); ok { + r = rc + } else { + r = io.NopCloser(options.Stdin) + } + + converted, cleanup, err := ensureOCIArchive(ctx, client, r, options, prefix) + if err != nil { + return "", err + } + defer cleanup() + + iis := tarchive.NewImageImportStream(converted, "") + is := transferimage.NewStore("", opts...) + + pf, done := transferutil.ProgressHandler(ctx, os.Stderr) + defer done() + + if err := client.Transfer(ctx, iis, is, transfer.WithProgress(pf)); err != nil { + return "", err + } + + return imageName, nil +} + +func ensureOCIArchive(ctx context.Context, client *containerd.Client, r io.ReadCloser, options types.ImageImportOptions, prefix string) (io.ReadCloser, func(), error) { + buf := &bytes.Buffer{} + tee := io.TeeReader(r, buf) + + isStandardArchive, err := detectStandardImageArchive(tee) + if err != nil { + return nil, func() {}, err + } + + combined := io.NopCloser(io.MultiReader(buf, r)) + if isStandardArchive { + return combined, func() { r.Close() }, nil + } + + converted, err := convertRootfsToOCIArchive(ctx, client, combined, options, prefix) + if err != nil { + r.Close() + return nil, func() {}, err + } + + cleanup := func() { + r.Close() + if converted != nil { + converted.Close() + } + } + + return converted, cleanup, nil } -func importRootfs(ctx context.Context, client *containerd.Client, snapshotter string, options types.ImageImportOptions) (images.Image, error) { - var zero images.Image +func detectStandardImageArchive(r io.Reader) (bool, error) { + tr := tar.NewReader(r) + const maxHeadersToCheck = 10 + + for i := 0; i < maxHeadersToCheck; i++ { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return false, err + } + + name := pathpkg.Clean(hdr.Name) + if name == "manifest.json" || name == ocispec.ImageLayoutFile { + return true, nil + } + } + return false, nil +} + +func convertRootfsToOCIArchive(ctx context.Context, client *containerd.Client, r io.ReadCloser, options types.ImageImportOptions, prefix string) (io.ReadCloser, error) { + defer r.Close() ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { - return zero, err + return nil, err } defer done(ctx) - if options.Stdin == nil { - return zero, fmt.Errorf("no input stream provided") - } - decomp, err := compression.DecompressStream(options.Stdin) + decomp, err := compression.DecompressStream(r) if err != nil { - return zero, err + return nil, err } defer decomp.Close() cs := client.ContentStore() - - ref := randomRef("import-rootfs-") + ref := randomRef("import-layer-") w, err := content.OpenWriter(ctx, cs, content.WithRef(ref)) if err != nil { - return zero, err + return nil, err } defer w.Close() + if err := w.Truncate(0); err != nil { - return zero, err + return nil, err + } + + layerDigest, diffID, layerSize, err := compressAndWriteLayer(ctx, w, decomp) + if err != nil { + return nil, err } + imgConfig, configDigest, err := buildImageConfig(diffID, options) + if err != nil { + return nil, err + } + + layerContent, err := readLayerContent(ctx, cs, layerDigest, layerSize) + if err != nil { + return nil, err + } + + return buildDockerArchive(imgConfig, configDigest, layerContent, layerDigest, prefix) +} + +func compressAndWriteLayer(ctx context.Context, w content.Writer, r io.Reader) (digest.Digest, digest.Digest, int64, error) { digester := digest.Canonical.Digester() - tee := io.TeeReader(decomp, digester.Hash()) + tee := io.TeeReader(r, digester.Hash()) pr, pw := io.Pipe() gz := gzip.NewWriter(pw) + doneCh := make(chan error, 1) go func() { - _, err := io.Copy(gz, tee) - if err != nil { - doneCh <- err + defer func() { _ = gz.Close() + }() + + if _, err := io.Copy(gz, tee); err != nil { + doneCh <- err _ = pw.CloseWithError(err) return } @@ -105,10 +223,10 @@ func importRootfs(ctx context.Context, client *containerd.Client, snapshotter st n, err := io.Copy(w, pr) if err != nil { - return zero, err + return "", "", 0, err } if err := <-doneCh; err != nil { - return zero, err + return "", "", 0, err } diffID := digester.Digest() @@ -116,21 +234,18 @@ func importRootfs(ctx context.Context, client *containerd.Client, snapshotter st "containerd.io/uncompressed": diffID.String(), } if err := w.Commit(ctx, n, "", content.WithLabels(labels)); err != nil && !errdefs.IsAlreadyExists(err) { - return zero, err - } - layerDesc := ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2LayerGzip, - Digest: w.Digest(), - Size: n, + return "", "", 0, err } + return w.Digest(), diffID, n, nil +} + +func buildImageConfig(diffID digest.Digest, options types.ImageImportOptions) ([]byte, digest.Digest, error) { ociplat := platforms.DefaultSpec() if options.Platform != "" { - p, err := platforms.Parse(options.Platform) - if err != nil { - return zero, err + if p, err := platforms.Parse(options.Platform); err == nil { + ociplat = p } - ociplat = p } created := time.Now().UTC() @@ -153,100 +268,85 @@ func importRootfs(ctx context.Context, client *containerd.Client, snapshotter st }}, } - manifestDesc, _, err := writeConfigAndManifest(ctx, cs, snapshotter, imgConfig, []ocispec.Descriptor{layerDesc}) + configJSON, err := json.Marshal(imgConfig) if err != nil { - return zero, err + return nil, "", err } + return configJSON, digest.FromBytes(configJSON), nil +} - storedName := options.Reference - if storedName == "" { - storedName = manifestDesc.Digest.String() - } else if refParsed, err := referenceutil.Parse(storedName); err == nil { - if refParsed.ExplicitTag == "" { - storedName = refParsed.FamiliarName() + ":latest" - } - if p2, err := referenceutil.Parse(storedName); err == nil { - storedName = p2.String() - } +func readLayerContent(ctx context.Context, cs content.Store, layerDigest digest.Digest, size int64) ([]byte, error) { + ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: layerDigest, Size: size}) + if err != nil { + return nil, err } - name := storedName + defer ra.Close() - img := images.Image{ - Name: name, - Target: manifestDesc, - CreatedAt: time.Now(), - } - if _, err := client.ImageService().Update(ctx, img); err != nil { - if !errdefs.IsNotFound(err) { - return zero, err - } - if _, err := client.ImageService().Create(ctx, img); err != nil { - return zero, err - } + layerContent := make([]byte, size) + if _, err := ra.ReadAt(layerContent, 0); err != nil { + return nil, err } + return layerContent, nil +} + +func buildDockerArchive(configJSON []byte, configDigest digest.Digest, layerContent []byte, layerDigest digest.Digest, prefix string) (io.ReadCloser, error) { + layerFileName := layerDigest.Encoded() + ".tar.gz" + configFileName := configDigest.Encoded() + ".json" - cimg := containerd.NewImage(client, img) - if err := cimg.Unpack(ctx, snapshotter); err != nil { - return zero, err + var repoTags []string + if parsed, err := referenceutil.Parse(prefix); err == nil && parsed.String() != "" { + repoTags = []string{parsed.String()} } - return img, nil -} -func randomRef(prefix string) string { - var b [6]byte - _, _ = rand.Read(b[:]) - return prefix + base64.RawURLEncoding.EncodeToString(b[:]) -} + dockerManifest := []struct { + Config string `json:"Config"` + RepoTags []string `json:"RepoTags,omitempty"` + Layers []string `json:"Layers"` + }{{ + Config: configFileName, + RepoTags: repoTags, + Layers: []string{layerFileName}, + }} -func writeConfigAndManifest(ctx context.Context, cs content.Store, snapshotter string, config ocispec.Image, layers []ocispec.Descriptor) (ocispec.Descriptor, digest.Digest, error) { - configJSON, err := json.Marshal(config) + dockerManifestJSON, err := json.Marshal(dockerManifest) if err != nil { - return ocispec.Descriptor{}, "", err - } - configDesc := ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2Config, - Digest: digest.FromBytes(configJSON), - Size: int64(len(configJSON)), + return nil, err } - gcLabel := map[string]string{} - if len(config.RootFS.DiffIDs) > 0 && snapshotter != "" { - gcLabel[fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snapshotter)] = identity.ChainID(config.RootFS.DiffIDs).String() - } - if err := content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(configJSON), configDesc, content.WithLabels(gcLabel)); err != nil && !errdefs.IsAlreadyExists(err) { - return ocispec.Descriptor{}, "", err - } + buf := &bytes.Buffer{} + tw := tar.NewWriter(buf) - manifest := struct { - MediaType string `json:"mediaType,omitempty"` - ocispec.Manifest + files := []struct { + name string + content []byte }{ - MediaType: images.MediaTypeDockerSchema2Manifest, - Manifest: ocispec.Manifest{ - Versioned: specs.Versioned{SchemaVersion: 2}, - Config: configDesc, - Layers: layers, - }, - } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - return ocispec.Descriptor{}, "", err - } - manifestDesc := ocispec.Descriptor{ - MediaType: images.MediaTypeDockerSchema2Manifest, - Digest: digest.FromBytes(manifestJSON), - Size: int64(len(manifestJSON)), + {"manifest.json", dockerManifestJSON}, + {configFileName, configJSON}, + {layerFileName, layerContent}, } - refLabels := map[string]string{ - "containerd.io/gc.ref.content.0": configDesc.Digest.String(), - } - for i, l := range layers { - refLabels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String() + for _, f := range files { + if err := tw.WriteHeader(&tar.Header{ + Name: f.name, + Mode: 0644, + Size: int64(len(f.content)), + }); err != nil { + return nil, err + } + if _, err := tw.Write(f.content); err != nil { + return nil, err + } } - if err := content.WriteBlob(ctx, cs, manifestDesc.Digest.String(), bytes.NewReader(manifestJSON), manifestDesc, content.WithLabels(refLabels)); err != nil && !errdefs.IsAlreadyExists(err) { - return ocispec.Descriptor{}, "", err + + if err := tw.Close(); err != nil { + return nil, err } - return manifestDesc, configDesc.Digest, nil + return io.NopCloser(buf), nil +} + +func randomRef(prefix string) string { + var b [6]byte + _, _ = rand.Read(b[:]) + return prefix + base64.RawURLEncoding.EncodeToString(b[:]) } diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go index 8731b0cfc94..8aa6fbd05a3 100644 --- a/pkg/cmd/image/push.go +++ b/pkg/cmd/image/push.go @@ -37,12 +37,14 @@ import ( dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config" "github.com/containerd/containerd/v2/pkg/reference" "github.com/containerd/log" + "github.com/containerd/platforms" "github.com/containerd/stargz-snapshotter/estargz" "github.com/containerd/stargz-snapshotter/estargz/zstdchunked" estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/errutil" + "github.com/containerd/nerdctl/v2/pkg/imgutil" nerdconverter "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/imgutil/push" @@ -110,7 +112,6 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options return err } ref := parsedReference.String() - refDomain := parsedReference.Domain platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platforms) if err != nil { @@ -146,53 +147,14 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options defer client.ImageService().Delete(ctx, esgzImg.Name, images.SynchronousDelete()) log.G(ctx).Infof("pushing as an eStargz image (%s, %s)", esgzImg.Target.MediaType, esgzImg.Target.Digest) } - - // In order to push images where most layers are the same but the - // repository name is different, it is necessary to refresh the - // PushTracker. Otherwise, the MANIFEST_BLOB_UNKNOWN error will occur due - // to the registry not creating the corresponding layer link file, - // resulting in the failure of the entire image push. - pushTracker := docker.NewInMemoryTracker() - - pushFunc := func(r remotes.Resolver) error { - return push.Push(ctx, client, r, pushTracker, options.Stdout, pushRef, ref, platMC, options.AllowNondistributableArtifacts, options.Quiet) - } - - var dOpts []dockerconfigresolver.Opt - if options.GOptions.InsecureRegistry { - log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain) - dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) - } - dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) - - ho, err := dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...) - if err != nil { - return err - } - - resolverOpts := docker.ResolverOptions{ - Tracker: pushTracker, - Hosts: dockerconfig.ConfigureHosts(ctx, *ho), - } - - resolver := docker.NewResolver(resolverOpts) - if err = pushFunc(resolver); err != nil { - // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused" - if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { + if !options.AllowNondistributableArtifacts { + if err := pushImageWithLocal(ctx, client, parsedReference, pushRef, ref, options, platMC); err != nil { return err } - if options.GOptions.InsecureRegistry { - log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) - dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) - resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...) - if err != nil { - return err - } - return pushFunc(resolver) + } else { + if err := imgutil.PushImageWithTransfer(ctx, client, parsedReference, pushRef, ref, options); err != nil { + return err } - log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) - log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") - return err } img, err := client.ImageService().Get(ctx, pushRef) @@ -263,3 +225,57 @@ func isReusableESGZ(ctx context.Context, cs content.Store, desc ocispec.Descript } return true } + +func pushImageWithLocal(ctx context.Context, client *containerd.Client, parsedReference *referenceutil.ImageReference, pushRef, rawRef string, options types.ImagePushOptions, platMC platforms.MatchComparer) error { + ref := parsedReference.String() + refDomain := parsedReference.Domain + + // In order to push images where most layers are the same but the + // repository name is different, it is necessary to refresh the + // PushTracker. Otherwise, the MANIFEST_BLOB_UNKNOWN error will occur due + // to the registry not creating the corresponding layer link file, + // resulting in the failure of the entire image push. + pushTracker := docker.NewInMemoryTracker() + + pushFunc := func(r remotes.Resolver) error { + return push.Push(ctx, client, r, pushTracker, options.Stdout, pushRef, ref, platMC, options.AllowNondistributableArtifacts, options.Quiet) + } + + var dOpts []dockerconfigresolver.Opt + if options.GOptions.InsecureRegistry { + log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) + + ho, err := dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...) + if err != nil { + return err + } + + resolverOpts := docker.ResolverOptions{ + Tracker: pushTracker, + Hosts: dockerconfig.ConfigureHosts(ctx, *ho), + } + + resolver := docker.NewResolver(resolverOpts) + if err = pushFunc(resolver); err != nil { + // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused" + if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { + return err + } + if options.GOptions.InsecureRegistry { + log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) + resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...) + if err != nil { + return err + } + return pushFunc(resolver) + } + log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) + log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") + return err + } + return nil +} diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index 0a499b3f135..a35a83a97f5 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -19,55 +19,104 @@ package image import ( "context" "fmt" + "io" + "os" + + "github.com/distribution/reference" + "github.com/opencontainers/go-digest" containerd "github.com/containerd/containerd/v2/client" - "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/containerd/v2/core/transfer" + tarchive "github.com/containerd/containerd/v2/core/transfer/archive" + transferimage "github.com/containerd/containerd/v2/core/transfer/image" + "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" - "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/strutil" + "github.com/containerd/nerdctl/v2/pkg/transferutil" ) // Save exports `images` to a `io.Writer` (e.g., a file writer, or os.Stdout) specified by `options.Stdout`. -func Save(ctx context.Context, client *containerd.Client, images []string, options types.ImageSaveOptions, exportOpts ...archive.ExportOpt) error { +func Save(ctx context.Context, client *containerd.Client, images []string, options types.ImageSaveOptions) error { images = strutil.DedupeStrSlice(images) + var exportOpts []tarchive.ExportOpt + + if len(options.Platform) > 0 { + for _, ps := range options.Platform { + p, err := platforms.Parse(ps) + if err != nil { + return fmt.Errorf("invalid platform %q: %w", ps, err) + } + exportOpts = append(exportOpts, tarchive.WithPlatform(p)) + } + } + if options.AllPlatforms { + exportOpts = append(exportOpts, tarchive.WithAllPlatforms) + } + platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platform) if err != nil { return err } - exportOpts = append(exportOpts, archive.WithPlatform(platMC)) - imageStore := client.ImageService() + imageService := client.ImageService() + var storeOpts []transferimage.StoreOpt + for _, img := range images { + var imageRef string - savedImages := make(map[string]struct{}) - walker := &imagewalker.ImageWalker{ - Client: client, - OnFound: func(ctx context.Context, found imagewalker.Found) error { - if found.UniqueImages > 1 { - return fmt.Errorf("ambiguous digest ID: multiple IDs found with provided prefix %s", found.Req) + var dgst digest.Digest + var err error + if dgst, err = digest.Parse(img); err != nil { + if dgst, err = digest.Parse("sha256:" + img); err != nil { + named, err := reference.ParseNormalizedNamed(img) + if err != nil { + return fmt.Errorf("invalid image name %q: %w", img, err) + } + imageRef = reference.TagNameOnly(named).String() + err = EnsureAllContent(ctx, client, imageRef, platMC, options.GOptions) + if err != nil { + return err + } + storeOpts = append(storeOpts, transferimage.WithExtraReference(imageRef)) + continue } + } - // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 - err = EnsureAllContent(ctx, client, found.Image.Name, platMC, options.GOptions) - if err != nil { - return err - } + filters := []string{fmt.Sprintf("target.digest~=^%s$", dgst.String())} + imageList, err := imageService.List(ctx, filters...) + if err != nil { + return fmt.Errorf("failed to list images: %w", err) + } + if len(imageList) == 0 { + return fmt.Errorf("image %q: not found", img) + } - imgName := found.Image.Name - if _, ok := savedImages[imgName]; !ok { - savedImages[imgName] = struct{}{} - exportOpts = append(exportOpts, archive.WithImage(imageStore, imgName)) - } - return nil - }, + imageRef = imageList[0].Name + err = EnsureAllContent(ctx, client, imageRef, platMC, options.GOptions) + if err != nil { + return err + } + storeOpts = append(storeOpts, transferimage.WithExtraReference(imageRef)) } - // check if all images exist - if err := walker.WalkAll(ctx, images, false); err != nil { - return err - } + w := nopWriteCloser{options.Stdout} + + pf, done := transferutil.ProgressHandler(ctx, os.Stderr) + defer done() + + return client.Transfer(ctx, + transferimage.NewStore("", storeOpts...), + tarchive.NewImageExportStream(w, "", exportOpts...), + transfer.WithProgress(pf), + ) +} + +type nopWriteCloser struct { + io.Writer +} - return client.Export(ctx, options.Stdout, exportOpts...) +func (nopWriteCloser) Close() error { + return nil } diff --git a/pkg/cmd/image/tag.go b/pkg/cmd/image/tag.go index 60ab191d4f7..e9476c2bde8 100644 --- a/pkg/cmd/image/tag.go +++ b/pkg/cmd/image/tag.go @@ -18,79 +18,37 @@ package image import ( "context" - "fmt" containerd "github.com/containerd/containerd/v2/client" - "github.com/containerd/containerd/v2/core/images" - "github.com/containerd/errdefs" - "github.com/containerd/log" + transferimage "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/nerdctl/v2/pkg/api/types" - "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagOptions) error { - imageService := client.ImageService() - var srcName string - walker := &imagewalker.ImageWalker{ - Client: client, - OnFound: func(ctx context.Context, found imagewalker.Found) error { - if srcName == "" { - srcName = found.Image.Name - } - return nil - }, - } - matchCount, err := walker.Walk(ctx, options.Source) + parsedSource, err := referenceutil.Parse(options.Source) if err != nil { return err } - if matchCount < 1 { - return fmt.Errorf("%s: not found", options.Source) - } - parsedReference, err := referenceutil.Parse(options.Target) + parsedTarget, err := referenceutil.Parse(options.Target) if err != nil { return err } - ctx, done, err := client.WithLease(ctx) + platMC, err := platformutil.NewMatchComparer(false, nil) if err != nil { return err } - defer done(ctx) - - // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 - platMC, err := platformutil.NewMatchComparer(true, nil) + err = EnsureAllContent(ctx, client, parsedSource.String(), platMC, options.GOptions) if err != nil { return err } - err = EnsureAllContent(ctx, client, srcName, platMC, options.GOptions) - if err != nil { - log.G(ctx).Warn("Unable to fetch missing layers before committing. " + - "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.") - } - - img, err := imageService.Get(ctx, srcName) - if err != nil { - return err - } + sourceStore := transferimage.NewStore(parsedSource.String()) + targetStore := transferimage.NewStore(parsedTarget.String()) - img.Name = parsedReference.String() - if _, err = imageService.Create(ctx, img); err != nil { - if errdefs.IsAlreadyExists(err) { - if err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()); err != nil { - return err - } - if _, err = imageService.Create(ctx, img); err != nil { - return err - } - } else { - return err - } - } - return nil + return client.Transfer(ctx, sourceStore, targetStore) } diff --git a/pkg/errutil/errors_check.go b/pkg/errutil/errors_check.go index 202c4fe8518..8db2a166fcd 100644 --- a/pkg/errutil/errors_check.go +++ b/pkg/errutil/errors_check.go @@ -24,3 +24,18 @@ func IsErrConnectionRefused(err error) bool { const errMessage = "connect: connection refused" return strings.Contains(err.Error(), errMessage) } + +// IsErrHTTPResponseToHTTPSClient returns whether err is +// "http: server gave HTTP response to HTTPS client" +func IsErrHTTPResponseToHTTPSClient(err error) bool { + const errMessage = "server gave HTTP response to HTTPS client" + return strings.Contains(err.Error(), errMessage) +} + +// IsErrTLSHandshakeFailure returns whether err is a TLS handshake or certificate verification error +func IsErrTLSHandshakeFailure(err error) bool { + errStr := err.Error() + return strings.Contains(errStr, "tls:") || + strings.Contains(errStr, "x509:") || + strings.Contains(errStr, "certificate") +} diff --git a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go index 8577b8e2bc6..3397df877ca 100644 --- a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go +++ b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go @@ -20,10 +20,16 @@ import ( "context" "crypto/tls" "errors" + "fmt" + "os" + "path/filepath" + + "github.com/pelletier/go-toml/v2" "github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes/docker" dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config" + "github.com/containerd/containerd/v2/core/transfer/registry" "github.com/containerd/errdefs" "github.com/containerd/log" ) @@ -193,3 +199,70 @@ func NewAuthCreds(refHostname string) (AuthCreds, error) { return credFunc, nil } + +func NewCredentialHelper(refHostname string) (registry.CredentialHelper, error) { + authCreds, err := NewAuthCreds(refHostname) + if err != nil { + return nil, err + } + return &credentialHelper{authCreds: authCreds}, nil +} + +type credentialHelper struct { + authCreds AuthCreds +} + +func (ch *credentialHelper) GetCredentials(ctx context.Context, ref, host string) (registry.Credentials, error) { + username, secret, err := ch.authCreds(host) + if err != nil { + return registry.Credentials{}, err + } + return registry.Credentials{ + Host: host, + Username: username, + Secret: secret, + }, nil +} + +type hostFileConfig struct { + SkipVerify *bool `toml:"skip_verify,omitempty"` +} + +// CreateTmpHostsConfig creates a temporary hosts directory with hosts.toml configured for skip_verify +// Returns the temporary directory path or empty string if creation failed +func CreateTmpHostsConfig(hostname string, skipVerify bool) (string, error) { + if !skipVerify { + return "", nil + } + + tempDir, err := os.MkdirTemp("", "nerdctl-hosts-*") + if err != nil { + return "", fmt.Errorf("failed to create temp directory: %w", err) + } + + hostDir := filepath.Join(tempDir, hostname) + if err := os.MkdirAll(hostDir, 0755); err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("failed to create host directory: %w", err) + } + + config := hostFileConfig{} + if skipVerify { + skip := true + config.SkipVerify = &skip + } + + data, err := toml.Marshal(config) + if err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("failed to marshal hosts config: %w", err) + } + + hostsTomlPath := filepath.Join(hostDir, "hosts.toml") + if err := os.WriteFile(hostsTomlPath, data, 0644); err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("failed to write hosts.toml: %w", err) + } + + return tempDir, nil +} diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index 08d10be6437..e7ba4c3c22e 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "net/http" "reflect" "github.com/opencontainers/image-spec/identity" @@ -39,7 +38,6 @@ import ( "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" - "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" @@ -133,38 +131,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, return nil, err } - var dOpts []dockerconfigresolver.Opt - if options.GOptions.InsecureRegistry { - log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain) - dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) - } - dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) - resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) - if err != nil { - return nil, err - } - - img, err := PullImage(ctx, client, resolver, parsedReference.String(), options) - if err != nil { - // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused". - if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { - return nil, err - } - if options.GOptions.InsecureRegistry { - log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain) - dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) - resolver, err = dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) - if err != nil { - return nil, err - } - return PullImage(ctx, client, resolver, parsedReference.String(), options) - } - log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", parsedReference.Domain) - log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") - return nil, err - - } - return img, nil + return PullImageWithTransfer(ctx, client, parsedReference, rawRef, options) } // ResolveDigest resolves `rawRef` and returns its descriptor digest. diff --git a/pkg/imgutil/load/load.go b/pkg/imgutil/load/load.go index 0afb322f4e4..5e80096d5db 100644 --- a/pkg/imgutil/load/load.go +++ b/pkg/imgutil/load/load.go @@ -20,19 +20,19 @@ import ( "context" "errors" "fmt" - "io" "os" "strings" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" - "github.com/containerd/containerd/v2/core/images/archive" - "github.com/containerd/containerd/v2/pkg/archive/compression" + "github.com/containerd/containerd/v2/core/transfer" + tarchive "github.com/containerd/containerd/v2/core/transfer/archive" + transferimage "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" - "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/platformutil" + "github.com/containerd/nerdctl/v2/pkg/transferutil" ) // FromArchive loads and unpacks the images from the tar archive specified in image load options. @@ -54,27 +54,59 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I return nil, errors.New("stdin is empty and input flag is not specified") } } - decompressor, err := compression.DecompressStream(options.Stdin) - if err != nil { + + if _, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platform); err != nil { return nil, err } - platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platform) + + imageService := client.ImageService() + beforeImages, err := imageService.List(ctx) if err != nil { return nil, err } - imgs, err := importImages(ctx, client, decompressor, options.GOptions.Snapshotter, platMC) - if err != nil { - return nil, err + beforeSet := make(map[string]bool) + for _, img := range beforeImages { + beforeSet[img.Name] = true } - unpackedImages := make([]images.Image, 0, len(imgs)) - for _, img := range imgs { - err := unpackImage(ctx, client, img, platMC, options) + + var storeOpts []transferimage.StoreOpt + platUnpack := platforms.DefaultSpec() + if len(options.Platform) > 0 { + p, err := platforms.Parse(options.Platform[0]) if err != nil { - return unpackedImages, fmt.Errorf("error unpacking image (%s): %w", img.Name, err) + return nil, fmt.Errorf("invalid platform %q: %w", options.Platform[0], err) } - unpackedImages = append(unpackedImages, img) + platUnpack = p + storeOpts = append(storeOpts, transferimage.WithPlatforms(p)) + } else if !options.AllPlatforms { + storeOpts = append(storeOpts, transferimage.WithPlatforms(platUnpack)) } - return unpackedImages, nil + storeOpts = append(storeOpts, transferimage.WithUnpack(platUnpack, options.GOptions.Snapshotter)) + storeOpts = append(storeOpts, transferimage.WithDigestRef("import", true, true)) + + var loadedImages []images.Image + pf, done := transferutil.ProgressHandler(ctx, options.Stdout) + defer done() + + err = client.Transfer(ctx, + tarchive.NewImageImportStream(options.Stdin, ""), + transferimage.NewStore("", storeOpts...), + transfer.WithProgress(func(p transfer.Progress) { + if p.Event == "saved" { + if img, err := imageService.Get(ctx, p.Name); err == nil { + if !beforeSet[img.Name] { + loadedImages = append(loadedImages, img) + if !options.Quiet { + fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img.Name) + } + } + } + } + pf(p) + }), + ) + + return loadedImages, err } // FromOCIArchive loads and unpacks the images from the OCI formatted archive at the provided file system path. @@ -95,57 +127,3 @@ func FromOCIArchive(ctx context.Context, client *containerd.Client, pathToOCIArc return FromArchive(ctx, client, options) } - -type readCounter struct { - io.Reader - N int -} - -func (r *readCounter) Read(p []byte) (int, error) { - n, err := r.Reader.Read(p) - if n > 0 { - r.N += n - } - return n, err -} - -func importImages(ctx context.Context, client *containerd.Client, in io.Reader, snapshotter string, platformMC platforms.MatchComparer) ([]images.Image, error) { - // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient(). - // Otherwise unpacking may fail. - r := &readCounter{Reader: in} - imgs, err := client.Import(ctx, r, - containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), - containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), - containerd.WithImportPlatform(platformMC), - ) - if err != nil { - if r.N == 0 { - // Avoid confusing "unrecognized image format" - return nil, errors.New("no image was built") - } - if errors.Is(err, images.ErrEmptyWalk) { - err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) - } - return nil, err - } - return imgs, nil -} - -func unpackImage(ctx context.Context, client *containerd.Client, model images.Image, platform platforms.MatchComparer, options types.ImageLoadOptions) error { - image := containerd.NewImageWithPlatform(client, model, platform) - - if !options.Quiet { - fmt.Fprintf(options.Stdout, "unpacking %s (%s)...\n", model.Name, model.Target.Digest) - } - - err := image.Unpack(ctx, options.GOptions.Snapshotter) - if err != nil { - return err - } - - // Loaded message is shown even when quiet. - repo, tag := imgutil.ParseRepoTag(model.Name) - fmt.Fprintf(options.Stdout, "Loaded image: %s:%s\n", repo, tag) - - return nil -} diff --git a/pkg/imgutil/transfer.go b/pkg/imgutil/transfer.go new file mode 100644 index 00000000000..532f9982aee --- /dev/null +++ b/pkg/imgutil/transfer.go @@ -0,0 +1,232 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package imgutil + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/remotes/docker" + "github.com/containerd/containerd/v2/core/transfer" + transferimage "github.com/containerd/containerd/v2/core/transfer/image" + "github.com/containerd/containerd/v2/core/transfer/registry" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/errutil" + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" + "github.com/containerd/nerdctl/v2/pkg/platformutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/transferutil" +) + +func prepareImageStore(ctx context.Context, parsedReference *referenceutil.ImageReference, options types.ImagePullOptions) (*transferimage.Store, error) { + var storeOpts []transferimage.StoreOpt + if len(options.OCISpecPlatform) > 0 { + storeOpts = append(storeOpts, transferimage.WithPlatforms(options.OCISpecPlatform...)) + } + + unpackEnabled := len(options.OCISpecPlatform) == 1 + if options.Unpack != nil { + unpackEnabled = *options.Unpack + if unpackEnabled && len(options.OCISpecPlatform) != 1 { + return nil, fmt.Errorf("unpacking requires a single platform to be specified (e.g., --platform=amd64)") + } + } + + if unpackEnabled { + platform := options.OCISpecPlatform[0] + snapshotter := options.GOptions.Snapshotter + storeOpts = append(storeOpts, transferimage.WithUnpack(platform, snapshotter)) + } + + return transferimage.NewStore(parsedReference.String(), storeOpts...), nil +} + +func createOCIRegistry(ctx context.Context, parsedReference *referenceutil.ImageReference, gOptions types.GlobalCommandOptions, plainHTTP bool) (*registry.OCIRegistry, func(), error) { + ch, err := dockerconfigresolver.NewCredentialHelper(parsedReference.Domain) + if err != nil { + return nil, nil, err + } + + opts := []registry.Opt{ + registry.WithCredentials(ch), + } + + var tmpHostsDir string + cleanup := func() { + if tmpHostsDir != "" { + os.RemoveAll(tmpHostsDir) + } + } + + // If insecure-registry is set, create a temporary hosts.toml with skip_verify + if gOptions.InsecureRegistry { + tmpHostsDir, err = dockerconfigresolver.CreateTmpHostsConfig(parsedReference.Domain, true) + if err != nil { + log.G(ctx).WithError(err).Warnf("failed to create temporary hosts.toml for %q, continuing without it", parsedReference.Domain) + } else if tmpHostsDir != "" { + opts = append(opts, registry.WithHostDir(tmpHostsDir)) + } + } else if len(gOptions.HostsDir) > 0 { + opts = append(opts, registry.WithHostDir(gOptions.HostsDir[0])) + } + + if isLocalHost, err := docker.MatchLocalhost(parsedReference.Domain); err != nil { + cleanup() + return nil, nil, err + } else if isLocalHost || plainHTTP { + opts = append(opts, registry.WithDefaultScheme("http")) + } + + reg, err := registry.NewOCIRegistry(ctx, parsedReference.String(), opts...) + if err != nil { + cleanup() + return nil, nil, err + } + + return reg, cleanup, nil +} + +func PullImageWithTransfer(ctx context.Context, client *containerd.Client, parsedReference *referenceutil.ImageReference, rawRef string, options types.ImagePullOptions) (*EnsuredImage, error) { + store, err := prepareImageStore(ctx, parsedReference, options) + if err != nil { + return nil, err + } + + progressWriter := options.Stderr + if options.ProgressOutputToStdout { + progressWriter = options.Stdout + } + + fetcher, cleanup, err := createOCIRegistry(ctx, parsedReference, options.GOptions, false) + if err != nil { + return nil, err + } + defer cleanup() + + transferErr := doTransfer(ctx, client, fetcher, store, options.Quiet, progressWriter) + + if transferErr != nil && (errors.Is(transferErr, http.ErrSchemeMismatch) || errutil.IsErrConnectionRefused(transferErr) || errutil.IsErrHTTPResponseToHTTPSClient(transferErr) || errutil.IsErrTLSHandshakeFailure(transferErr)) { + if options.GOptions.InsecureRegistry { + log.G(ctx).WithError(transferErr).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain) + fetcher, cleanup2, err := createOCIRegistry(ctx, parsedReference, options.GOptions, true) + if err != nil { + return nil, err + } + defer cleanup2() + transferErr = doTransfer(ctx, client, fetcher, store, options.Quiet, progressWriter) + } + } + + if transferErr != nil { + return nil, transferErr + } + + imageStore := client.ImageService() + stored, err := store.Get(ctx, imageStore) + if err != nil { + return nil, err + } + + plMatch := platformutil.NewMatchComparerFromOCISpecPlatformSlice(options.OCISpecPlatform) + containerdImage := containerd.NewImageWithPlatform(client, stored, plMatch) + imgConfig, err := getImageConfig(ctx, containerdImage) + if err != nil { + return nil, err + } + + snapshotter := options.GOptions.Snapshotter + snOpt := getSnapshotterOpts(snapshotter) + + return &EnsuredImage{ + Ref: rawRef, + Image: containerdImage, + ImageConfig: *imgConfig, + Snapshotter: snapshotter, + Remote: snOpt.isRemote(), + }, nil +} + +func preparePushStore(pushRef string, options types.ImagePushOptions) (*transferimage.Store, error) { + platformsSlice, err := platformutil.NewOCISpecPlatformSlice(options.AllPlatforms, options.Platforms) + if err != nil { + return nil, err + } + + storeOpts := []transferimage.StoreOpt{} + if len(platformsSlice) > 0 { + storeOpts = append(storeOpts, transferimage.WithPlatforms(platformsSlice...)) + } + + return transferimage.NewStore(pushRef, storeOpts...), nil +} + +func PushImageWithTransfer(ctx context.Context, client *containerd.Client, parsedReference *referenceutil.ImageReference, pushRef, rawRef string, options types.ImagePushOptions) error { + source, err := preparePushStore(pushRef, options) + if err != nil { + return err + } + + progressWriter := io.Discard + if options.Stdout != nil { + progressWriter = options.Stdout + } + + pusher, cleanup, err := createOCIRegistry(ctx, parsedReference, options.GOptions, false) + if err != nil { + return err + } + defer cleanup() + + transferErr := doTransfer(ctx, client, source, pusher, options.Quiet, progressWriter) + + if transferErr != nil && (errors.Is(transferErr, http.ErrSchemeMismatch) || errutil.IsErrConnectionRefused(transferErr) || errutil.IsErrHTTPResponseToHTTPSClient(transferErr) || errutil.IsErrTLSHandshakeFailure(transferErr)) { + if options.GOptions.InsecureRegistry { + log.G(ctx).WithError(transferErr).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain) + pusher, cleanup2, err := createOCIRegistry(ctx, parsedReference, options.GOptions, true) + if err != nil { + return err + } + defer cleanup2() + transferErr = doTransfer(ctx, client, source, pusher, options.Quiet, progressWriter) + } + } + + if transferErr != nil { + log.G(ctx).WithError(transferErr).Errorf("server %q does not seem to support HTTPS", parsedReference.Domain) + log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") + return transferErr + } + + return nil +} + +func doTransfer(ctx context.Context, client *containerd.Client, src, dst interface{}, quiet bool, progressWriter io.Writer) error { + opts := make([]transfer.Opt, 0, 1) + if !quiet { + pf, done := transferutil.ProgressHandler(ctx, progressWriter) + defer done() + opts = append(opts, transfer.WithProgress(pf)) + } + return client.Transfer(ctx, src, dst, opts...) +} diff --git a/pkg/transferutil/progress.go b/pkg/transferutil/progress.go new file mode 100644 index 00000000000..15baf5d508d --- /dev/null +++ b/pkg/transferutil/progress.go @@ -0,0 +1,231 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package transferutil + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/containerd/containerd/v2/core/transfer" + "github.com/containerd/containerd/v2/pkg/progress" +) + +// From https://github.com/containerd/containerd/blob/v2.2.0-rc.0/cmd/ctr/commands/image/pull.go#L240-L473 +type progressNode struct { + transfer.Progress + children []*progressNode + root bool +} + +func (n *progressNode) mainDesc() *ocispec.Descriptor { + if n.Desc != nil { + return n.Desc + } + for _, c := range n.children { + if desc := c.mainDesc(); desc != nil { + return desc + } + } + return nil +} + +// ProgressHandler returns a progress callback and a cleanup function to render transfer progress. +// This implementation is based on containerd's ctr command progress handler. +func ProgressHandler(ctx context.Context, out io.Writer) (transfer.ProgressFunc, func()) { + ctx, cancel := context.WithCancel(ctx) + var ( + fw = progress.NewWriter(out) + start = time.Now() + statuses = map[string]*progressNode{} + roots = []*progressNode{} + pc = make(chan transfer.Progress, 5) + status string + closeC = make(chan struct{}) + ) + + progressFn := func(p transfer.Progress) { + select { + case pc <- p: + case <-ctx.Done(): + } + } + + done := func() { + cancel() + <-closeC + } + + go func() { + defer close(closeC) + for { + select { + case p := <-pc: + if p.Name == "" { + status = p.Event + continue + } + if node, ok := statuses[p.Name]; !ok { + node = &progressNode{ + Progress: p, + root: true, + } + if len(p.Parents) == 0 { + roots = append(roots, node) + } else { + var parents []string + for _, parent := range p.Parents { + pStatus, ok := statuses[parent] + if ok { + parents = append(parents, parent) + pStatus.children = append(pStatus.children, node) + node.root = false + } + } + node.Progress.Parents = parents + if node.root { + roots = append(roots, node) + } + } + statuses[p.Name] = node + } else { + if len(node.Progress.Parents) != len(p.Parents) { + var parents []string + var removeRoot bool + for _, parent := range p.Parents { + pStatus, ok := statuses[parent] + if ok { + parents = append(parents, parent) + var found bool + for _, child := range pStatus.children { + if child.Progress.Name == p.Name { + found = true + break + } + } + if !found { + pStatus.children = append(pStatus.children, node) + } + if node.root { + removeRoot = true + } + node.root = false + } + } + p.Parents = parents + // Check if needs to remove from root + if removeRoot { + for i := range roots { + if roots[i] == node { + roots = append(roots[:i], roots[i+1:]...) + break + } + } + } + } + node.Progress = p + } + + displayHierarchy(fw, status, roots, start) + fw.Flush() + + case <-ctx.Done(): + return + } + } + }() + + return progressFn, done +} + +func displayHierarchy(w io.Writer, status string, roots []*progressNode, start time.Time) { + total := displayNode(w, "", roots) + for _, r := range roots { + if desc := r.mainDesc(); desc != nil { + fmt.Fprintf(w, "%s %s\n", desc.MediaType, desc.Digest) + } + } + // Print the Status line + fmt.Fprintf(w, "%s\telapsed: %-4.1fs\ttotal: %7.6v\t(%v)\t\n", + status, + time.Since(start).Seconds(), + progress.Bytes(total), + progress.NewBytesPerSecond(total, time.Since(start))) +} + +func displayNode(w io.Writer, prefix string, nodes []*progressNode) int64 { + var total int64 + for i, node := range nodes { + status := node.Progress + total += status.Progress + pf, cpf := prefixes(i, len(nodes)) + if node.root { + pf, cpf = "", "" + } + + name := prefix + pf + shortenName(status.Name) + + switch status.Event { + case "downloading", "uploading", "extracting": + var bar progress.Bar + if status.Total > 0.0 { + bar = progress.Bar(float64(status.Progress) / float64(status.Total)) + } + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t%8.8s/%s\t\n", + name, + status.Event, + bar, + progress.Bytes(status.Progress), progress.Bytes(status.Total)) + case "resolving", "waiting": + bar := progress.Bar(0.0) + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t\n", + name, + status.Event, + bar) + case "complete", "extracted": + bar := progress.Bar(1.0) + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t\n", + name, + status.Event, + bar) + default: + fmt.Fprintf(w, "%-40.40s\t%s\t\n", + name, + status.Event) + } + total += displayNode(w, prefix+cpf, node.children) + } + return total +} + +func prefixes(index, length int) (string, string) { + if index+1 == length { + return "└──", " " + } + return "├──", "│ " +} + +func shortenName(name string) string { + if strings.HasPrefix(name, "sha256:") && len(name) == 71 { + return "(" + name[7:19] + ")" + } + return name +} From 3c77b01014e872407fe9846f80dfb8cca8dd2199 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:10:33 +0000 Subject: [PATCH 316/868] build(deps): bump github.com/cyphar/filepath-securejoin Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.4.1 to 0.6.1. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Changelog](https://github.com/cyphar/filepath-securejoin/blob/main/CHANGELOG.md) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.6.1) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: Akihiro Suda --- .github/workflows/job-lint-project.yml | 3 +++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 63a5c343963..96b772ae477 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -49,8 +49,11 @@ jobs: repo-access-token: ${{ secrets.GITHUB_TOKEN }} # go-licenses-ignore is set because go-licenses cannot detect the license of the following package: # * go-base36: Apache-2.0 OR MIT (https://github.com/multiformats/go-base36/blob/master/LICENSE.md) + # * filepath-securejoin: MPL-2.0 AND BSD-3-Clause, exceptionally approved by CNCF + # (https://github.com/cncf/foundation/issues/1154#issuecomment-3562385979) # # The list of the CNCF-approved licenses can be found here: # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md go-licenses-ignore: | github.com/multiformats/go-base36 + github.com/cyphar/filepath-securejoin diff --git a/go.mod b/go.mod index d218a932772..17e29962bf2 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.6.0 - github.com/cyphar/filepath-securejoin v0.4.1 //gomodjail:unconfined + github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.0.3+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined diff --git a/go.sum b/go.sum index a36d8718464..535ccf74806 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X 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.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +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/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= From 736a7e35c7122eb67263cf09e1c9e74c514c7c1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:11:56 +0000 Subject: [PATCH 317/868] build(deps): bump github.com/opencontainers/selinux Bumps [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) from 1.12.0 to 1.13.0. - [Release notes](https://github.com/opencontainers/selinux/releases) - [Commits](https://github.com/opencontainers/selinux/compare/v1.12.0...v1.13.0) --- updated-dependencies: - dependency-name: github.com/opencontainers/selinux dependency-version: 1.13.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 3 ++- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ff6f5a8af7e..51b40aeeb2b 100644 --- a/go.mod +++ b/go.mod @@ -113,7 +113,7 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect - github.com/opencontainers/selinux v1.12.0 // indirect + github.com/opencontainers/selinux v1.13.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -148,6 +148,7 @@ require ( ) require ( + cyphar.com/go-pathrs v0.2.1 // indirect github.com/moby/moby/api v1.52.0 // indirect github.com/moby/moby/client v0.1.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect diff --git a/go.sum b/go.sum index 5cdbdb9a212..506fe14828f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +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= 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= @@ -252,8 +254,8 @@ github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= -github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= -github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= +github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84= +github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= From 3baeb0fee0e0ede2434a919b3ec1019260d92e04 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Tue, 2 Dec 2025 00:19:45 +0900 Subject: [PATCH 318/868] fix: sort results of `nerdctl ps` and `nerdctl compose ps` alphabetically The current implementation can cause the unit test for `FormatPorts` in `pkg/formatter` to fail intermittently, as shown below: ```bash $ go test -run TestFormatPorts -count 10 --- FAIL: TestFormatPorts (0.00s) --- FAIL: TestFormatPorts/mixed_tcp_and_udp_with_consecutive_ports_on_anyhost (0.00s) formatter_test.go:191: assertion failed: 0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp (tt.expected string) != 0.0.0.0:3002-3003->8082-8083/udp, 0.0.0.0:3000-3001->8080-8081/tcp (result string) --- FAIL: TestFormatPorts (0.00s) --- FAIL: TestFormatPorts/mixed_tcp_and_udp_with_consecutive_ports_on_anyhost (0.00s) formatter_test.go:191: assertion failed: 0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp (tt.expected string) != 0.0.0.0:3002-3003->8082-8083/udp, 0.0.0.0:3000-3001->8080-8081/tcp (result string) --- FAIL: TestFormatPorts (0.00s) --- FAIL: TestFormatPorts/mixed_tcp_and_udp_with_consecutive_ports_on_anyhost (0.00s) formatter_test.go:191: assertion failed: 0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp (tt.expected string) != 0.0.0.0:3002-3003->8082-8083/udp, 0.0.0.0:3000-3001->8080-8081/tcp (result string) FAIL exit status 1 FAIL github.com/containerd/nerdctl/v2/pkg/formatter 0.005s ``` This occurs because the `FormatPorts` function iterates over a map, whose iteration order is non-deterministic. As a result, the output string may vary between test runs. This behavior has been reported in issue/#4626. To address this, this commit ensures that the string returned by `FormatPorts` is sorted alphabetically, resulting in stable and predictable output. Signed-off-by: Hayato Kiwata --- pkg/formatter/formatter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index c5b8a8be305..ff25312f9a2 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -163,6 +163,8 @@ func FormatPorts(ports []cni.PortMapping) string { ) } + sort.Strings(displayPorts) + return strings.Join(displayPorts, ", ") } From d18d5b7b3f7ee377b28ee723d1d71cceaa8179b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:25:41 +0000 Subject: [PATCH 319/868] build(deps): bump github.com/klauspost/compress from 1.18.1 to 1.18.2 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.1 to 1.18.2. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.1...v1.18.2) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 51b40aeeb2b..2c581ab375d 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.4.0 github.com/ipfs/go-cid v0.6.0 - github.com/klauspost/compress v1.18.1 + github.com/klauspost/compress v1.18.2 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 diff --git a/go.sum b/go.sum index 506fe14828f..64d363d9c27 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 092ebc75c32606321c7c6c22aee6c08ea89663b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:25:57 +0000 Subject: [PATCH 320/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.9.1 to 2.10.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.9.1...v2.10.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 ++- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 51b40aeeb2b..9c225a88f2b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.14.0-rc.1 - github.com/compose-spec/compose-go/v2 v2.9.1 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.10.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined @@ -152,6 +152,7 @@ require ( github.com/moby/moby/api v1.52.0 // indirect github.com/moby/moby/client v0.1.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect ) replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 506fe14828f..a46f2a7c89b 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= -github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= +github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAirA9btoTh/gB0Y= +github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY= @@ -351,6 +351,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/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= From 8a21115df1e04c906fb57491a97fc359730aa3ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:08:39 +0000 Subject: [PATCH 321/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.0+incompatible to 29.1.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.0...v29.1.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.1.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5f09ec2f1ef..f3bb967cfde 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.1+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index b8875889d94..3389e627a6d 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.0+incompatible h1:Ru/t9JgWbrwr8pIqh8XULT3CdoFMsg9CMXtaE+4Zymk= -github.com/docker/cli v29.1.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.1+incompatible h1:gGQk5qx62yPKRm3bUdKBzmDBSQzp17hlSLbV1F7jjys= +github.com/docker/cli v29.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From d312a5daabfd39da615cfbc302b71854c9327fa9 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 25 Nov 2025 15:57:29 +0100 Subject: [PATCH 322/868] Handle --gpus flag using CDI This change switches to using CDI to handle the --gpus flag. This removes the custom implementation that invoked the nvidia-container-cli directly. This mechanism does not align with existing implementations. Signed-off-by: Evan Lezar --- docs/gpu.md | 44 ++++++++++++--- pkg/cmd/container/run_linux.go | 61 ++++++--------------- pkg/composer/serviceparser/serviceparser.go | 15 +++-- 3 files changed, 66 insertions(+), 54 deletions(-) diff --git a/docs/gpu.md b/docs/gpu.md index 009170c1a37..cc21247a238 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -3,14 +3,18 @@ | :zap: Requirement | nerdctl >= 0.9 | |-------------------|----------------| +> [!NOTE] +> The description in this section applies to nerdctl v2.3 or later. +> Users of prior releases of nerdctl should refer to + nerdctl provides docker-compatible NVIDIA GPU support. ## Prerequisites - NVIDIA Drivers - Same requirement as when you use GPUs on Docker. For details, please refer to [the doc by NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#pre-requisites). -- `nvidia-container-cli` - - containerd relies on this CLI for setting up GPUs inside container. You can install this via [`libnvidia-container` package](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/arch-overview.html#libnvidia-container). +- The NVIDIA Container Toolkit + - containerd relies on the NVIDIA Container Toolkit to make GPUs usable inside a container. You can install the NVIDIA Container Toolkit by following the [official installation instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). ## Options for `nerdctl run --gpus` @@ -27,23 +31,24 @@ You can also pass detailed configuration to `--gpus` option as a list of key-val - `count`: number of GPUs to use. `all` exposes all available GPUs. - `device`: IDs of GPUs to use. UUID or numbers of GPUs can be specified. -- `capabilities`: [Driver capabilities](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities). If unset, use default driver `utility`, `compute`. The following example exposes a specific GPU to the container. ``` -nerdctl run -it --rm --gpus '"capabilities=utility,compute",device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi +nerdctl run -it --rm --gpus 'device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi ``` +Note that although `capabilities` options may be provided, these are ignored when processing the GPU request since nerdctl v2.3. + ## Fields for `nerdctl compose` `nerdctl compose` also supports GPUs following [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/deploy.md#devices). -You can use GPUs on compose when you specify some of the following `capabilities` in `services.demo.deploy.resources.reservations.devices`. +You can use GPUs on compose when you specify the `driver` as `nvidia` or one or +more of the following `capabilities` in `services.demo.deploy.resources.reservations.devices`. - `gpu` - `nvidia` -- all allowed capabilities for `nerdctl run --gpus` Available fields are the same as `nerdctl run --gpus`. @@ -59,12 +64,37 @@ services: resources: reservations: devices: - - capabilities: ["utility"] + - driver: nvidia count: all ``` ## Trouble Shooting +### `nerdctl run --gpus` fails due to an unresolvable CDI device + +If the required CDI specifications for NVIDIA devices are not available on the +system, the `nerdctl run` command will fail with an error similar to: `CDI device injection failed: unresolvable CDI devices nvidia.com/gpu=all` (the +exact error message will depend on the device(s) requested). + +This should be the same error message that is reported when the `--device` flag +is used to request a CDI device: +``` +nerdctl run --device=nvidia.com/gpu=all +``` + +Ensure that the NVIDIA Container Toolkit (>= v1.18.0 is recommended) is installed and the requested CDI devices are present in the ouptut of `nvidia-ctk cdi list`: + +``` +$ nvidia-ctk cdi list +INFO[0000] Found 3 CDI devices +nvidia.com/gpu=0 +nvidia.com/gpu=GPU-3eb87630-93d5-b2b6-b8ff-9b359caf4ee2 +nvidia.com/gpu=all +``` + +See the NVIDIA Container Toolkit [CDI documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html) for more information. + + ### `nerdctl run --gpus` fails when using the Nvidia gpu-operator If the Nvidia driver is installed by the [gpu-operator](https://github.com/NVIDIA/gpu-operator).The `nerdctl run` will fail with the error message `(FATA[0000] exec: "nvidia-container-cli": executable file not found in $PATH)`. diff --git a/pkg/cmd/container/run_linux.go b/pkg/cmd/container/run_linux.go index 3280d3e532d..851307d905e 100644 --- a/pkg/cmd/container/run_linux.go +++ b/pkg/cmd/container/run_linux.go @@ -25,7 +25,6 @@ import ( "github.com/opencontainers/runtime-spec/specs-go" containerd "github.com/containerd/containerd/v2/client" - "github.com/containerd/containerd/v2/contrib/nvidia" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/log" @@ -99,7 +98,7 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts if options.Sysctl != nil { opts = append(opts, WithSysctls(strutil.ConvertKVStringsToMap(options.Sysctl))) } - gpuOpt, err := parseGPUOpts(options.GPUs) + gpuOpt, err := parseGPUOpts(options.GOptions.CDISpecDirs, options.GPUs) if err != nil { return nil, err } @@ -262,60 +261,36 @@ func withOOMScoreAdj(score int) oci.SpecOpts { } } -func parseGPUOpts(value []string) (res []oci.SpecOpts, _ error) { +func parseGPUOpts(cdiSpecDirs []string, value []string) (res []oci.SpecOpts, _ error) { for _, gpu := range value { - gpuOpt, err := parseGPUOpt(gpu) + req, err := ParseGPUOptCSV(gpu) if err != nil { return nil, err } - res = append(res, gpuOpt) + res = append(res, withCDIDevices(cdiSpecDirs, req.toCDIDeviceIDS()...)) } return res, nil } -func parseGPUOpt(value string) (oci.SpecOpts, error) { - req, err := ParseGPUOptCSV(value) - if err != nil { - return nil, err +func (req *GPUReq) toCDIDeviceIDS() []string { + var cdiDeviceIDs []string + for _, id := range req.normalizeDeviceIDs() { + cdiDeviceIDs = append(cdiDeviceIDs, "nvidia.com/gpu="+id) } + return cdiDeviceIDs +} - var gpuOpts []nvidia.Opts - +func (req *GPUReq) normalizeDeviceIDs() []string { if len(req.DeviceIDs) > 0 { - gpuOpts = append(gpuOpts, nvidia.WithDeviceUUIDs(req.DeviceIDs...)) - } else if req.Count > 0 { - var devices []int - for i := 0; i < req.Count; i++ { - devices = append(devices, i) - } - gpuOpts = append(gpuOpts, nvidia.WithDevices(devices...)) - } else if req.Count < 0 { - gpuOpts = append(gpuOpts, nvidia.WithAllDevices) + return req.DeviceIDs } - - str2cap := make(map[string]nvidia.Capability) - for _, c := range nvidia.AllCaps() { - str2cap[string(c)] = c - } - var nvidiaCaps []nvidia.Capability - for _, c := range req.Capabilities { - if cp, isNvidiaCap := str2cap[c]; isNvidiaCap { - nvidiaCaps = append(nvidiaCaps, cp) - } + if req.Count < 0 { + return []string{"all"} } - if len(nvidiaCaps) != 0 { - gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidiaCaps...)) - } else { - // Add "utility", "compute" capability if unset. - // Please see also: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities - gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidia.Utility, nvidia.Compute)) - } - - if rootlessutil.IsRootless() { - // "--no-cgroups" option is needed to nvidia-container-cli in rootless environment - // Please see also: https://github.com/moby/moby/issues/38729#issuecomment-463493866 - gpuOpts = append(gpuOpts, nvidia.WithNoCgroups) + var ids []string + for i := 0; i < req.Count; i++ { + ids = append(ids, fmt.Sprintf("%d", i)) } - return nvidia.WithGPUs(gpuOpts...), nil + return ids } diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 804250f80ec..534acabcfd9 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -30,7 +30,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" - "github.com/containerd/containerd/v2/contrib/nvidia" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/identifiers" @@ -262,9 +261,17 @@ func getMemLimit(svc types.ServiceConfig) (types.UnitBytes, error) { func getGPUs(svc types.ServiceConfig) (reqs []string, _ error) { // "gpu" and "nvidia" are also allowed capabilities (but not used as nvidia driver capabilities) // https://github.com/moby/moby/blob/v20.10.7/daemon/nvidia_linux.go#L37 - capset := map[string]struct{}{"gpu": {}, "nvidia": {}} - for _, c := range nvidia.AllCaps() { - capset[string(c)] = struct{}{} + capset := map[string]struct{}{ + "gpu": {}, "nvidia": {}, + // Allow the list of capabilities here (excluding "all" and "none") + // https://github.com/NVIDIA/nvidia-container-toolkit/blob/ff7c2d4866a7d46d1bf2a83590b263e10ec99cb5/internal/config/image/capabilities.go#L28-L38 + "compat32": {}, + "compute": {}, + "display": {}, + "graphics": {}, + "ngx": {}, + "utility": {}, + "video": {}, } if svc.Deploy != nil && svc.Deploy.Resources.Reservations != nil { for _, dev := range svc.Deploy.Resources.Reservations.Devices { From ad4369d8f7558220b8aee75d2425aa7cebe7ac33 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 2 Dec 2025 20:40:40 +0900 Subject: [PATCH 323/868] fix(namespace): require --label falg for update command Signed-off-by: Park jungtae --- cmd/nerdctl/namespace/namespace_update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/namespace/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go index dd15cd91a49..0e02f78a9c8 100644 --- a/cmd/nerdctl/namespace/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -36,7 +36,8 @@ func updateCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } - cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace") + cmd.Flags().StringArrayP("label", "l", nil, "Set labels for a namespace (required)") + cmd.MarkFlagRequired("label") return cmd } From 5e7aa0be4b4a81e44d9287cb7f44eb82af599c24 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 2 Dec 2025 20:43:29 +0900 Subject: [PATCH 324/868] fix(namespace): add namespace existence check in update command Signed-off-by: Park jungtae --- pkg/cmd/namespace/common.go | 23 ++++++++++++++++++++++- pkg/cmd/namespace/update.go | 3 +++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/namespace/common.go b/pkg/cmd/namespace/common.go index e08939e0427..e15d00d726c 100644 --- a/pkg/cmd/namespace/common.go +++ b/pkg/cmd/namespace/common.go @@ -16,7 +16,15 @@ package namespace -import "strings" +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/compose-spec/compose-go/v2/errdefs" + "github.com/containerd/containerd/v2/pkg/namespaces" +) func objectWithLabelArgs(args []string) map[string]string { if len(args) >= 1 { @@ -39,3 +47,16 @@ func labelArgs(labelStrings []string) map[string]string { return labels } + +// namespaceExists checks if the namespace exists +func namespaceExists(ctx context.Context, store namespaces.Store, namespace string) error { + nsList, err := store.List(ctx) + if err != nil { + return err + } + if slices.Contains(nsList, namespace) { + return nil + } + + return fmt.Errorf("namespace %s: %w", namespace, errdefs.ErrNotFound) +} diff --git a/pkg/cmd/namespace/update.go b/pkg/cmd/namespace/update.go index 63d2d8a5971..91b6b714905 100644 --- a/pkg/cmd/namespace/update.go +++ b/pkg/cmd/namespace/update.go @@ -27,6 +27,9 @@ import ( func Update(ctx context.Context, client *containerd.Client, namespace string, options types.NamespaceUpdateOptions) error { labelsArg := objectWithLabelArgs(options.Labels) namespaces := client.NamespaceService() + if err := namespaceExists(ctx, namespaces, namespace); err != nil { + return err + } for k, v := range labelsArg { if err := namespaces.SetLabel(ctx, namespace, k, v); err != nil { return err From 8c964034c79519838960512b8a4a626b0881d956 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 2 Dec 2025 21:08:57 +0900 Subject: [PATCH 325/868] fix(namespace): add namespace existence check in Inspect command Signed-off-by: Park jungtae --- pkg/cmd/namespace/common.go | 1 + pkg/cmd/namespace/inspect.go | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/namespace/common.go b/pkg/cmd/namespace/common.go index e15d00d726c..309d2f90f90 100644 --- a/pkg/cmd/namespace/common.go +++ b/pkg/cmd/namespace/common.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/compose-spec/compose-go/v2/errdefs" + "github.com/containerd/containerd/v2/pkg/namespaces" ) diff --git a/pkg/cmd/namespace/inspect.go b/pkg/cmd/namespace/inspect.go index 3a7a4932815..ebe327da3d0 100644 --- a/pkg/cmd/namespace/inspect.go +++ b/pkg/cmd/namespace/inspect.go @@ -21,6 +21,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/formatter" @@ -28,10 +29,17 @@ import ( ) func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces []string, options types.NamespaceInspectOptions) error { - result := make([]interface{}, len(inspectedNamespaces)) - for index, ns := range inspectedNamespaces { + result := []interface{}{} + warns := []error{} + + for _, ns := range inspectedNamespaces { ctx = namespaces.WithNamespace(ctx, ns) - labels, err := client.NamespaceService().Labels(ctx, ns) + namespaceService := client.NamespaceService() + if err := namespaceExists(ctx, namespaceService, ns); err != nil { + warns = append(warns, err) + continue + } + labels, err := namespaceService.Labels(ctx, ns) if err != nil { return err } @@ -39,7 +47,13 @@ func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces Name: ns, Labels: &labels, } - result[index] = nsInspect + result = append(result, nsInspect) + } + if err := formatter.FormatSlice(options.Format, options.Stdout, result); err != nil { + return err + } + for _, warn := range warns { + log.G(ctx).Warn(warn) } - return formatter.FormatSlice(options.Format, options.Stdout, result) + return nil } From 7dba47d564625fe5e5831150770db12a507d6fae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:02:05 +0000 Subject: [PATCH 326/868] build(deps): bump actions/checkout from 6.0.0 to 6.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index f60446202e3..e9512c5b062 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 88a6dc9e3d1..42078e59c95 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index afab345faef..e6f528e9bdf 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index ff00d9f9d02..4de2f1457b8 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 96b772ae477..1af262636c3 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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 ee4e21daf71..61c55559ae0 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index e82a96e8eff..43790b93e4d 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index b797c39f0cf..8cba6f34c90 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -71,7 +71,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index a0f9ed58000..6d9fafb81c1 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -30,7 +30,7 @@ jobs: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 2840a4e6527..fbd67dad75f 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index b896e44fb77..316916809ef 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 320e5640ac7..8d4a7efdbca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index cb74fb27401..f6c6d890090 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -46,7 +46,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 - name: "Run" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index e3399d36cf6..91511735664 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -32,7 +32,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 100 - if: ${{ matrix.canary }} From b4c6c8fd3119ce8000805110199b670dd3b55e12 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 5 Nov 2025 22:41:13 +0800 Subject: [PATCH 327/868] configure containerd config with remote snapshotter Add remote snapshot annotations and transfer unpack config for stargz, soci, and fuse-overlayfs snapshotter plugins. Signed-off-by: ChengyuZhu6 --- Dockerfile.d/etc_containerd_config.toml | 9 +++++++++ ...test-integration-etc_containerd_config.toml | 18 ++++++++++++++++++ Dockerfile.d/test-integration-rootless.sh | 9 +++++++++ README.md | 1 + docs/nydus.md | 5 +++++ docs/overlaybd.md | 5 +++++ docs/rootless.md | 10 ++++++++++ docs/soci.md | 5 +++++ docs/stargz.md | 5 +++++ .../rootless/containerd-rootless-setuptool.sh | 18 ++++++++++++++++++ 10 files changed, 85 insertions(+) diff --git a/Dockerfile.d/etc_containerd_config.toml b/Dockerfile.d/etc_containerd_config.toml index dccac081af4..583ebcc3d46 100644 --- a/Dockerfile.d/etc_containerd_config.toml +++ b/Dockerfile.d/etc_containerd_config.toml @@ -5,3 +5,12 @@ version = 2 [proxy_plugins.stargz] type = "snapshot" address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock" + [proxy_plugins.stargz.exports] + root = "/var/lib/containerd-stargz-grpc/" + enable_remote_snapshot_annotations = "true" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" diff --git a/Dockerfile.d/test-integration-etc_containerd_config.toml b/Dockerfile.d/test-integration-etc_containerd_config.toml index d37df58da75..0a6cc862e77 100644 --- a/Dockerfile.d/test-integration-etc_containerd_config.toml +++ b/Dockerfile.d/test-integration-etc_containerd_config.toml @@ -5,8 +5,26 @@ version = 2 [proxy_plugins.stargz] type = "snapshot" address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock" + [proxy_plugins.stargz.exports] + root = "/var/lib/containerd-stargz-grpc/" + enable_remote_snapshot_annotations = "true" # Enable soci snapshotter [proxy_plugins.soci] type = "snapshot" address = "/run/soci-snapshotter-grpc/soci-snapshotter-grpc.sock" + [proxy_plugins.soci.exports] + root = "/var/lib/soci-snapshotter-grpc" + enable_remote_snapshot_annotations = "true" + +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" + +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "soci" + +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" diff --git a/Dockerfile.d/test-integration-rootless.sh b/Dockerfile.d/test-integration-rootless.sh index f6e243f32b5..63f383462cc 100755 --- a/Dockerfile.d/test-integration-rootless.sh +++ b/Dockerfile.d/test-integration-rootless.sh @@ -53,6 +53,15 @@ else [proxy_plugins."stargz"] type = "snapshot" address = "/run/user/$(id -u)/containerd-stargz-grpc/containerd-stargz-grpc.sock" + [proxy_plugins.stargz.exports] + root = "/home/rootless/.local/share/containerd-stargz-grpc/" + enable_remote_snapshot_annotations = "true" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" EOF systemctl --user restart containerd.service containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing diff --git a/README.md b/README.md index b0cb1698a95..d01e10fc3b8 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ Advanced features: - [`./docs/stargz.md`](./docs/stargz.md): Lazy-pulling using Stargz Snapshotter - [`./docs/nydus.md`](./docs/nydus.md): Lazy-pulling using Nydus Snapshotter +- [`./docs/soci.md`](./docs/soci.md): Lazy-pulling using SOCI Snapshotter - [`./docs/overlaybd.md`](./docs/overlaybd.md): Lazy-pulling using OverlayBD Snapshotter - [`./docs/ocicrypt.md`](./docs/ocicrypt.md): Running encrypted images - [`./docs/gpu.md`](./docs/gpu.md): Using GPUs inside containers diff --git a/docs/nydus.md b/docs/nydus.md index 1019827a548..c8f912d01cf 100644 --- a/docs/nydus.md +++ b/docs/nydus.md @@ -15,6 +15,11 @@ Nydus snapshotter is a remote snapshotter plugin of containerd for [Nydus](https [proxy_plugins.nydus] type = "snapshot" address = "/run/containerd-nydus-grpc/containerd-nydus-grpc.sock" + +# Optional: Configure nydus for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "nydus" ``` - Launch `containerd` and `containerd-nydus-grpc` diff --git a/docs/overlaybd.md b/docs/overlaybd.md index caa4673403e..e6e6284c42b 100644 --- a/docs/overlaybd.md +++ b/docs/overlaybd.md @@ -17,6 +17,11 @@ See https://github.com/containerd/accelerated-container-image to learn further i [proxy_plugins.overlaybd] type = "snapshot" address = "/run/overlaybd-snapshotter/overlaybd.sock" + +# Optional: Configure overlaybd for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlaybd" ``` - Launch `containerd` and `overlaybd-snapshotter` diff --git a/docs/rootless.md b/docs/rootless.md index 1000bd50865..4b3f593f760 100644 --- a/docs/rootless.md +++ b/docs/rootless.md @@ -73,6 +73,11 @@ Then, add the following config to `~/.config/containerd/config.toml`, and run `s type = "snapshot" # NOTE: replace "1000" with your actual UID address = "/run/user/1000/containerd-fuse-overlayfs.sock" + +# Optional: Configure fuse-overlayfs for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "fuse-overlayfs" ``` The snapshotter can be specified as `$CONTAINERD_SNAPSHOTTER`. @@ -98,6 +103,11 @@ Then, add the following config to `~/.config/containerd/config.toml` and run `sy type = "snapshot" # NOTE: replace "1000" with your actual UID address = "/run/user/1000/containerd-stargz-grpc/containerd-stargz-grpc.sock" + +# Optional: Configure stargz for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" ``` The snapshotter can be specified as `$CONTAINERD_SNAPSHOTTER`. diff --git a/docs/soci.md b/docs/soci.md index 0e91bea2443..e79b7f472d9 100644 --- a/docs/soci.md +++ b/docs/soci.md @@ -30,6 +30,11 @@ For detailed information about the differences between v1 and v2, see the [SOCI [proxy_plugins.soci] type = "snapshot" address = "/run/soci-snapshotter-grpc/soci-snapshotter-grpc.sock" + +# Optional: Configure soci for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "soci" ``` - Launch `containerd` and `soci-snapshotter-grpc` diff --git a/docs/stargz.md b/docs/stargz.md index 57cd22f303e..16e5c365c22 100644 --- a/docs/stargz.md +++ b/docs/stargz.md @@ -22,6 +22,11 @@ See https://github.com/containerd/stargz-snapshotter to learn further informatio [proxy_plugins.stargz] type = "snapshot" address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock" + +# Optional: Configure stargz for image unpacking (allows automatic snapshotter selection) +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" ``` - Launch `containerd` and `containerd-stargz-grpc` diff --git a/extras/rootless/containerd-rootless-setuptool.sh b/extras/rootless/containerd-rootless-setuptool.sh index 27627640d51..c0f754e37b8 100755 --- a/extras/rootless/containerd-rootless-setuptool.sh +++ b/extras/rootless/containerd-rootless-setuptool.sh @@ -404,6 +404,15 @@ cmd_entrypoint_install_fuse_overlayfs() { [proxy_plugins."fuse-overlayfs"] type = "snapshot" address = "${XDG_RUNTIME_DIR}/containerd-fuse-overlayfs.sock" + [proxy_plugins."fuse-overlayfs".exports] + root = "${XDG_DATA_HOME}/containerd-fuse-overlayfs/" + enable_remote_snapshot_annotations = "true" + [[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "fuse-overlayfs" + [[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" ### END ### EOT INFO "Set \`export CONTAINERD_SNAPSHOTTER=\"fuse-overlayfs\"\` to use the fuse-overlayfs snapshotter." @@ -449,6 +458,15 @@ cmd_entrypoint_install_stargz() { [proxy_plugins."stargz"] type = "snapshot" address = "${XDG_RUNTIME_DIR}/containerd-stargz-grpc/containerd-stargz-grpc.sock" + [proxy_plugins.stargz.exports] + root = "${XDG_DATA_HOME}/containerd-stargz-grpc/" + enable_remote_snapshot_annotations = "true" + [[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" + [[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" ### END ### EOT INFO "Set \`export CONTAINERD_SNAPSHOTTER=\"stargz\"\` to use the stargz snapshotter." From 7d8ab7c5c18048a6c55af9ae14fad1f5ffa78f62 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 27 Nov 2025 07:35:16 +0800 Subject: [PATCH 328/868] Support legacy push/pull for containerd 1.7.x Add version detection to automatically select Transfer service (2.0+) or legacy resolver methods (< 2.0) for better compatibility. Signed-off-by: ChengyuZhu6 --- pkg/cmd/image/push.go | 18 ++++++++- pkg/containerdutil/version.go | 53 +++++++++++++++++++++++++++ pkg/imgutil/imgutil.go | 47 +++++++++++++++++++++++- pkg/infoutil/infoutil.go | 13 ------- pkg/taskutil/taskutil.go | 4 +- pkg/testutil/nerdtest/requirements.go | 4 +- 6 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 pkg/containerdutil/version.go diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go index 8aa6fbd05a3..505e8eb7129 100644 --- a/pkg/cmd/image/push.go +++ b/pkg/cmd/image/push.go @@ -43,6 +43,7 @@ import ( estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/imgutil" nerdconverter "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" @@ -152,8 +153,21 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options return err } } else { - if err := imgutil.PushImageWithTransfer(ctx, client, parsedReference, pushRef, ref, options); err != nil { - return err + // Transfer service is available in containerd 1.7, but full support is only in 2.0+ + // For containerd 1.7, use the legacy resolver-based push method for better compatibility + useTransferAPI := containerdutil.SupportsFullTransferService(ctx, client) + if !useTransferAPI { + log.G(ctx).Debug("Detected containerd < 2.0, using legacy push method") + } + + if useTransferAPI { + if err := imgutil.PushImageWithTransfer(ctx, client, parsedReference, pushRef, ref, options); err != nil { + return err + } + } else { + if err := pushImageWithLocal(ctx, client, parsedReference, pushRef, ref, options, platMC); err != nil { + return err + } } } diff --git a/pkg/containerdutil/version.go b/pkg/containerdutil/version.go new file mode 100644 index 00000000000..6880c918efa --- /dev/null +++ b/pkg/containerdutil/version.go @@ -0,0 +1,53 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerdutil + +import ( + "context" + "fmt" + + "github.com/Masterminds/semver/v3" + + containerd "github.com/containerd/containerd/v2/client" +) + +func ServerSemVer(ctx context.Context, client *containerd.Client) (*semver.Version, error) { + v, err := client.Version(ctx) + if err != nil { + return nil, err + } + sv, err := semver.NewVersion(v.Version) + if err != nil { + return nil, fmt.Errorf("failed to parse the containerd version %q: %w", v.Version, err) + } + return sv, nil +} + +// SupportsFullTransferService checks if the containerd version fully supports the Transfer service. +// While containerd 1.7 has Transfer service, full support is only available in 2.0+. +// The following features are missing in containerd 1.7: +// - Non-distributable artifacts support +// - Registry configuration options: WithHostDir(), WithDefaultScheme() etc. +func SupportsFullTransferService(ctx context.Context, client *containerd.Client) bool { + sv, err := ServerSemVer(ctx, client) + if err != nil { + // If we can't determine version, assume it's an older version for safety + return false + } + v20, _ := semver.NewVersion("2.0.0") + return !sv.LessThan(v20) +} diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index e7ba4c3c22e..227f1dbd37b 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "reflect" "github.com/opencontainers/image-spec/identity" @@ -38,6 +39,8 @@ import ( "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" + "github.com/containerd/nerdctl/v2/pkg/errutil" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" @@ -131,7 +134,49 @@ func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, return nil, err } - return PullImageWithTransfer(ctx, client, parsedReference, rawRef, options) + // Transfer service is available in containerd 1.7, but full support is only in 2.0+ + // For containerd 1.7, use the legacy resolver-based pull method for better compatibility + useTransferAPI := containerdutil.SupportsFullTransferService(ctx, client) + if !useTransferAPI { + log.G(ctx).Debug("Detected containerd < 2.0, using legacy pull method") + } + + if useTransferAPI { + return PullImageWithTransfer(ctx, client, parsedReference, rawRef, options) + } + + var dOpts []dockerconfigresolver.Opt + if options.GOptions.InsecureRegistry { + log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) + resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) + if err != nil { + return nil, err + } + + img, err := PullImage(ctx, client, resolver, parsedReference.String(), options) + if err != nil { + // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused". + if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { + return nil, err + } + if options.GOptions.InsecureRegistry { + log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain) + dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) + resolver, err = dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) + if err != nil { + return nil, err + } + return PullImage(ctx, client, resolver, parsedReference.String(), options) + } + log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", parsedReference.Domain) + log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") + return nil, err + + } + return img, nil } // ResolveDigest resolves `rawRef` and returns its descriptor digest. diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index ce6bf9085b1..75cdc7b59e6 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -25,7 +25,6 @@ import ( "strings" "time" - "github.com/Masterminds/semver/v3" "github.com/docker/docker/pkg/sysinfo" containerd "github.com/containerd/containerd/v2/client" @@ -146,18 +145,6 @@ func ServerVersion(ctx context.Context, client *containerd.Client) (*dockercompa return v, nil } -func ServerSemVer(ctx context.Context, client *containerd.Client) (*semver.Version, error) { - v, err := client.Version(ctx) - if err != nil { - return nil, err - } - sv, err := semver.NewVersion(v.Version) - if err != nil { - return nil, fmt.Errorf("failed to parse the containerd version %q: %w", v.Version, err) - } - return sv, nil -} - func buildctlVersion() dockercompat.ComponentVersion { buildctlBinary, err := buildkitutil.BuildctlBinary() if err != nil { diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index ec5f96585d6..633c188cf73 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -46,7 +46,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/cioutil" "github.com/containerd/nerdctl/v2/pkg/consoleutil" - "github.com/containerd/nerdctl/v2/pkg/infoutil" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" ) // TaskOptions contains options for creating a new task @@ -201,7 +201,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } else { var in io.Reader if opts.IsInteractive { - if sv, err := infoutil.ServerSemVer(ctx, client); err != nil { + if sv, err := containerdutil.ServerSemVer(ctx, client); err != nil { log.G(ctx).Warn(err) } else if sv.LessThan(semver.MustParse("1.6.0-0")) { log.G(ctx).Warnf("`nerdctl (run|exec) -i` without `-t` expects containerd 1.6 or later, got containerd %v", sv) diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index d4af8490339..fe3e053d83a 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -33,8 +33,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" - "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -477,7 +477,7 @@ func ContainerdVersion(v string) *test.Requirement { return false, fmt.Sprintf("failed to create client: %v", err) } defer cancel() - if sv, err := infoutil.ServerSemVer(ctx, client); err != nil { + if sv, err := containerdutil.ServerSemVer(ctx, client); err != nil { return false, err.Error() } else if sv.LessThan(semver.MustParse(v)) { return false, fmt.Sprintf("`nerdctl commit --compression expects containerd %s or later, got containerd %v", v, sv) From d38a3d74bbfc4e6f68f3c86d0d6967760f9b1f77 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 3 Dec 2025 17:09:22 +0900 Subject: [PATCH 329/868] docs/command-reference.md: fix nerdctl namespace anchors without blue_square Signed-off-by: Hayato Kiwata --- docs/command-reference.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 4bd051fa86a..6786c757f0c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -79,11 +79,11 @@ - [:whale: nerdctl volume rm](#whale-nerdctl-volume-rm) - [:whale: nerdctl volume prune](#whale-nerdctl-volume-prune) - [Namespace management](#namespace-management) - - [:nerd_face: nerdctl namespace create](#nerd_face-blue_square-nerdctl-namespace-create) - - [:nerd_face: nerdctl namespace inspect](#nerd_face-blue_square-nerdctl-namespace-inspect) - - [:nerd_face: nerdctl namespace ls](#nerd_face-blue_square-nerdctl-namespace-ls) - - [:nerd_face: nerdctl namespace remove](#nerd_face-blue_square-nerdctl-namespace-remove) - - [:nerd_face: nerdctl namespace update](#nerd_face-blue_square-nerdctl-namespace-update) + - [:nerd_face: nerdctl namespace create](#nerd_face-nerdctl-namespace-create) + - [:nerd_face: nerdctl namespace inspect](#nerd_face-nerdctl-namespace-inspect) + - [:nerd_face: nerdctl namespace ls](#nerd_face-nerdctl-namespace-ls) + - [:nerd_face: nerdctl namespace remove](#nerd_face-nerdctl-namespace-remove) + - [:nerd_face: nerdctl namespace update](#nerd_face-nerdctl-namespace-update) - [AppArmor profile management](#apparmor-profile-management) - [:nerd_face: nerdctl apparmor inspect](#nerd_face-nerdctl-apparmor-inspect) - [:nerd_face: nerdctl apparmor load](#nerd_face-nerdctl-apparmor-load) From 5595bd294849df98ee66e32e5e4992ae3cb62231 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:01:55 +0000 Subject: [PATCH 330/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.1+incompatible to 29.1.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.1...v29.1.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.1.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f3bb967cfde..3f3c7d2a838 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.2+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 3389e627a6d..83e076ce182 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.1+incompatible h1:gGQk5qx62yPKRm3bUdKBzmDBSQzp17hlSLbV1F7jjys= -github.com/docker/cli v29.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8= +github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From e6aa885db8a2de270ffad3f7370328b68c5c6f15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:02:15 +0000 Subject: [PATCH 331/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.7 to 0.15.8. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.7...v0.15.8) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3f3c7d2a838..4f44a9d598c 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.7 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.8 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 83e076ce182..d3c2076b030 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.7 h1:0/IVOpqM3TClKjzGRhmT3nq38IIZ62eACxGxTKcDk/0= -github.com/containerd/nydus-snapshotter v0.15.7/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms= +github.com/containerd/nydus-snapshotter v0.15.8 h1:UnXnbb1ZpxvOUcOmR0i31cLfuqkU9hXZraL/9EiFVWk= +github.com/containerd/nydus-snapshotter v0.15.8/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From d9bfe4852fdf3e097279c0f3e7c070d6b051ad5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:02:21 +0000 Subject: [PATCH 332/868] build(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2 Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3f3c7d2a838..fd459681126 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined - github.com/spf13/cobra v1.10.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 diff --git a/go.sum b/go.sum index 83e076ce182..36604c5c030 100644 --- a/go.sum +++ b/go.sum @@ -287,8 +287,8 @@ github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= From ec41209cad90eb6fa5b375da2f431928138ebdc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:01:36 +0000 Subject: [PATCH 333/868] build(deps): bump tonistiigi/xx from 1.8.0 to 1.9.0 Bumps tonistiigi/xx from 1.8.0 to 1.9.0. --- updated-dependencies: - dependency-name: tonistiigi/xx dependency-version: 1.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ddf99183bb6..d7d5a4d3c42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 ARG KUBO_VERSION=v0.38.2 -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0@sha256:add602d55daca18914838a78221f6bbe4284114b452c86a48f96d59aeb00f5c6 AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-trixie AS build-base From 7e68e60206ea0f4c26eecb9b666284bf836f1c2c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 8 Dec 2025 14:52:56 +0900 Subject: [PATCH 334/868] Refactor container_list_test.go to use Tigron Updates tests to use nerdtest.Setup and the Tigron testing framework as per issue #4613. Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_list_test.go | 84 +++++++++++++------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/cmd/nerdctl/container/container_list_test.go b/cmd/nerdctl/container/container_list_test.go index 751cfabc64c..ebd78c17944 100644 --- a/cmd/nerdctl/container/container_list_test.go +++ b/cmd/nerdctl/container/container_list_test.go @@ -20,43 +20,67 @@ import ( "fmt" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/2598 func TestContainerListWithFormatLabel(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - cID := tID - labelK := "label-key-" + tID - labelV := "label-value-" + tID - - base.Cmd("run", "-d", - "--name", cID, - "--label", labelK+"="+labelV, - testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", cID).AssertOK() - base.Cmd("ps", "-a", - "--filter", "label="+labelK, - "--format", fmt.Sprintf("{{.Label %q}}", labelK)).AssertOutExactly(labelV + "\n") + nerdtest.Setup() + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + labelK := "label-key-" + data.Identifier() + labelV := "label-value-" + data.Identifier() + helpers.Ensure("run", "-d", + "--name", data.Identifier(), + "--label", labelK+"="+labelV, + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + labelK := "label-key-" + data.Identifier() + return helpers.Command("ps", "-a", + "--filter", "label="+labelK, + "--format", fmt.Sprintf("{{.Label %q}}", labelK)) //nolint:dupামিটার + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + labelV := "label-value-" + data.Identifier() + return test.Expects(0, nil, expect.Equals(labelV+"\n"))(data, helpers) + }, + } + testCase.Run(t) } func TestContainerListWithJsonFormatLabel(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - cID := tID - labelK := "label-key-" + tID - labelV := "label-value-" + tID - - base.Cmd("run", "-d", - "--name", cID, - "--label", labelK+"="+labelV, - testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", cID).AssertOK() - base.Cmd("ps", "-a", - "--filter", "label="+labelK, - "--format", "json").AssertOutContains(fmt.Sprintf("%s=%s", labelK, labelV)) + nerdtest.Setup() + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + labelK := "label-key-" + data.Identifier() + labelV := "label-value-" + data.Identifier() + helpers.Ensure("run", "-d", + "--name", data.Identifier(), + "--label", labelK+"="+labelV, + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + labelK := "label-key-" + data.Identifier() + return helpers.Command("ps", "-a", + "--filter", "label="+labelK, + "--format", "json") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + labelK := "label-key-" + data.Identifier() + labelV := "label-value-" + data.Identifier() + return test.Expects(0, nil, expect.Contains(fmt.Sprintf("%s=%s", labelK, labelV)))(data, helpers) + }, + } + testCase.Run(t) } From f4991ec2b6e8fefecce740cc20610d24185637c6 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Fri, 5 Dec 2025 23:57:06 +0900 Subject: [PATCH 335/868] fix: support tmpfs long syntax in compose volumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the current implementation, nerdctl compose command ignores tmpfs configurations specified in the long syntax within the volumes section of compose.yml [1]. [1] https://docs.docker.com/reference/compose-file/services/#long-syntax-6 > - `type`: The mount type. Either `volume`, `bind`, `tmpfs`, `image`, `npipe`, or `cluster` > - `target`: The path in the container where the volume is mounted. > - `read_only`: Flag to set the volume as read-only. > - `tmpfs`: Configures additional tmpfs options: > - `size`: The size for the tmpfs mount in bytes (either numeric or as bytes unit). > - `mode`: The file mode for the tmpfs mount as Unix permission bits as an octal number. Introduced in Docker Compose version [2.14.0](https://docs.docker.com/compose/releases/release-notes/#2260). This behavior has been reported in issue#4556. Therefore, this commit modifies so that when tmpfs is specified using the long syntax in the volumes section, tmpfs is created within the container. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/compose/compose_up_linux_test.go | 70 +++++++++++++++++++ pkg/composer/serviceparser/serviceparser.go | 33 ++++++++- .../serviceparser/serviceparser_test.go | 37 ++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index f3a20190115..d822d9a6d4f 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -671,3 +671,73 @@ services: base.Cmd("images").AssertOutNotContains(testutil.CommonImage) base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertExitCode(1) } + +func TestComposeTmpfsVolume(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := data.Identifier("tmpfs") + composeYAML := fmt.Sprintf(` +services: + tmpfs: + container_name: %s + image: %s + command: sleep infinity + volumes: + - type: tmpfs + target: /target-rw + tmpfs: + size: 64m + - type: tmpfs + target: /target-ro + read_only: true + tmpfs: + size: 64m + mode: 0o1770 +`, containerName, testutil.CommonImage) + + composeYAMLPath := data.Temp().Save(composeYAML, "compose.yaml") + + helpers.Ensure("compose", "-f", composeYAMLPath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, containerName) + + data.Labels().Set("composeYAML", composeYAMLPath) + data.Labels().Set("containerName", containerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "rw tmpfs mount", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-rw", "/proc/mounts") + }, + Expected: test.Expects(0, nil, + expect.All( + expect.Contains("/target-rw"), + expect.Contains("rw"), + expect.Contains("size=65536k"), + ), + ), + }, + { + Description: "ro tmpfs mount with mode", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("containerName"), "grep", "/target-ro", "/proc/mounts") + }, + Expected: test.Expects(0, nil, + expect.All( + expect.Contains("/target-ro"), + expect.Contains("ro"), + expect.Contains("size=65536k"), + expect.Contains("mode=1770"), + ), + ), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") + } + + testCase.Run(t) +} diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 804250f80ec..fac762d69f6 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -699,7 +699,14 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e if err != nil { return nil, err } - c.RunArgs = append(c.RunArgs, "-v="+vStr) + + switch v.Type { + case "tmpfs": + c.RunArgs = append(c.RunArgs, "--tmpfs="+vStr) + default: + c.RunArgs = append(c.RunArgs, "-v="+vStr) + } + c.Mkdir = mkdir } @@ -778,6 +785,7 @@ func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Proj "ReadOnly", "Bind", "Volume", + "Tmpfs", ); len(unknown) > 0 { log.L.Warnf("Ignoring: volume: %+v", unknown) } @@ -800,6 +808,29 @@ func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Proj return "", nil, fmt.Errorf("volume target must be an absolute path, got %q", c.Target) } + if c.Type == "tmpfs" { + var opts []string + + if c.ReadOnly { + opts = append(opts, "ro") + } + if c.Tmpfs != nil { + if c.Tmpfs.Size != 0 { + opts = append(opts, fmt.Sprintf("size=%d", c.Tmpfs.Size)) + } + if c.Tmpfs.Mode != 0 { + opts = append(opts, fmt.Sprintf("mode=%o", c.Tmpfs.Mode)) + } + } + + s := c.Target + if len(opts) > 0 { + s = fmt.Sprintf("%s:%s", s, strings.Join(opts, ",")) + } + + return s, mkdir, nil + } + if c.Source == "" { // anonymous volume s := c.Target diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index ee7a704897d..856d732cbf4 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -439,6 +439,43 @@ services: } } +func TestTmpfsVolumeLongSyntax(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("test is not compatible with windows") + } + + const dockerComposeYAML = ` +services: + foo: + image: nginx:alpine + volumes: + - type: tmpfs + target: /target + read_only: true + tmpfs: + size: 2G + mode: 0o1770 +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + fooSvc, err := project.GetService("foo") + assert.NilError(t, err) + + foo, err := Parse(project, fooSvc) + assert.NilError(t, err) + + t.Logf("foo: %+v", foo) + for _, c := range foo.Containers { + assert.Assert(t, in(c.RunArgs, "--tmpfs=/target:ro,size=2147483648,mode=1770")) + } +} + func TestParseNetworkMode(t *testing.T) { t.Parallel() const dockerComposeYAML = ` From 025f455ca8d941f029f2cd5989737e2fafb26c77 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 8 Dec 2025 15:47:09 +0900 Subject: [PATCH 336/868] Refactor container_exec_test.go to use Tigron Updates tests to use nerdtest.Setup and the Tigron testing framework as per issue #4613. Replaced base.Cmd with helpers.Command and base.Assert with test.Expects. Also updated TestExecStdin to use cmd.Feed instead of WithStdin. Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_exec_test.go | 181 ++++++++++--------- 1 file changed, 99 insertions(+), 82 deletions(-) diff --git a/cmd/nerdctl/container/container_exec_test.go b/cmd/nerdctl/container/container_exec_test.go index f5a15e3572a..d1a14e9d410 100644 --- a/cmd/nerdctl/container/container_exec_test.go +++ b/cmd/nerdctl/container/container_exec_test.go @@ -17,107 +17,124 @@ package container import ( - "errors" "runtime" "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/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestExec(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testContainer := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() - base.EnsureContainerStarted(testContainer) - - base.Cmd("exec", testContainer, "echo", "success").AssertOutExactly("success\n") + nerdtest.Setup() + + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("exec", data.Identifier(), "echo", "success") + }, + Expected: test.Expects(0, nil, expect.Equals("success\n")), + } + testCase.Run(t) } func TestExecWithDoubleDash(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - testContainer := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() - base.EnsureContainerStarted(testContainer) - - base.Cmd("exec", testContainer, "--", "echo", "success").AssertOutExactly("success\n") + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("exec", data.Identifier(), "--", "echo", "success") + }, + Expected: test.Expects(0, nil, expect.Equals("success\n")), + } + testCase.Run(t) } func TestExecStdin(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - testContainer := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainer).Run() - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() - base.EnsureContainerStarted(testContainer) + nerdtest.Setup() const testStr = "test-exec-stdin" - opts := []func(*testutil.Cmd){ - testutil.WithStdin(strings.NewReader(testStr)), + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("exec", "-i", data.Identifier(), "cat") + cmd.Feed(strings.NewReader(testStr)) + return cmd + }, + Expected: test.Expects(0, nil, expect.Equals(testStr)), } - base.Cmd("exec", "-i", testContainer, "cat").CmdOption(opts...).AssertOutExactly(testStr) + testCase.Run(t) } // FYI: https://github.com/containerd/nerdctl/blob/e4b2b6da56555dc29ed66d0fd8e7094ff2bc002d/cmd/nerdctl/run_test.go#L177 func TestExecEnv(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testContainer := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK() - base.EnsureContainerStarted(testContainer) - - base.Env = append(base.Env, "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host") - base.Cmd("exec", - "--env", "FOO=foo1,foo2", - "--env", "BAR=bar1 bar2", - "--env", "BAZ=", - "--env", "QUX", // not exported in OS - "--env", "QUUX=quux1", - "--env", "QUUX=quux2", - "--env", "CORGE", // OS exported - "--env", "GRAULT=grault_key=grault_value", // value contains `=` char - "--env", "GARPLY=", // OS exported - "--env", "WALDO=", // not exported in OS - - testContainer, "env").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "\nFOO=foo1,foo2\n") { - return errors.New("got bad FOO") - } - if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") { - return errors.New("got bad BAR") - } - if !strings.Contains(stdout, "\nBAZ=\n") && runtime.GOOS != "windows" { - return errors.New("got bad BAZ") - } - if strings.Contains(stdout, "QUX") { - return errors.New("got bad QUX (should not be set)") - } - if !strings.Contains(stdout, "\nQUUX=quux2\n") { - return errors.New("got bad QUUX") - } - if !strings.Contains(stdout, "\nCORGE=corge-value-in-host\n") { - return errors.New("got bad CORGE") - } - if !strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n") { - return errors.New("got bad GRAULT") - } - if !strings.Contains(stdout, "\nGARPLY=\n") && runtime.GOOS != "windows" { - return errors.New("got bad GARPLY") - } - if !strings.Contains(stdout, "\nWALDO=\n") && runtime.GOOS != "windows" { - return errors.New("got bad WALDO") - } - - return nil - }) + nerdtest.Setup() + + testCase := &test.Case{ + Env: map[string]string{ + "CORGE": "corge-value-in-host", + "GARPLY": "garply-value-in-host", + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + 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("exec", + "--env", "FOO=foo1,foo2", + "--env", "BAR=bar1 bar2", + "--env", "BAZ=", + "--env", "QUX", // not exported in OS + "--env", "QUUX=quux1", + "--env", "QUUX=quux2", + "--env", "CORGE", // OS exported + "--env", "GRAULT=grault_key=grault_value", // value contains `=` char + "--env", "GARPLY=", // OS exported + "--env", "WALDO=", // not exported in OS + + data.Identifier(), "env") + }, + Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "\nFOO=foo1,foo2\n"), "got bad FOO") + assert.Assert(t, strings.Contains(stdout, "\nBAR=bar1 bar2\n"), "got bad BAR") + if runtime.GOOS != "windows" { + assert.Assert(t, strings.Contains(stdout, "\nBAZ=\n"), "got bad BAZ") + } + assert.Assert(t, !strings.Contains(stdout, "QUX"), "got bad QUX (should not be set)") + assert.Assert(t, strings.Contains(stdout, "\nQUUX=quux2\n"), "got bad QUUX") + assert.Assert(t, strings.Contains(stdout, "\nCORGE=corge-value-in-host\n"), "got bad CORGE") + assert.Assert(t, strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n"), "got bad GRAULT") + if runtime.GOOS != "windows" { + assert.Assert(t, strings.Contains(stdout, "\nGARPLY=\n"), "got bad GARPLY") + assert.Assert(t, strings.Contains(stdout, "\nWALDO=\n"), "got bad WALDO") + } + }), + } + testCase.Run(t) } From a853c646ea20bd3397cfb9ec8bf77c024b4d1428 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:02:19 +0000 Subject: [PATCH 337/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.8 to 0.15.9. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.8...v0.15.9) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bc6da5f1cb1..f84bea58e9d 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.8 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.9 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4ca7e8d0d25..d9e01ac54f4 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.8 h1:UnXnbb1ZpxvOUcOmR0i31cLfuqkU9hXZraL/9EiFVWk= -github.com/containerd/nydus-snapshotter v0.15.8/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= +github.com/containerd/nydus-snapshotter v0.15.9 h1:yFPr75WyO49sWJiBFMKNZo6kK4ed2hc13YxGn5KHWCU= +github.com/containerd/nydus-snapshotter v0.15.9/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From d5feed79d9641645c494573a3aed65e233e59faf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:19:59 +0000 Subject: [PATCH 338/868] build(deps): bump github.com/containernetworking/plugins Bumps [github.com/containernetworking/plugins](https://github.com/containernetworking/plugins) from 1.8.0 to 1.9.0. - [Release notes](https://github.com/containernetworking/plugins/releases) - [Commits](https://github.com/containernetworking/plugins/compare/v1.8.0...v1.9.0) --- updated-dependencies: - dependency-name: github.com/containernetworking/plugins dependency-version: 1.9.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bc6da5f1cb1..2c7c16c71a7 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/containerd/stargz-snapshotter/ipfs v0.18.1 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined - github.com/containernetworking/plugins v1.8.0 //gomodjail:unconfined + github.com/containernetworking/plugins v1.9.0 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4ca7e8d0d25..e6521a95c4d 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= 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.8.0 h1:WjGbV/0UQyo8A4qBsAh6GaDAtu1hevxVxsEuqtBqUFk= -github.com/containernetworking/plugins v1.8.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= +github.com/containernetworking/plugins v1.9.0 h1:Mg3SXBdRGkdXyFC4lcwr6u2ZB2SDeL6LC3U+QrEANuQ= +github.com/containernetworking/plugins v1.9.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= From 6fd5210fba0a66b06ffbffbc806980318bf371c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:02:20 +0000 Subject: [PATCH 339/868] build(deps): bump the golang-x group with 6 updates Bumps the golang-x group with 6 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.45.0` | `0.46.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.47.0` | `0.48.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.18.0` | `0.19.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.38.0` | `0.39.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.37.0` | `0.38.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.31.0` | `0.32.0` | Updates `golang.org/x/crypto` from 0.45.0 to 0.46.0 - [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.46.0) Updates `golang.org/x/net` from 0.47.0 to 0.48.0 - [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0) Updates `golang.org/x/sync` from 0.18.0 to 0.19.0 - [Commits](https://github.com/golang/sync/compare/v0.18.0...v0.19.0) Updates `golang.org/x/sys` from 0.38.0 to 0.39.0 - [Commits](https://github.com/golang/sys/compare/v0.38.0...v0.39.0) Updates `golang.org/x/term` from 0.37.0 to 0.38.0 - [Commits](https://github.com/golang/term/compare/v0.37.0...v0.38.0) Updates `golang.org/x/text` from 0.31.0 to 0.32.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.32.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 14 +++++++------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index cb20e56da4d..3406588832a 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.45.0 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 //gomodjail:unconfined - golang.org/x/sys v0.38.0 //gomodjail:unconfined - golang.org/x/term v0.37.0 //gomodjail:unconfined - golang.org/x/text v0.31.0 + golang.org/x/crypto v0.46.0 + golang.org/x/net v0.48.0 + golang.org/x/sync v0.19.0 //gomodjail:unconfined + golang.org/x/sys v0.39.0 //gomodjail:unconfined + golang.org/x/term v0.38.0 //gomodjail:unconfined + golang.org/x/text v0.32.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined ) @@ -135,7 +135,7 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.29.0 // indirect + golang.org/x/mod v0.30.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined google.golang.org/grpc v1.76.0 // indirect diff --git a/go.sum b/go.sum index a5edd634e26..9acb912011b 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= @@ -376,8 +376,8 @@ 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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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= @@ -394,8 +394,8 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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= @@ -408,8 +408,8 @@ 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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 +432,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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 +443,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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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,8 +454,8 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= @@ -468,8 +468,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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= From 151623b1bcd39b8b07bca792364d573fd29803db Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 11 Dec 2025 13:38:11 +0900 Subject: [PATCH 340/868] --help: fix output Signed-off-by: Akihiro Suda --- cmd/nerdctl/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 51dfb26736e..375fc2d45b7 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -111,7 +111,7 @@ func usage(c *cobra.Command) error { t += "\n" return t } - s += printCommands("helpers.Management commands", managementCommands) + s += printCommands("Management commands", managementCommands) s += printCommands("Commands", nonManagementCommands) s += Bold("Flags") + ":\n" From a3411d2fd7790f96b5fa5f1c123c03b0b0b317bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:01:49 +0000 Subject: [PATCH 341/868] build(deps): bump actions/cache from 4.3.0 to 5.0.0 Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 5.0.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/0057852bfaa89a56745cba8c7296529d2fc39830...a7833574556fa59680c1b7cb190c1735db73ebf0) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 6d9fafb81c1..8a01fa2c673 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index fbd67dad75f..61e35e9507e 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 with: path: /root/.vagrant.d key: vagrant From 7c2a81dabc453bc234f1b91b3a35ca1c7bae50c8 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Fri, 12 Dec 2025 19:31:15 +0900 Subject: [PATCH 342/868] docs: add additional nerdtest `Requirement` Signed-off-by: Hayato Kiwata --- docs/testing/tools.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/testing/tools.md b/docs/testing/tools.md index ec1eb9056a7..1274c51b4a7 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -394,14 +394,29 @@ nerdtest.Soci // a test requires the soci snapshotter nerdtest.Stargz // a test requires the stargz snapshotter nerdtest.Rootless // a test requires Rootless nerdtest.Rootful // a test requires Rootful +nerdtest.RootlessWithDetachNetNS // a test requires rootless with detached netns (RootlessKit v2) +nerdtest.RootlessWithoutDetachNetNS // a test requires rootless without detached netns (RootlessKit v1) nerdtest.Build // a test requires buildkit nerdtest.CGroup // a test requires cgroup +nerdtest.CgroupsAccessible // a test requires cgroup; passes if rootful, or rootless with cgroup v2 +nerdtest.CGroupV2 // a test requires cgroup v2 nerdtest.NerdctlNeedsFixing // indicates that a test cannot be run on nerdctl yet as a fix is required nerdtest.BrokenTest // indicates that a test needs to be fixed and has been restricted to run only in certain cases nerdtest.OnlyIPv6 // a test is meant to run solely in the ipv6 environment nerdtest.OnlyKubernetes // a test is meant to run solely in the Kubernetes environment nerdtest.IsFlaky // indicates that a test will fail in a flaky way - this may be the test fault, or more likely something racy in nerdctl nerdtest.Private // see below +nerdtest.Registry // a test requires a registry to be deployed +nerdtest.IPFS // a test requires ipfs (binary present) +nerdtest.Gomodjail // a test requires the target binary to be packed with gomodjail +nerdtest.AllowModifyUserns // a test requires allow-modify-userns to be enabled +nerdtest.RemapIDs // a test requires snapshotter to support ID remapping +nerdtest.HyperV // a test requires Hyper-V (Windows) + +nerdtest.Info(func(info dockercompat.Info) error { ... }) // `nerdctl info` should satisfy custom conditions +nerdtest.SociVersion("0.10.0") // SOCI snapshotter version check +nerdtest.ContainerdVersion("2.0.0") // containerd version check +nerdtest.CNIFirewallVersion("1.7.1") // CNI firewall plugin version check ``` ### About `nerdtest.Private` From b7853787e9d9e22b5422f8ae18b56296acba0b87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:01:43 +0000 Subject: [PATCH 343/868] build(deps): bump actions/cache from 5.0.0 to 5.0.1 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/a7833574556fa59680c1b7cb190c1735db73ebf0...9255dc7a253b0ccc959486e2bca901246202afeb) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 8a01fa2c673..ebfddd0dbab 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 61e35e9507e..37ea275605d 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: /root/.vagrant.d key: vagrant From deb3bff41d1af32ff24ec5c0d69d5b57be052954 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:01:53 +0000 Subject: [PATCH 344/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.2+incompatible to 29.1.3+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.2...v29.1.3) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.1.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3406588832a..fe12a205101 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.3+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 9acb912011b..1fded43dc23 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8= -github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c= +github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 8221594a075f000e5cccbfea4c0572db44e0d109 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 15 Dec 2025 20:39:13 +0900 Subject: [PATCH 345/868] test: refactor compose_up_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- cmd/nerdctl/compose/compose_up_linux_test.go | 1161 +++++++++++++----- 1 file changed, 867 insertions(+), 294 deletions(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index d822d9a6d4f..6e0476b859c 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -19,37 +19,44 @@ package compose import ( "fmt" "io" - "os" + "path/filepath" + "strconv" "strings" "testing" - "time" "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" - "github.com/containerd/log" "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/composer/serviceparser" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "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 TestComposeUp(t *testing.T) { - base := testutil.NewBase(t) - helpers.ComposeUp(t, base, fmt.Sprintf(` -services: + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } + composeYAML := fmt.Sprintf(` +services: wordpress: image: %s restart: always ports: - - 8080:80 + - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -57,7 +64,6 @@ services: WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html - db: image: %s restart: always @@ -72,52 +78,142 @@ services: volumes: wordpress: db: -`, testutil.WordpressImage, testutil.MariaDBImage)) +`, testutil.WordpressImage, hostPort, testutil.MariaDBImage) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + wordpressContainerName := serviceparser.DefaultContainerName(projectName, "wordpress", "1") + dbContainerName := serviceparser.DefaultContainerName(projectName, "db", "1") + + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("projectName", projectName) + data.Labels().Set("wordpressContainerName", wordpressContainerName) + data.Labels().Set("dbContainerName", dbContainerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, wordpressContainerName) + nerdtest.EnsureContainerStarted(helpers, dbContainerName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.Contains(data.Labels().Get("wordpressContainerName")), + expect.Contains(data.Labels().Get("dbContainerName")), + ), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + if p := data.Labels().Get("hostPort"); p != "" { + if port, err := strconv.Atoi(p); err == nil { + _ = portlock.Release(port) + } + } + if projectName := data.Labels().Get("projectName"); projectName != "" { + helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 1}) + helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 1}) + } + } + + testCase.Run(t) } func TestComposeUpBuild(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } - const dockerComposeYAML = ` + composeYAML := fmt.Sprintf(` services: web: build: . ports: - - 8080:80 -` - dockerfile := fmt.Sprintf(`FROM %s + - %d:80 +`, hostPort) + dockerfile := fmt.Sprintf(`FROM %s COPY index.html /usr/share/nginx/html/index.html `, testutil.NginxAlpineImage) - indexHTML := t.Name() + indexHTML := data.Identifier("indexHTML") - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(dockerfile, "Dockerfile") + data.Temp().Save(indexHTML, "index.html") - comp.WriteFile("Dockerfile", dockerfile) - comp.WriteFile("index.html", indexHTML) + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("indexHTML", data.Temp().Path("index.html")) - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 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), indexHTML)) + helpers.Ensure("compose", "-f", composePath, "up", "-d", "--build") + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "web", "1")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "HTTP request to the web container", + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + host := fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")) + resp, err := nettestutil.HTTPGet(host, 5, false) + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + t.Log(fmt.Sprintf("respBody=%q", respBody)) + assert.Assert(t, strings.Contains(string(respBody), data.Labels().Get("indexHTML"))) + }, + } + }, + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if portStr := data.Labels().Get("hostPort"); portStr != "" { + port, _ := strconv.Atoi(portStr) + _ = portlock.Release(port) + } + } + + testCase.Run(t) } func TestComposeUpNetWithStaticIP(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Static IP assignment is not supported rootless mode yet.") - } - base := testutil.NewBase(t) - staticIP := "172.20.0.12" - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + staticIP := "10.4.255.254" + subnet := "10.4.255.0/24" + var composeYAML = fmt.Sprintf(` services: svc0: image: %s @@ -129,31 +225,55 @@ networks: net0: ipam: config: - - subnet: 172.20.0.0/24 -`, testutil.NginxAlpineImage, staticIP) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") - inspectCmd := base.Cmd("inspect", svc0, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") - result := inspectCmd.Run() - stdoutContent := result.Stdout() + result.Stderr() - assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent) - if !strings.Contains(stdoutContent, staticIP) { - log.L.Errorf("test failed, the actual container ip is %s", stdoutContent) - t.Fail() - return + - subnet: %s +`, testutil.NginxAlpineImage, staticIP, subnet) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + containerName := serviceparser.DefaultContainerName(projectName, "svc0", "1") + + data.Labels().Set("staticIP", staticIP) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("containerName", containerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, containerName) } + + testCase.SubTests = []*test.Case{ + { + Description: "static IP is assigned to container", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("containerName"), "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("staticIP"))) + }, + } + }, + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeUpMultiNet(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: svc0: image: %s @@ -176,126 +296,269 @@ networks: net1: {} net2: {} `, testutil.NginxAlpineImage, testutil.NginxAlpineImage, testutil.NginxAlpineImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") + svc1 := serviceparser.DefaultContainerName(projectName, "svc1", "1") + svc2 := serviceparser.DefaultContainerName(projectName, "svc2", "1") + + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("svc0", svc0) + data.Labels().Set("svc1", svc1) + data.Labels().Set("svc2", svc2) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, svc0) + nerdtest.EnsureContainerStarted(helpers, svc1) + nerdtest.EnsureContainerStarted(helpers, svc2) + } + + testCase.SubTests = []*test.Case{ + { + Description: "svc0 can ping itself", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc0") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "svc0 can ping svc1", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc1") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "svc0 can ping svc2", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc0"), "ping", "-c", "1", "svc2") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "svc1 can ping svc0", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc1"), "ping", "-c", "1", "svc0") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "svc2 can ping svc0", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc2"), "ping", "-c", "1", "svc0") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "svc1 cannot ping svc2", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("svc1"), "ping", "-c", "1", "svc2") + }, + Expected: test.Expects(1, nil, nil), + }, + } - svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") - svc1 := serviceparser.DefaultContainerName(projectName, "svc1", "1") - svc2 := serviceparser.DefaultContainerName(projectName, "svc2", "1") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - base.Cmd("exec", svc0, "ping", "-c", "1", "svc0").AssertOK() - base.Cmd("exec", svc0, "ping", "-c", "1", "svc1").AssertOK() - base.Cmd("exec", svc0, "ping", "-c", "1", "svc2").AssertOK() - base.Cmd("exec", svc1, "ping", "-c", "1", "svc0").AssertOK() - base.Cmd("exec", svc2, "ping", "-c", "1", "svc0").AssertOK() - base.Cmd("exec", svc1, "ping", "-c", "1", "svc2").AssertFail() + testCase.Run(t) } func TestComposeUpOsEnvVar(t *testing.T) { - base := testutil.NewBase(t) - const containerName = "nginxAlpine" - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Env = map[string]string{ + "ADDRESS": "0.0.0.0", + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + const containerName = "nginxAlpine" + + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } + + var composeYAML = fmt.Sprintf(` services: svc1: image: %s container_name: %s ports: - - ${ADDRESS:-127.0.0.1}:8080:80 -`, testutil.NginxAlpineImage, containerName) + - ${ADDRESS:-127.0.0.1}:%d:80 +`, testutil.NginxAlpineImage, containerName, hostPort) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") - base.Env = append(base.Env, "ADDRESS=0.0.0.0") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + data.Labels().Set("containerName", containerName) + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + data.Labels().Set("composeYAML", composePath) - inspect := base.InspectContainer(containerName) - inspect80TCP := (*inspect.NetworkSettings.Ports)["80/tcp"] - expected := nat.PortBinding{ - HostIP: "0.0.0.0", - HostPort: "8080", + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, containerName) } - assert.Equal(base.T, expected, inspect80TCP[0]) + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "inspect", data.Labels().Get("containerName")) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc), "unexpected number of containers") + inspect80TCP := (*dc[0].NetworkSettings.Ports)["80/tcp"] + assert.Assert(t, len(inspect80TCP) > 0, "no host bindings for 80/tcp") + expected := nat.PortBinding{ + HostIP: "0.0.0.0", + HostPort: data.Labels().Get("hostPort"), + } + assert.Equal(t, expected, inspect80TCP[0]) + }), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeUpDotEnvFile(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = ` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = ` services: svc3: image: ghcr.io/stargz-containers/nginx:$TAG ` - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(`TAG=1.19-alpine-org`, ".env") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("composeYAML", composePath) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + } + + testCase.Expected = test.Expects(0, nil, nil) - envFile := `TAG=1.19-alpine-org` - comp.WriteFile(".env", envFile) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Run(t) } func TestComposeUpEnvFileNotFoundError(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = ` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = ` services: svc4: image: ghcr.io/stargz-containers/nginx:$TAG ` - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(`TAG=1.19-alpine-org`, "envFile") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("composeYAML", composePath) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // env-file is relative to the current working directory and not the project directory + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "--env-file", "envFile", "up", "-d") + } + + testCase.Expected = test.Expects(1, nil, nil) - envFile := `TAG=1.19-alpine-org` - comp.WriteFile("envFile", envFile) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } - //env-file is relative to the current working directory and not the project directory - base.ComposeCmd("-f", comp.YAMLFullPath(), "--env-file", "envFile", "up", "-d").AssertFail() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Run(t) } func TestComposeUpWithScale(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + test1 := serviceparser.DefaultContainerName(projectName, "test", "1") + test2 := serviceparser.DefaultContainerName(projectName, "test", "2") - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--scale", "test=2").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("test1", test1) + data.Labels().Set("test2", test2) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutContains(serviceparser.DefaultContainerName(projectName, "test", "2")) + helpers.Ensure("compose", "-f", composePath, "up", "-d", "--scale", "test=2") + nerdtest.EnsureContainerStarted(helpers, test1) + nerdtest.EnsureContainerStarted(helpers, test2) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.Contains(data.Labels().Get("test1")), + expect.Contains(data.Labels().Get("test2")), + ), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeIPAMConfig(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: foo: image: %s @@ -308,79 +571,148 @@ networks: - subnet: 10.1.100.0/24 `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + fooContainer := serviceparser.DefaultContainerName(projectName, "foo", "1") + + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("fooContainer", fooContainer) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, fooContainer) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", "-f", "{{json .NetworkSettings.Networks }}", data.Labels().Get("fooContainer")) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains("10.1.100.")) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - base.Cmd("inspect", "-f", `{{json .NetworkSettings.Networks }}`, serviceparser.DefaultContainerName(projectName, "foo", "1")).AssertOutContains("10.1.100.") + testCase.Run(t) } func TestComposeUpRemoveOrphans(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var ( - dockerComposeYAMLOrphan = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var ( + dockerComposeYAMLOrphan = fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) - dockerComposeYAMLFull = fmt.Sprintf(` + dockerComposeYAMLFull = fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) - ) + ) + + composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") + composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") + + projectName := data.Identifier("project") + t.Logf("projectName=%q", projectName) + + testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") + orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") - compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) - defer compOrphan.CleanUp() - compFull := testutil.NewComposeDir(t, dockerComposeYAMLFull) - defer compFull.CleanUp() + data.Labels().Set("composeOrphanPath", composeOrphanPath) + data.Labels().Set("composeFullPath", composeFullPath) + data.Labels().Set("projectName", projectName) + data.Labels().Set("orphanContainer", orphanContainer) - projectName := fmt.Sprintf("nerdctl-compose-test-%d", time.Now().Unix()) - t.Logf("projectName=%q", projectName) + helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") + helpers.Ensure("compose", "-p", projectName, "-f", composeOrphanPath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, testContainer) + nerdtest.EnsureContainerStarted(helpers, orphanContainer) - orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") + helpers.Command("compose", "-p", projectName, "-f", composeFullPath, "ps").Run( + &test.Expected{ + ExitCode: 0, + Output: expect.Contains(orphanContainer), + }, + ) + helpers.Ensure("compose", "-p", projectName, "-f", composeOrphanPath, "up", "-d", "--remove-orphans") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFullPath"), "ps") + } - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run() - base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d").AssertOK() - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutContains(orphanContainer) - base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d", "--remove-orphans").AssertOK() - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutNotContains(orphanContainer) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(data.Labels().Get("orphanContainer")), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeOrphanPath") != "" { + helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphanPath"), "down", "-v") + } + if data.Labels().Get("composeFullPath") != "" { + helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFullPath"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeUpIdempotent(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("composeYAML", composePath) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "test", "1")) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + } + + testCase.Expected = test.Expects(0, nil, nil) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeUpNoRecreateDependencies(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: foo: image: %s @@ -392,29 +724,61 @@ services: - foo `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "foo").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + fooContainer := serviceparser.DefaultContainerName(projectName, "foo", "1") + barContainer := serviceparser.DefaultContainerName(projectName, "bar", "1") - fooName := serviceparser.DefaultContainerName(projectName, "foo", "1") - id1Cmd := base.Cmd("inspect", fooName, "--format", "{{.Id}}") - id1Res := id1Cmd.Run() - out1 := strings.TrimSpace(id1Res.Stdout()) - assert.Assert(id1Cmd.Base.T, id1Res.ExitCode == 0, id1Res.Stdout()+id1Res.Stderr()) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("projectName", projectName) + data.Labels().Set("fooContainer", fooContainer) + data.Labels().Set("barContainer", barContainer) + } - // Bring up dependent service; ensure foo is not recreated (ID unchanged) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "bar").AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "foo is not recreated when starting bar", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "foo") + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("fooContainer")) + + helpers.Command("inspect", data.Labels().Get("fooContainer"), "--format", "{{.Id}}").Run( + &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + data.Labels().Set("fooContainerID", strings.TrimSpace(stdout)) + }, + }, + ) + + // Bring up dependent service; ensure foo is not recreated (ID unchanged) + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "bar") + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("barContainer")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("fooContainer"), "--format", "{{.Id}}") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("fooContainerID")) + }, + } + }, + }, + } - id2Cmd := base.Cmd("inspect", fooName, "--format", "{{.Id}}") - id2Res := id2Cmd.Run() - out2 := strings.TrimSpace(id2Res.Stdout()) - assert.Assert(id2Cmd.Base.T, id2Res.ExitCode == 0, id2Res.Stdout()+id2Res.Stderr()) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - assert.Equal(base.T, out1, out2) + testCase.Run(t) } func TestComposeUpWithExternalNetwork(t *testing.T) { @@ -477,22 +841,30 @@ networks: } func TestComposeUpWithBypass4netns(t *testing.T) { - // docker does not support bypass4netns mode - testutil.DockerIncompatible(t) - if !rootlessutil.IsRootless() { - t.Skip("test needs rootless") - } - testutil.RequireKernelVersion(t, ">= 5.9.0-0") - testutil.RequireSystemService(t, "bypass4netnsd") - base := testutil.NewBase(t) - helpers.ComposeUp(t, base, fmt.Sprintf(` -services: + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.Rootless, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testutil.RequireKernelVersion(t, ">= 5.9.0-0") + testutil.RequireSystemService(t, "bypass4netnsd") + + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } + + composeYAML := fmt.Sprintf(` +services: wordpress: image: %s restart: always ports: - - 8080:80 + - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -502,7 +874,6 @@ services: - wordpress:/var/www/html annotations: - nerdctl/bypass4netns=1 - db: image: %s restart: always @@ -519,21 +890,68 @@ services: volumes: wordpress: db: -`, testutil.WordpressImage, testutil.MariaDBImage)) +`, testutil.WordpressImage, hostPort, testutil.MariaDBImage) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("projectName", projectName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "wordpress", "1")) + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "db", "1")) + + helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 0}) + helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 0}) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(_ string, tt tig.T) { + host := fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")) + resp, err := nettestutil.HTTPGet(host, 5, false) + assert.NilError(tt, err) + body, err := io.ReadAll(resp.Body) + assert.NilError(tt, err) + _ = resp.Body.Close() + assert.Assert(tt, strings.Contains(string(body), testutil.WordpressIndexHTMLSnippet)) + t.Log("wordpress seems functional") + }, + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + if p := data.Labels().Get("hostPort"); p != "" { + if port, err := strconv.Atoi(p); err == nil { + _ = portlock.Release(port) + } + } + + if projectName := data.Labels().Get("projectName"); projectName != "" { + helpers.Command("volume", "inspect", fmt.Sprintf("%s_db", projectName)).Run(&test.Expected{ExitCode: 1}) + helpers.Command("network", "inspect", fmt.Sprintf("%s_default", projectName)).Run(&test.Expected{ExitCode: 1}) + } + } + + testCase.Run(t) } func TestComposeUpProfile(t *testing.T) { - base := testutil.NewBase(t) - serviceRegular := testutil.Identifier(t) + "-regular" - serviceProfiled := testutil.Identifier(t) + "-profiled" + testCase := nerdtest.Setup() - // write the env.common file to tmpdir - tmpDir := t.TempDir() - envFilePath := fmt.Sprintf("%s/env.common", tmpDir) - err := os.WriteFile(envFilePath, []byte("TEST_ENV_INJECTION=WORKS\n"), 0644) - assert.NilError(t, err) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + serviceRegular := data.Identifier("regular") + serviceProfiled := data.Identifier("profiled") + + envFilePath := data.Temp().Save(`TEST_ENV_INJECTION=WORKS\n`, "env.common") - dockerComposeYAML := fmt.Sprintf(` + composeYAML := fmt.Sprintf(` services: %s: image: %[3]s @@ -546,41 +964,84 @@ services: - %[4]s `, serviceRegular, serviceProfiled, testutil.NginxAlpineImage, envFilePath) - // * Test with profile - // Should run both the services: - // - matching active profile - // - one without profile - comp1 := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp1.CleanUp() - base.ComposeCmd("-f", comp1.YAMLFullPath(), "--profile", "test-profile", "up", "-d").AssertOK() - - psCmd := base.Cmd("ps", "-a", "--format={{.Names}}") - psCmd.AssertOutContains(serviceRegular) - psCmd.AssertOutContains(serviceProfiled) - - execCmd := base.ComposeCmd("-f", comp1.YAMLFullPath(), "exec", serviceProfiled, "env") - execCmd.AssertOutContains("TEST_ENV_INJECTION=WORKS") - - base.ComposeCmd("-f", comp1.YAMLFullPath(), "--profile", "test-profile", "down", "-v").AssertOK() - - // * Test without profile - // Should run: - // - service without profile - comp2 := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp2.CleanUp() - base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").AssertOK() - - psCmd = base.Cmd("ps", "-a", "--format={{.Names}}") - psCmd.AssertOutContains(serviceRegular) - psCmd.AssertOutNotContains(serviceProfiled) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("serviceRegular", serviceRegular) + data.Labels().Set("serviceProfiled", serviceProfiled) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("regularContainer", serviceparser.DefaultContainerName(projectName, serviceRegular, "1")) + data.Labels().Set("profiledContainer", serviceparser.DefaultContainerName(projectName, serviceProfiled, "1")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "with profile", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "--profile", "test-profile", "up", "-d") + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("profiledContainer")) + + helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "exec", data.Labels().Get("serviceProfiled"), "env"). + Run(&test.Expected{ + ExitCode: 0, + Output: expect.Contains("TEST_ENV_INJECTION=WORKS"), + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--format={{.Names}}") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.Contains(data.Labels().Get("serviceRegular")), + expect.Contains(data.Labels().Get("serviceProfiled")), + ), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "--profile", "test-profile", "down", "-v") + }, + }, + { + Description: "profiled not started without profile flag", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--format={{.Names}}") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.Contains(data.Labels().Get("serviceRegular")), + expect.DoesNotContain(data.Labels().Get("serviceProfiled")), + ), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + }, + }, + } + + testCase.Run(t) } func TestComposeUpAbortOnContainerExit(t *testing.T) { - base := testutil.NewBase(t) - serviceRegular := "regular" - serviceProfiled := "exited" - dockerComposeYAML := fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + serviceRegular := data.Identifier("regular") + serviceProfiled := data.Identifier("exited") + composeYAML := fmt.Sprintf(` services: %s: image: %s @@ -588,75 +1049,173 @@ services: image: %s entrypoint: /bin/sh -c "exit 1" `, serviceRegular, testutil.NginxAlpineImage, serviceProfiled, testutil.BusyboxImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - // here we run 'compose up --abort-on-container-exit' command - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--abort-on-container-exit").AssertExitCode(1) - time.Sleep(3 * time.Second) - psCmd := base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") - - psCmd.AssertOutContains(serviceRegular) - psCmd.AssertOutContains(serviceProfiled) - base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // this time we run 'compose up' command without --abort-on-container-exit flag - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - time.Sleep(3 * time.Second) - psCmd = base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") - - // this time the regular service should not be listed in the output - psCmd.AssertOutNotContains(serviceRegular) - psCmd.AssertOutContains(serviceProfiled) - base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // in this sub-test we are ensuring that flags '-d' and '--abort-on-container-exit' cannot be ran together - c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--abort-on-container-exit") - expected := icmd.Expected{ - ExitCode: 1, - } - c.Assert(expected) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("serviceRegular", serviceRegular) + data.Labels().Set("serviceProfiled", serviceProfiled) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("regularContainer", serviceparser.DefaultContainerName(projectName, serviceRegular, "1")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "abort on container exit", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--abort-on-container-exit").Run( + &test.Expected{ + ExitCode: 1, + }, + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.Contains(data.Labels().Get("serviceRegular")), + expect.Contains(data.Labels().Get("serviceProfiled")), + ), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + }, + }, + { + Description: "no abort flag keeps other services running", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("regularContainer")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.DoesNotContain(data.Labels().Get("serviceRegular")), + expect.Contains(data.Labels().Get("serviceProfiled")), + ), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + }, + }, + // in this sub-test we are ensuring that flags '-d' and '--abort-on-container-exit' cannot be ran together + { + Description: "flag -d incompatible with --abort-on-container-exit", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "-d", "--abort-on-container-exit") + }, + Expected: test.Expects(1, nil, nil), + }, + } + + testCase.Run(t) } func TestComposeUpPull(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.NoParallel = true + testCase.Require = nerdtest.Private + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - // Cases where pull is required - for _, pull := range []string{"missing", "always"} { - t.Run(fmt.Sprintf("pull=%s", pull), func(t *testing.T) { - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.Cmd("images").AssertOutNotContains(testutil.CommonImage) - t.Cleanup(func() { - base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() - }) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", pull).AssertOutContains("hi") - }) - } - - t.Run("pull=never, no pull", func(t *testing.T) { - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.Cmd("images").AssertOutNotContains(testutil.CommonImage) - t.Cleanup(func() { - base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() - }) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", "never").AssertExitCode(1) - }) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + data.Labels().Set("composeYAML", composePath) + } + + testCase.SubTests = []*test.Case{ + { + Description: "pull=missing", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Command("images").Run( + &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(testutil.CommonImage), + }, + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "missing") + }, + Expected: test.Expects(0, nil, expect.Contains("hi")), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") + }, + }, + { + Description: "pull=always", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Command("images").Run( + &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(testutil.CommonImage), + }, + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "always") + }, + Expected: test.Expects(0, nil, expect.Contains("hi")), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") + }, + }, + { + Description: "pull=never, no pull", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Command("images").Run( + &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(testutil.CommonImage), + }, + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up", "--pull", "never") + }, + Expected: test.Expects(1, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down") + }, + }, + } + + testCase.Run(t) } func TestComposeUpServicePullPolicy(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.Private - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: test: image: %s @@ -664,12 +1223,26 @@ services: pull_policy: "never" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + data.Labels().Set("composeYAML", composePath) + + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Command("images").Run( + &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(testutil.CommonImage), + }, + ) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "up") + } - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.Cmd("images").AssertOutNotContains(testutil.CommonImage) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertExitCode(1) + testCase.Expected = test.Expects(1, nil, nil) + + testCase.Run(t) } func TestComposeTmpfsVolume(t *testing.T) { From e5d183ff6ed0b606613e69500369af0b800e9713 Mon Sep 17 00:00:00 2001 From: Swapnanil-Gupta Date: Mon, 15 Dec 2025 19:58:34 +0000 Subject: [PATCH 346/868] Revert "Fix SOCI image convertion regression for 0.12.0 release" This reverts commit accc2f38793f5f1bfb53c9f5b3b96e2a047ccdc4. Signed-off-by: Swapnanil-Gupta --- pkg/snapshotterutil/sociutil.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/snapshotterutil/sociutil.go b/pkg/snapshotterutil/sociutil.go index 4f55c917eb1..82e8773ce66 100644 --- a/pkg/snapshotterutil/sociutil.go +++ b/pkg/snapshotterutil/sociutil.go @@ -117,13 +117,6 @@ func ConvertSociIndexV2(ctx context.Context, client *client.Client, srcRef strin sociCmd.Args = append(sociCmd.Args, "convert") - // The following option temporarily fix the image conversion regression in SOCI v0.12.0 - // https://github.com/awslabs/soci-snapshotter/issues/1789 - // TODO: remove after the bug is fixed in SOCI - if err := CheckSociVersion("0.12.0"); err == nil { - sociCmd.Args = append(sociCmd.Args, "--force") - } - if sOpts.AllPlatforms { sociCmd.Args = append(sociCmd.Args, "--all-platforms") } else if len(sOpts.Platforms) > 0 { From bc45754ae55f47ec95bd87532f8aebc4a54755d9 Mon Sep 17 00:00:00 2001 From: Yash Kukrecha Date: Mon, 24 Nov 2025 15:47:04 -0600 Subject: [PATCH 347/868] (feat): Default net.ipv4.ip_unprivileged_port_start to 0 inside containers Signed-off-by: Yash Kukrecha --- .../container/container_inspect_linux_test.go | 6 +++- .../container_run_runtime_linux_test.go | 28 +++++++++++++++++ pkg/cmd/container/create.go | 30 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 21098c322f2..1c82925bf46 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -362,8 +362,12 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) { assert.Equal(t, "", inspect.HostConfig.UTSMode) assert.Equal(t, hc.ShmSize, inspect.HostConfig.ShmSize) assert.Equal(t, hc.Runtime, inspect.HostConfig.Runtime) - assert.Equal(t, 0, len(inspect.HostConfig.Sysctls)) assert.Equal(t, 0, len(inspect.HostConfig.Devices)) + // Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment. + got := len(inspect.HostConfig.Sysctls) + if got != 0 && got != 1 { + t.Fatalf("unexpected number of Sysctls entries: %d (want 0 or 1)", got) + } } func TestContainerInspectHostConfigDNS(t *testing.T) { diff --git a/cmd/nerdctl/container/container_run_runtime_linux_test.go b/cmd/nerdctl/container/container_run_runtime_linux_test.go index ea7473f2d20..2d42734ec8a 100644 --- a/cmd/nerdctl/container/container_run_runtime_linux_test.go +++ b/cmd/nerdctl/container/container_run_runtime_linux_test.go @@ -27,3 +27,31 @@ func TestRunSysctl(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--sysctl", "net.ipv4.ip_forward=1", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_forward").AssertOutExactly("1\n") } + +func TestRunSysctl_DefaultUnprivilegedPortStart(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // No --sysctl flags, default network mode (non-host). + // We expect net.ipv4.ip_unprivileged_port_start=0 inside the container, + // because withDefaultUnprivilegedPortSysctl should apply the default. + base.Cmd( + "run", "--rm", + testutil.AlpineImage, + "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", + ).AssertOutExactly("0\n") +} + +func TestRunSysctl_UnprivilegedPortStartOverride(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // User explicitly sets net.ipv4.ip_unprivileged_port_start=1000. + // We must NOT override this; the container should see "1000". + base.Cmd( + "run", "--rm", + "--sysctl", "net.ipv4.ip_unprivileged_port_start=1000", + testutil.AlpineImage, + "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", + ).AssertOutExactly("1000\n") +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 69e6919ccd3..b877643bdd7 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -27,6 +27,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strconv" "strings" @@ -330,6 +331,10 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } opts = append(opts, umaskOpts...) + if !isHostNetwork(netLabelOpts) { + opts = append(opts, withDefaultUnprivilegedPortSysctl()) + } + rtCOpts, err := generateRuntimeCOpts(options.GOptions.CgroupManager, options.Runtime) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err @@ -563,6 +568,31 @@ func GenerateLogURI(dataStore string) (*url.URL, error) { return cio.LogURIGenerator("binary", selfExe, args) } +func isHostNetwork(netOpts types.NetworkOptions) bool { + return slices.Contains(netOpts.NetworkSlice, "host") +} + +// withDefaultUnprivilegedPortSysctl ensures that containers can bind to +// privileged ports (<1024) without requiring CAP_NET_BIND_SERVICE inside +// the container by defaulting net.ipv4.ip_unprivileged_port_start to 0 +// in the container's network namespace. +func withDefaultUnprivilegedPortSysctl() oci.SpecOpts { + const key = "net.ipv4.ip_unprivileged_port_start" + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + if s.Linux == nil { + s.Linux = &specs.Linux{} + } + if s.Linux.Sysctl == nil { + s.Linux.Sysctl = make(map[string]string) + } + + if _, exists := s.Linux.Sysctl[key]; !exists { + s.Linux.Sysctl[key] = "0" + } + return nil + } +} + func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) { if rootlessutil.IsRootless() { detachedNetNS, err := rootlessutil.DetachedNetNS() From 5db35e0dd9489e10362dc2dcec2fc868788a3682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Thu, 18 Dec 2025 14:38:11 +0100 Subject: [PATCH 348/868] nerdctl image prune -f means --force, not --filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix ambiguous flag in command reference for `nerdctl image prune`: `-f` was given as the short flag for both `--filter` and `--force`, but checking the source this should be `--force` only. Signed-off-by: Kristoffer Grönlund --- docs/command-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 6786c757f0c..3f9f415051d 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -978,7 +978,7 @@ Usage: `nerdctl image prune [OPTIONS]` Flags: - :whale: `-a, --all`: Remove all unused images, not just dangling ones -- :whale: `-f, --filter`: Filter the images. +- :whale: `--filter`: Filter the images. - :whale: `--filter=until=`: Images created before given date formatted timestamps or Go duration strings. Currently does not support Unix timestamps. - :whale: `--filter=label=`: Matches images based on the presence of a label alone or a label and a value - :whale: `-f, --force`: Do not prompt for confirmation From aa98f6cbef13549fdd831088e88171d2983bed04 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 16:09:27 +0900 Subject: [PATCH 349/868] Fix `'C:\\Program Files\\Linux Containers\\kernel' not found` Fix issue 4664 Signed-off-by: Akihiro Suda --- pkg/cmd/container/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index b877643bdd7..064ad89395e 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -580,7 +580,8 @@ func withDefaultUnprivilegedPortSysctl() oci.SpecOpts { const key = "net.ipv4.ip_unprivileged_port_start" return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { if s.Linux == nil { - s.Linux = &specs.Linux{} + // NOP, as the target platform is not Linux + return nil } if s.Linux.Sysctl == nil { s.Linux.Sysctl = make(map[string]string) From a7d9fb0f2bbf8e0389f4d96b6d801b0f4636cacc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:45:23 +0000 Subject: [PATCH 350/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.2.0...v2.2.1) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.2.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_update.go | 4 ++-- go.mod | 14 ++++++------ go.sum | 28 +++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/nerdctl/container/container_update.go b/cmd/nerdctl/container/container_update.go index 28ac0f6d078..51240b87d24 100644 --- a/cmd/nerdctl/container/container_update.go +++ b/cmd/nerdctl/container/container_update.go @@ -333,8 +333,8 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string, if spec.Linux.Resources.Pids == nil { spec.Linux.Resources.Pids = &runtimespec.LinuxPids{} } - if spec.Linux.Resources.Pids.Limit != opts.PidsLimit { - spec.Linux.Resources.Pids.Limit = opts.PidsLimit + if spec.Linux.Resources.Pids.Limit == nil || (spec.Linux.Resources.Pids.Limit != nil && *spec.Linux.Resources.Pids.Limit != opts.PidsLimit) { + spec.Linux.Resources.Pids.Limit = &opts.PidsLimit } } } diff --git a/go.mod b/go.mod index fe12a205101..1f491f14946 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/Microsoft/hcsshim v0.14.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.3.0 - github.com/containerd/cgroups/v3 v3.1.0 //gomodjail:unconfined + github.com/containerd/cgroups/v3 v3.1.2 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.10.0 - github.com/containerd/containerd/v2 v2.2.0 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.2.1 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -52,7 +52,7 @@ require ( github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 - github.com/opencontainers/runtime-spec v1.2.1 + github.com/opencontainers/runtime-spec v1.3.0 github.com/pelletier/go-toml/v2 v2.2.4 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined @@ -70,7 +70,7 @@ require ( golang.org/x/term v0.38.0 //gomodjail:unconfined golang.org/x/text v0.32.0 gotest.tools/v3 v3.5.2 - tags.cncf.io/container-device-interface v1.0.1 //gomodjail:unconfined + tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) require ( @@ -112,8 +112,8 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect - github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect - github.com/opencontainers/selinux v1.13.0 // indirect + github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect + github.com/opencontainers/selinux v1.13.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -144,7 +144,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect - tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect + tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 1fded43dc23..2b7d5c57e91 100644 --- a/go.sum +++ b/go.sum @@ -25,14 +25,14 @@ github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAir github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= -github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY= -github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= +github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= -github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8= -github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE= +github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk= +github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -250,12 +250,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= -github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= -github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= -github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84= -github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +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.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= +github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= @@ -519,7 +519,7 @@ pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= -tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= -tags.cncf.io/container-device-interface/specs-go v1.0.0 h1:8gLw29hH1ZQP9K1YtAzpvkHCjjyIxHZYzBAvlQ+0vD8= -tags.cncf.io/container-device-interface/specs-go v1.0.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= +tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= +tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= +tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= +tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= From 300c75fd659154cb578cb72eeb91f03ae515ccb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:00:07 +0000 Subject: [PATCH 351/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.9 to 0.15.10. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.9...v0.15.10) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fe12a205101..c3bff453bbe 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.9 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.10 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 1fded43dc23..f40a9b4966d 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.9 h1:yFPr75WyO49sWJiBFMKNZo6kK4ed2hc13YxGn5KHWCU= -github.com/containerd/nydus-snapshotter v0.15.9/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= +github.com/containerd/nydus-snapshotter v0.15.10 h1:hphjuKOqSHLGznNJiAvmsOWkdu4qFXjf4DzGrWSuIsM= +github.com/containerd/nydus-snapshotter v0.15.10/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From f5c1822cbe23b9ee06426177c8ac06b319dcc81a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:00:28 +0000 Subject: [PATCH 352/868] build(deps): bump github.com/rootless-containers/rootlesskit/v2 Bumps [github.com/rootless-containers/rootlesskit/v2](https://github.com/rootless-containers/rootlesskit) from 2.3.5 to 2.3.6. - [Release notes](https://github.com/rootless-containers/rootlesskit/releases) - [Commits](https://github.com/rootless-containers/rootlesskit/compare/v2.3.5...v2.3.6) --- updated-dependencies: - dependency-name: github.com/rootless-containers/rootlesskit/v2 dependency-version: 2.3.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fe12a205101..811a64744be 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/runtime-spec v1.2.1 github.com/pelletier/go-toml/v2 v2.2.4 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v2 v2.3.6 //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 diff --git a/go.sum b/go.sum index 1fded43dc23..c9eab78db2d 100644 --- a/go.sum +++ b/go.sum @@ -274,8 +274,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk= -github.com/rootless-containers/rootlesskit/v2 v2.3.5 h1:WGY05oHE7xQpSkCGfYP9lMY5z19tCxA8PhWlvP1cKx8= -github.com/rootless-containers/rootlesskit/v2 v2.3.5/go.mod h1:83EIYLeMX8UeNgLHkR1PefoSV76aKEC+OyI3vzrEfvw= +github.com/rootless-containers/rootlesskit/v2 v2.3.6 h1:m/26nAx0DbHZYaM46+uoQjfpu9G77QLzWj2jz25chO8= +github.com/rootless-containers/rootlesskit/v2 v2.3.6/go.mod h1:pv+RESmjRmeUIOsEWOT1f8560CrdaQrDW0YsF4K5kAY= 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= From d46dec33a95037c7b01a064b1bef32b76ab4e332 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:07:42 +0900 Subject: [PATCH 353/868] update containerd (2.2.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 10 +++++----- Dockerfile | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 189b92b4749..7410f722c01 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -48,7 +48,7 @@ jobs: - runner: ubuntu-24.04-arm # Additionally build for old containerd on amd - runner: ubuntu-24.04 - containerd-version: v1.7.28 + containerd-version: v1.7.30 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -77,7 +77,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.28 + containerd-version: v1.7.30 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-24.04 @@ -94,7 +94,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.28 + containerd-version: v1.7.30 # ipv6 - runner: ubuntu-24.04 target: rootful @@ -146,9 +146,9 @@ jobs: go-version: 1.25 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.2.0 + containerd-version: 2.2.1 # Note: these as for amd64 - containerd-sha: 8a7956e91a33bca10b13b196df00e73acebb7f3931e0d1456c0209139f96251b + containerd-sha: f5d8e90ecb6c1c7e33ecddf8cc268a93b9e5b54e0e850320d765511d76624f41 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.8.0 linux-cni-sha: ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 diff --git a/Dockerfile b/Dockerfile index d7d5a4d3c42..e29976cd9cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.2.0@1c4457e00facac03ce1d75f7b6777a7a851e5c41 +ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 ARG RUNC_VERSION=v1.3.3@d842d7719497cc3b774fd71620278ac9e17710e0 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY From 14b8fa4917d1d00be81f666f6e698989e3245d7f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:08:11 +0900 Subject: [PATCH 354/868] update runc (1.4.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e29976cd9cc..5a6e1a1ba6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 -ARG RUNC_VERSION=v1.3.3@d842d7719497cc3b774fd71620278ac9e17710e0 +ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY # Extra deps: Build From 3f62767b3f0ee1c4a7e9fc79057cadfeb6b81e16 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:08:57 +0900 Subject: [PATCH 355/868] update CNI plugins (1.9.0) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 | 2 -- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 | 2 ++ hack/provisioning/kube/kind.sh | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 7410f722c01..9ed3d6b4e9b 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -150,5 +150,5 @@ jobs: # Note: these as for amd64 containerd-sha: f5d8e90ecb6c1c7e33ecddf8cc268a93b9e5b54e0e850320d765511d76624f41 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 - linux-cni-version: v1.8.0 - linux-cni-sha: ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 + linux-cni-version: v1.9.0 + linux-cni-sha: 58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 diff --git a/Dockerfile b/Dockerfile index 5a6e1a1ba6d..2f5b1849876 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 -ARG CNI_PLUGINS_VERSION=v1.8.0@BINARY +ARG CNI_PLUGINS_VERSION=v1.9.0@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.25.2@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 deleted file mode 100644 index 40b7ebdd7f2..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.8.0 +++ /dev/null @@ -1,2 +0,0 @@ -ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 cni-plugins-linux-amd64-v1.8.0.tgz -57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb cni-plugins-linux-arm64-v1.8.0.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 new file mode 100644 index 00000000000..b23c10549fd --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 @@ -0,0 +1,2 @@ +58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 cni-plugins-linux-amd64-v1.9.0.tgz +2596ef56329dd1269026f46b8df262f09ba43c92dbfb940e1e69fbccccd30a29 cni-plugins-linux-arm64-v1.9.0.tgz diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index 43b74e4eb8b..71040a2d0e2 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -22,11 +22,11 @@ readonly root GO_VERSION=1.25 KIND_VERSION=v0.30.0 -CNI_PLUGINS_VERSION=v1.8.0 +CNI_PLUGINS_VERSION=v1.9.0 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_AMD64=ab3bda535f9d90766cccc90d3dddb5482003dd744d7f22bcf98186bf8eea8be6 +CNI_PLUGINS_SHA_AMD64=58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_ARM64=57ce466fc3b79db1f19b8f4c63e07a1112306efa53c94fe810a2150dd9e07ddb +CNI_PLUGINS_SHA_ARM64=2596ef56329dd1269026f46b8df262f09ba43c92dbfb940e1e69fbccccd30a29 [ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64 From 32f9ae12b7036460f869fd568b38cef1adc5fbe5 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:10:04 +0900 Subject: [PATCH 356/868] update BuildKit (0.26.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 diff --git a/Dockerfile b/Dockerfile index 2f5b1849876..a34be826511 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.9.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.25.2@BINARY +ARG BUILDKIT_VERSION=v0.26.3@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 deleted file mode 100644 index fcea42d1add..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.25.2 +++ /dev/null @@ -1,2 +0,0 @@ -d18b8188b600e201a1b3d08c20e2a1e439cf51d2c2285f132dd9bd51c666f6d6 buildkit-v0.25.2.linux-amd64.tar.gz -f6fd69c40bec1788d650430ede40545a47bd765922ad23456a463e88b47039cf buildkit-v0.25.2.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 new file mode 100644 index 00000000000..79bad2db0fe --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 @@ -0,0 +1,2 @@ +249ae16ba4be59fadb51a49ff4d632bbf37200e2b6e187fa8574f0f1bce8166b buildkit-v0.26.3.linux-amd64.tar.gz +a98829f1b1b9ec596eb424dd03f03b9c7b596edac83e6700adf83ba0cb0d5f80 buildkit-v0.26.3.linux-arm64.tar.gz From f7e5bc5ad7462b07faffdc23bed0775edef4f59f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:10:42 +0900 Subject: [PATCH 357/868] update imgcrypt (2.0.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a34be826511..c77e7c22e5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ ARG BUILDKIT_VERSION=v0.26.3@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption -ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c +ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless ARG ROOTLESSKIT_VERSION=v2.3.5@BINARY ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY From 5fb3545134b352689e45254b5b68b650b89d5069 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 20:12:08 +0900 Subject: [PATCH 358/868] update RootlessKit (2.3.6) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/SHA256SUMS | 6 ++++++ Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 | 6 ++++++ 4 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.d/SHA256SUMS.d/SHA256SUMS delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 diff --git a/Dockerfile b/Dockerfile index c77e7c22e5f..7399714b62a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v2.3.5@BINARY +ARG ROOTLESSKIT_VERSION=v2.3.6@BINARY ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 diff --git a/Dockerfile.d/SHA256SUMS.d/SHA256SUMS b/Dockerfile.d/SHA256SUMS.d/SHA256SUMS new file mode 100644 index 00000000000..f9bb64f0557 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/SHA256SUMS @@ -0,0 +1,6 @@ +3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz +6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz +730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz +05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz +199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz +afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 deleted file mode 100644 index 96d484fe5c7..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 +++ /dev/null @@ -1,6 +0,0 @@ -478c14c3195bf989cd9a8e6bd129d227d5d88f1c11418967ffdc84a0072cc7a2 rootlesskit-aarch64.tar.gz -0622e52952a848219b86b902c9bdb96e1ebe575a3015c05e7da02569e83b3a61 rootlesskit-armv7l.tar.gz -b1ec12321c54860230c5d0bbbc6d651a746ac49bce7eeb36fd1ad1e0f0048d58 rootlesskit-ppc64le.tar.gz -8ee59e518cdb5770afab49307b400f585598ed2c06b4ffc81f7c36fbeea422d6 rootlesskit-riscv64.tar.gz -2a3198947cf322357106557c58a8d5f29a664961edf290ea305c94b03521f6c8 rootlesskit-s390x.tar.gz -118208e25becd144ee7317c172fc9decce7b16174d5c1bbf80f1d1d0eacc6b5f rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 new file mode 100644 index 00000000000..f9bb64f0557 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 @@ -0,0 +1,6 @@ +3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz +6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz +730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz +05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz +199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz +afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz From 8622837485327f2f8026c3087a92c5ffa00c8ff1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 22:57:47 +0900 Subject: [PATCH 359/868] update soci-snapshotter (0.12.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7399714b62a..0df82797953 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.9 -ARG SOCI_SNAPSHOTTER_VERSION=0.11.1 +ARG SOCI_SNAPSHOTTER_VERSION=0.12.1 ARG KUBO_VERSION=v0.38.2 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 017436cd158c822576ad8eed3099b90f1779d0e7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 22:58:03 +0900 Subject: [PATCH 360/868] update Kubo (0.39.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0df82797953..5ab9d77f08e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.12.1 -ARG KUBO_VERSION=v0.38.2 +ARG KUBO_VERSION=v0.39.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 59b65692ed5ce19436aad4078401382075c3eb27 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Dec 2025 22:59:02 +0900 Subject: [PATCH 361/868] update kind (0.31.0) Signed-off-by: Akihiro Suda --- hack/provisioning/kube/kind.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index 71040a2d0e2..3fd1aed52e3 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -21,7 +21,7 @@ readonly root . "$root/../../scripts/lib.sh" GO_VERSION=1.25 -KIND_VERSION=v0.30.0 +KIND_VERSION=v0.31.0 CNI_PLUGINS_VERSION=v1.9.0 # shellcheck disable=SC2034 CNI_PLUGINS_SHA_AMD64=58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 From 4a8e9d0ee70b747af3b535676de9f6329215870a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:01:34 +0000 Subject: [PATCH 362/868] build(deps): bump docker/setup-buildx-action from 3.11.1 to 3.12.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/e468171a9de216ec08956ac3ada2f0791b6bd435...8d2750c68a42422c14e847fe6c8ac0403b4cbd6f) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 3.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index e9512c5b062..c66a77c7a24 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -38,7 +38,7 @@ jobs: uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action From 7ba0ae28746797aa6b59f632d102d52249c74136 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:01:38 +0000 Subject: [PATCH 363/868] build(deps): bump actions/attest-build-provenance from 3.0.0 to 3.1.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/977bb373ede98d70efdf65b84cb5f73e068dcc2a...00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d4a7efdbca..879ae162430 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From 29cb1ce2f4b2c5af8b7445734dac152d1c1812c9 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 21 Dec 2025 01:00:58 +0900 Subject: [PATCH 364/868] fix: use private namespace for image build in private namespace In the current implementation, tests that require nerdtest.Build do not run against the specified namespace when a private namespace is configured. For example, with the following requirements, the test runs in a private namespace, but the internal logic of nerdtest.Build does not take the private namespace into account, causing buildctl or buildkit to be unavailable: ``` testCase.Require = require.All( nerdtest.Private, nerdtest.Build, ) ``` This commit fixes the behavior so that when a private namespace is specified, tests are executed considering that namespace instead of the default namespace. Signed-off-by: Hayato Kiwata --- pkg/testutil/nerdtest/requirements.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index d4af8490339..11c8e399113 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -337,7 +337,12 @@ var Build = &test.Requirement{ mess := "buildkitd is enabled" if isTargetNerdish() { - bkHostAddr, err := buildkitutil.GetBuildkitHost(defaultNamespace) + namespace := defaultNamespace + if ns := helpers.Read(Namespace); ns != "" { + namespace = string(ns) + } + + bkHostAddr, err := buildkitutil.GetBuildkitHost(namespace) if err != nil { ret = false mess = fmt.Sprintf("buildkitd is not enabled: %+v", err) From 38507f9be736a5fdb938d11a0b8df4c1e8731667 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 22 Dec 2025 02:16:13 +0900 Subject: [PATCH 365/868] chore: improve the visibility of orphaned containers in logs When executing the command on a compose file like the one below, orphaned containers are displayed, but the current implementation does not show the IDs of these orphaned containers, making them difficult to identify. ``` $ cat compose-full.yaml services: test: image: docker.io/library/busybox:latest command: ["sleep", "infinity"] orphan: image: docker.io/library/busybox:latest command: ["sleep", "infinity"] $ cat compose-orphan.yaml services: test: image: docker.io/library/busybox:latest command: ["sleep", "infinity"] $ sudo nerdctl compose -f compose-full.yaml up -d ... $ sudo nerdctl compose -f compose-orphan.yaml down -v ... WARN[0010] found 1 orphaned containers: [0x4000340000], you can run this command with the --remove-orphans flag to clean it up ... ``` Therefore, this commit modifies to display the IDs of orphaned containers. Additionally, since there was other logic that performed similar displays, this commit also modifies it in the same manner. Signed-off-by: Hayato Kiwata --- pkg/composer/down.go | 2 +- pkg/composer/orphans.go | 8 ++++++++ pkg/composer/run.go | 2 +- pkg/composer/up.go | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/composer/down.go b/pkg/composer/down.go index 6996f1bda33..d7d7c2f0f6b 100644 --- a/pkg/composer/down.go +++ b/pkg/composer/down.go @@ -65,7 +65,7 @@ func (c *Composer) Down(ctx context.Context, downOptions DownOptions) error { return fmt.Errorf("error removeing orphaned containers: %w", err) } } else { - log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans) + log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), containerShortIDs(orphans)) } } diff --git a/pkg/composer/orphans.go b/pkg/composer/orphans.go index cd45386fa0d..307f31c545b 100644 --- a/pkg/composer/orphans.go +++ b/pkg/composer/orphans.go @@ -55,3 +55,11 @@ func (c *Composer) getOrphanContainers(ctx context.Context, parsedServices []*se return orphanContainers, nil } + +func containerShortIDs(containers []containerd.Container) []string { + names := make([]string, 0, len(containers)) + for _, c := range containers { + names = append(names, c.ID()[:12]) + } + return names +} diff --git a/pkg/composer/run.go b/pkg/composer/run.go index 9928fbd8d5d..84807cd6dc6 100644 --- a/pkg/composer/run.go +++ b/pkg/composer/run.go @@ -201,7 +201,7 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error { return fmt.Errorf("error removing orphaned containers: %w", err) } } else { - log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans) + log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), containerShortIDs(orphans)) } } diff --git a/pkg/composer/up.go b/pkg/composer/up.go index 3a03a08a8ca..ec9155331bb 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -116,7 +116,7 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro return fmt.Errorf("error removing orphaned containers: %w", err) } } else { - log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans) + log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), containerShortIDs(orphans)) } } From 4a95d73245ff19835b09a3696281281833439061 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 21 Dec 2025 01:06:42 +0900 Subject: [PATCH 366/868] test: refactor compose_create_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- .../compose/compose_create_linux_test.go | 191 +++++++++++++----- 1 file changed, 143 insertions(+), 48 deletions(-) diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index a62d9b14104..581681e00e8 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -18,12 +18,15 @@ package compose import ( "fmt" + "path/filepath" + "regexp" "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/mod/tigron/tig" @@ -147,82 +150,174 @@ services: } func TestComposeCreatePull(t *testing.T) { + testCase := nerdtest.Setup() - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + testCase.NoParallel = true + testCase.Require = nerdtest.Private + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // `compose create --pull never` should fail: no such image - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "never").AssertFail() - // `compose create --pull missing(default)|always` should succeed: image is pulled and container is created - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "always").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created") + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("composeYAML", composePath) + } + + testCase.SubTests = []*test.Case{ + { + Description: "compose create --pull never fails when image missing", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "never") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "compose create --pull missing (default) pulls and creates a container", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), + }, + { + Description: "compose create --pull always pulls and creates a container", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "always") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } + + testCase.Run(t) } func TestComposeCreateBuild(t *testing.T) { - const imageSvc0 = "composebuild_svc0" + testCase := nerdtest.Setup() - dockerComposeYAML := fmt.Sprintf(` + testCase.NoParallel = true + testCase.Require = require.All( + nerdtest.Private, + nerdtest.Build, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + imageSvc0 := data.Identifier("composebuild_svc0") + composeYAML := fmt.Sprintf(` services: svc0: build: . image: %s `, imageSvc0) + dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) - dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(dockerfile, "Dockerfile") - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - comp.WriteFile("Dockerfile", dockerfile) - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("imageName", imageSvc0) + } - defer base.Cmd("rmi", imageSvc0).Run() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "compose create --no-build fails when image needs to be built", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--no-build") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "compose create --build builds image and creates container", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "create", "--build") + helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "images", "svc0").Run( + &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("imageName"))) + }, + }, + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile(`Created|created`))), + }, + } - // `compose create --no-build` should fail if service image needs build - base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--no-build").AssertFail() - // `compose create --build` should succeed: image is built and container is created - base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--build").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "svc0").AssertOutContains(imageSvc0) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } + + testCase.Run(t) } func TestComposeCreateWritesConfigHashLabel(t *testing.T) { - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: svc0: image: %s `, testutil.CommonImage) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - base := testutil.NewBase(t) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("containerName", serviceparser.DefaultContainerName(projectName, "svc0", "1")) - base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + helpers.Ensure("compose", "-f", composePath, "create") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", data.Labels().Get("containerName")) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains("com.docker.compose.config-hash")) - container := serviceparser.DefaultContainerName(projectName, "svc0", "1") - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", container). - AssertOutContains("com.docker.compose.config-hash") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if path := data.Labels().Get("composeYAML"); path != "" { + helpers.Anyhow("compose", "-f", path, "down", "-v") + } + } + + testCase.Run(t) } From 46b1f5a2129fb22c647be3ae8c6648e3eb44486c Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 22 Dec 2025 22:29:54 +0900 Subject: [PATCH 367/868] test: refactor compose_down_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- .../compose/compose_down_linux_test.go | 128 +++++++++++++----- 1 file changed, 93 insertions(+), 35 deletions(-) diff --git a/cmd/nerdctl/compose/compose_down_linux_test.go b/cmd/nerdctl/compose/compose_down_linux_test.go index 4a69c2ee9c4..2eea0c01827 100644 --- a/cmd/nerdctl/compose/compose_down_linux_test.go +++ b/cmd/nerdctl/compose/compose_down_linux_test.go @@ -19,78 +19,136 @@ package compose import ( "fmt" "testing" - "time" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeDownRemoveUsedNetwork(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var ( - dockerComposeYAMLOrphan = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAMLOrphan := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) - dockerComposeYAMLFull = fmt.Sprintf(` + dockerComposeYAMLFull := fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) - ) - compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) - defer compOrphan.CleanUp() - compFull := testutil.NewComposeDir(t, dockerComposeYAMLFull) - defer compFull.CleanUp() + composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") + composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") + + projectName := data.Identifier("project") + t.Logf("projectName=%q", projectName) + + testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") + orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") + + data.Labels().Set("composeOrphan", composeOrphanPath) + data.Labels().Set("composeFull", composeFullPath) + data.Labels().Set("projectName", projectName) + + helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, testContainer) + nerdtest.EnsureContainerStarted(helpers, orphanContainer) + } - projectName := fmt.Sprintf("nerdctl-compose-test-%d", time.Now().Unix()) - t.Logf("projectName=%q", projectName) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphan"), "down", "-v") + } - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "--remove-orphans").AssertOK() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: []error{ + fmt.Errorf("in use"), + }, + } + } - base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "-v").AssertCombinedOutContains("in use") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if composeFull := data.Labels().Get("composeFull"); composeFull != "" { + helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", composeFull, "down", "--remove-orphans") + } + } + testCase.Run(t) } func TestComposeDownRemoveOrphans(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - var ( - dockerComposeYAMLOrphan = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAMLOrphan := fmt.Sprintf(` services: test: image: %s command: "sleep infinity" `, testutil.CommonImage) - dockerComposeYAMLFull = fmt.Sprintf(` + dockerComposeYAMLFull := fmt.Sprintf(` %s orphan: image: %s command: "sleep infinity" `, dockerComposeYAMLOrphan, testutil.CommonImage) - ) - - compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) - defer compOrphan.CleanUp() - compFull := testutil.NewComposeDir(t, dockerComposeYAMLFull) - defer compFull.CleanUp() - - projectName := compFull.ProjectName() - t.Logf("projectName=%q", projectName) - - orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") - - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run() - base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "--remove-orphans").AssertOK() - base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps", "-a").AssertOutNotContains(orphanContainer) + composeOrphanPath := data.Temp().Save(dockerComposeYAMLOrphan, "compose-orphan.yaml") + composeFullPath := data.Temp().Save(dockerComposeYAMLFull, "compose-full.yaml") + + projectName := data.Identifier("project") + t.Logf("projectName=%q", projectName) + + testContainer := serviceparser.DefaultContainerName(projectName, "test", "1") + orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") + + data.Labels().Set("composeOrphan", composeOrphanPath) + data.Labels().Set("composeFull", composeFullPath) + data.Labels().Set("projectName", projectName) + data.Labels().Set("orphanContainer", orphanContainer) + + helpers.Ensure("compose", "-p", projectName, "-f", composeFullPath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, testContainer) + nerdtest.EnsureContainerStarted(helpers, orphanContainer) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeOrphan"), "down", "--remove-orphans") + } + + testCase.Expected = test.Expects(0, nil, nil) + + testCase.SubTests = []*test.Case{ + { + Description: "orphan container removed", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeFull"), "ps", "-a") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain(data.Labels().Get("orphanContainer")), + } + }, + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if composeFull := data.Labels().Get("composeFull"); composeFull != "" { + helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", composeFull, "down", "-v") + } + } + + testCase.Run(t) } From 9dea13fde922b60501fdff9313e19733d16cee57 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 24 Dec 2025 00:26:49 +0900 Subject: [PATCH 368/868] MAINTAINERS: promote Chengyu Zhu (ChengyuZhu6) from a REVIEWER to a COMMITTER Signed-off-by: Akihiro Suda --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index 0999f7c2b80..07009b5a5aa 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -17,12 +17,12 @@ "Zheaoli", "Zheao Li", "me@manjusaka.me","6E0D D9FA BAD5 AF61 D884 01EE 878F 445D 9C6C E65E" "djdongjin", "Jin Dong", "djdongjin95@gmail.com","" "yankay", "Kay Yan", "kay.yan@daocloud.io", "" +"ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","" # REVIEWERS # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" -"ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","" # EMERITUS # See EMERITUS.md From c1cbf7776e7eed3edc211d87548529ba8bdaf8fe Mon Sep 17 00:00:00 2001 From: Youfu Zhang Date: Fri, 12 Dec 2025 19:12:59 +0800 Subject: [PATCH 369/868] fix: allow localhost DNS servers when using host network This commit addresses the issue where nerdctl was unconditionally stripping localhost DNS servers from /etc/resolv.conf when container is using host network. Fixes: #4651 Signed-off-by: Youfu Zhang --- .../container_run_network_linux_test.go | 50 ++++++++++ .../container_network_manager.go | 8 +- pkg/resolvconf/resolvconf.go | 18 +++- pkg/resolvconf/resolvconf_linux_test.go | 97 +++++++++++++++++++ 4 files changed, 168 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 6d7b353cdea..13d94a2cab0 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -957,6 +957,56 @@ func TestHostNetworkHostName(t *testing.T) { testCase.Run(t) } +func TestHostNetworkDnsPreserved(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + // In some rootless CI job, slirp provides 10.0.2.3 as DNS server. + // We cannot simply parse host /etc/resolv.conf here. + helpers.Command("run", "--rm", + "-v", "/etc/resolv.conf:/mnt/resolv.conf:ro", + testutil.AlpineImage, + "grep", "-E", "^nameserver\\s+", "/mnt/resolv.conf").Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + data.Labels().Set("nameservers", stdout) + }, + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--network", "host", + testutil.AlpineImage, + "grep", "-E", "^nameserver\\s+", "/etc/resolv.conf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // container with --network=host should have same nameserver as host + nameservers := data.Labels().Get("nameservers") + return &test.Expected{ + Output: expect.Equals(nameservers), + } + }, + } + testCase.Run(t) +} + +func TestDefaultNetworkDnsNoLocalhost(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.Not(require.Windows), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + testutil.AlpineImage, "grep", "-E", "^nameserver\\s+(127\\.|::1)", "/etc/resolv.conf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, // no match + } + }, + } + testCase.Run(t) +} + func TestNoneNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index d41e3c7e17a..3638b450917 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -89,7 +89,7 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C } } -func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, []string, error) { +func fetchDNSResolverConfig(netOpts types.NetworkOptions, allowLocalhostDNS bool) ([]string, []string, []string, error) { dns := netOpts.DNSServers dnsSearch := netOpts.DNSSearchDomains dnsOptions := netOpts.DNSResolvConfOptions @@ -103,7 +103,7 @@ func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, [ conf = &resolvconf.File{} log.L.WithError(err).Debugf("resolvConf file doesn't exist on host") } - conf, err = resolvconf.FilterResolvDNS(conf.Content, true) + conf, err = resolvconf.FilterResolvDNSWithLocalhostOption(conf.Content, true, allowLocalhostDNS) if err != nil { return nil, nil, nil, err } @@ -291,7 +291,7 @@ func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, containe } resolvConfPath := filepath.Join(stateDir, "resolv.conf") - dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts) + dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, false) if err != nil { return nil, nil, err } @@ -671,7 +671,7 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe } resolvConfPath := filepath.Join(stateDir, "resolv.conf") - dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts) + dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, true) if err != nil { return nil, nil, err } diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index 16a809d390d..30aa53fe21c 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -184,7 +184,23 @@ func GetLastModified() *File { // 2. Given the caller provides the enable/disable state of IPv6, the filter // code will remove all IPv6 nameservers if it is not enabled for containers func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { - cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{}) + return FilterResolvDNSWithLocalhostOption(resolvConf, ipv6Enabled, false) +} + +// FilterResolvDNSWithLocalhostOption is like FilterResolvDNS but allows controlling +// whether localhost nameservers are preserved. This is useful for host network mode +// where the container should inherit the host's DNS configuration including localhost resolvers. +// +// Parameters: +// - resolvConf: the resolv.conf file content +// - ipv6Enabled: whether IPv6 nameservers should be preserved +// - allowLocalhostDNS: if true, localhost nameservers are preserved; if false, they are filtered out +func FilterResolvDNSWithLocalhostOption(resolvConf []byte, ipv6Enabled bool, allowLocalhostDNS bool) (*File, error) { + cleanedResolvConf := resolvConf + // if allowLocalhostDNS is false, remove localhost nameservers + if !allowLocalhostDNS { + cleanedResolvConf = localhostNSRegexp.ReplaceAll(cleanedResolvConf, []byte{}) + } // if IPv6 is not enabled, also clean out any IPv6 address nameserver if !ipv6Enabled { cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) diff --git a/pkg/resolvconf/resolvconf_linux_test.go b/pkg/resolvconf/resolvconf_linux_test.go index 2b7790712ac..2c204e0a8a1 100644 --- a/pkg/resolvconf/resolvconf_linux_test.go +++ b/pkg/resolvconf/resolvconf_linux_test.go @@ -320,3 +320,100 @@ func TestFilterResolvDns(t *testing.T) { } } } + +func TestFilterResolvDnsWithLocalhostOption(t *testing.T) { + testCases := []struct { + name string + input string + allowLocalhostDNS bool + ipv6Enabled bool + expected string + }{ + { + name: "filter_disallow_localhost_ipv6_disabled", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "nameserver 192.88.99.1\n", + }, + { + name: "filter_allow_localhost_ipv6_disabled", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: true, + ipv6Enabled: false, + expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\n", + }, + { + name: "filter_disallow_localhost_ipv6_enabled", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "nameserver 192.88.99.1\nnameserver 2001:db8::1\n", + }, + { + name: "filter_allow_localhost_ipv6_enabled", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: true, + ipv6Enabled: true, + expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + }, + { + name: "fallback_none_ipv6_disabled", + input: "", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_none_ipv6_enabled", + input: "", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + { + name: "fallback_localhost4_ipv6_disabled", + input: "nameserver 127.0.0.53", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_localhost4_ipv6_enabled", + input: "nameserver 127.0.0.53", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + { + name: "fallback_localhost6_ipv6_disabled", + input: "nameserver ::1", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_localhost6_ipv6_enabled", + input: "nameserver ::1", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + result, err := FilterResolvDNSWithLocalhostOption([]byte(tc.input), tc.ipv6Enabled, tc.allowLocalhostDNS) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("result is nil") + } + if tc.expected != string(result.Content) { + t.Fatalf("expected \n<%s> got \n<%s>", tc.expected, string(result.Content)) + } + }) + } +} From 04ea03e3db7e58f464c895238bf58f267608e7d1 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 17 Dec 2025 22:46:49 +0800 Subject: [PATCH 370/868] support nerdctl search command support nerdctl search command Signed-off-by: ChengyuZhu6 --- cmd/nerdctl/main.go | 2 + cmd/nerdctl/search/search.go | 86 ++++++++ cmd/nerdctl/search/search_linux_test.go | 241 +++++++++++++++++++++ cmd/nerdctl/search/search_test.go | 27 +++ docs/command-reference.md | 18 +- pkg/api/types/search_types.go | 36 ++++ pkg/cmd/search/search.go | 271 ++++++++++++++++++++++++ 7 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 cmd/nerdctl/search/search.go create mode 100644 cmd/nerdctl/search/search_linux_test.go create mode 100644 cmd/nerdctl/search/search_test.go create mode 100644 pkg/api/types/search_types.go create mode 100644 pkg/cmd/search/search.go diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 375fc2d45b7..f8ab56799bd 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -44,6 +44,7 @@ import ( "github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest" "github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace" "github.com/containerd/nerdctl/v2/cmd/nerdctl/network" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/search" "github.com/containerd/nerdctl/v2/cmd/nerdctl/system" "github.com/containerd/nerdctl/v2/cmd/nerdctl/volume" "github.com/containerd/nerdctl/v2/pkg/config" @@ -309,6 +310,7 @@ Config file ($NERDCTL_TOML): %s image.TagCommand(), image.RmiCommand(), image.HistoryCommand(), + search.Command(), // #endregion // #region System diff --git a/cmd/nerdctl/search/search.go b/cmd/nerdctl/search/search.go new file mode 100644 index 00000000000..eb1b652d35d --- /dev/null +++ b/cmd/nerdctl/search/search.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 search + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/search" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "search [OPTIONS] TERM", + Short: "Search registry for images", + Args: cobra.ExactArgs(1), + RunE: runSearch, + DisableFlagsInUseLine: true, + } + + flags := cmd.Flags() + + flags.Bool("no-trunc", false, "Don't truncate output") + flags.StringSliceP("filter", "f", nil, "Filter output based on conditions provided") + flags.Int("limit", 0, "Max number of search results") + flags.String("format", "", "Pretty-print search using a Go template") + + return cmd +} + +func processSearchFlags(cmd *cobra.Command) (types.SearchOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.SearchOptions{}, err + } + + noTrunc, err := cmd.Flags().GetBool("no-trunc") + if err != nil { + return types.SearchOptions{}, err + } + limit, err := cmd.Flags().GetInt("limit") + if err != nil { + return types.SearchOptions{}, err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return types.SearchOptions{}, err + } + filter, err := cmd.Flags().GetStringSlice("filter") + if err != nil { + return types.SearchOptions{}, err + } + + return types.SearchOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + NoTrunc: noTrunc, + Limit: limit, + Filters: filter, + Format: format, + }, nil +} + +func runSearch(cmd *cobra.Command, args []string) error { + options, err := processSearchFlags(cmd) + if err != nil { + return err + } + + return search.Search(cmd.Context(), args[0], options) +} diff --git a/cmd/nerdctl/search/search_linux_test.go b/cmd/nerdctl/search/search_linux_test.go new file mode 100644 index 00000000000..76c318b7f38 --- /dev/null +++ b/cmd/nerdctl/search/search_linux_test.go @@ -0,0 +1,241 @@ +/* + 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 search + +import ( + "errors" + "regexp" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// All tests in this file are based on the output of `nerdctl search alpine`. +// +// Expected output format (default behavior with --limit 10): +// +// NAME DESCRIPTION STARS OFFICIAL +// alpine A minimal Docker image based on Alpine Linux… 11437 [OK] +// alpine/git A simple git container running in alpine li… 249 +// alpine/socat Run socat command in alpine container 115 +// alpine/helm Auto-trigger docker build for kubernetes hel… 69 +// alpine/curl 11 +// alpine/k8s Kubernetes toolbox for EKS (kubectl, helm, i… 64 +// alpine/bombardier Auto-trigger docker build for bombardier whe… 28 +// alpine/httpie Auto-trigger docker build for `httpie` when … 21 +// alpine/terragrunt Auto-trigger docker build for terragrunt whe… 18 +// alpine/openssl openssl 7 + +func TestSearch(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "basic-search", + Command: test.Command("search", "alpine", "--limit", "5"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("NAME"), + expect.Contains("DESCRIPTION"), + expect.Contains("STARS"), + expect.Contains("OFFICIAL"), + expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)), + expect.Contains("alpine"), + expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux`)), + expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), + expect.Contains("[OK]"), + expect.Match(regexp.MustCompile(`alpine/\w+`)), + ), + } + }, + }, + { + Description: "search-library-image", + Command: test.Command("search", "library/alpine", "--limit", "5"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("NAME"), + expect.Contains("DESCRIPTION"), + expect.Contains("STARS"), + expect.Contains("OFFICIAL"), + expect.Contains("alpine"), + expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), + ), + } + }, + }, + { + Description: "search-with-no-trunc", + Command: test.Command("search", "alpine", "--limit", "3", "--no-trunc"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("NAME"), + expect.Contains("DESCRIPTION"), + expect.Contains("alpine"), + // With --no-trunc, the full description should be visible (not truncated with …) + expect.Match(regexp.MustCompile(`alpine\s+A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!`)), + ), + } + }, + }, + { + Description: "search-with-format", + Command: test.Command("search", "alpine", "--limit", "2", "--format", "{{.Name}}: {{.StarCount}}"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Match(regexp.MustCompile(`alpine:\s*\d+`)), + expect.DoesNotContain("NAME"), + expect.DoesNotContain("DESCRIPTION"), + expect.DoesNotContain("OFFICIAL"), + ), + } + }, + }, + { + Description: "search-output-format", + Command: test.Command("search", "alpine", "--limit", "5"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Match(regexp.MustCompile(`NAME\s+DESCRIPTION\s+STARS\s+OFFICIAL`)), + expect.Match(regexp.MustCompile(`(?m)^alpine\s+.*\s+\d+\s+\[OK\]\s*$`)), + expect.Match(regexp.MustCompile(`(?m)^alpine/\w+\s+.*\s+\d+\s*$`)), + expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s*$`)), + ), + } + }, + }, + { + Description: "search-description-formatting", + Command: test.Command("search", "alpine", "--limit", "10"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Match(regexp.MustCompile(`Alpine Linux…`)), + expect.DoesNotMatch(regexp.MustCompile(`(?m)^\s+\d+\s+`)), + expect.Match(regexp.MustCompile(`(?m)^[a-z0-9/_-]+\s+.*\s+\d+`)), + ), + } + }, + }, + } + + testCase.Run(t) +} + +func TestSearchWithFilter(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "filter-is-official-true", + Command: test.Command("search", "alpine", "--filter", "is-official=true", "--limit", "5"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("NAME"), + expect.Contains("OFFICIAL"), + expect.Contains("alpine"), + expect.Contains("[OK]"), + expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d+\s+\[OK\]`)), + ), + } + }, + }, + { + Description: "filter-stars", + Command: test.Command("search", "alpine", "--filter", "stars=10000"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("NAME"), + expect.Contains("STARS"), + expect.Contains("alpine"), + // The official alpine image has > 10000 stars + expect.Match(regexp.MustCompile(`alpine\s+.*\s+\d{4,}\s+\[OK\]`)), + ), + } + }, + }, + } + + testCase.Run(t) +} + +func TestSearchFilterErrors(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "invalid-filter-format", + Command: test.Command("search", "alpine", "--filter", "foo"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("bad format of filter (expected name=value)")}, + } + }, + }, + { + Description: "invalid-filter-key", + Command: test.Command("search", "alpine", "--filter", "foo=bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("invalid filter 'foo'")}, + } + }, + }, + { + Description: "invalid-stars-value", + Command: test.Command("search", "alpine", "--filter", "stars=abc"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("invalid filter 'stars=abc'")}, + } + }, + }, + { + Description: "invalid-is-official-value", + Command: test.Command("search", "alpine", "--filter", "is-official=abc"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("invalid filter 'is-official=abc'")}, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/search/search_test.go b/cmd/nerdctl/search/search_test.go new file mode 100644 index 00000000000..a76005fb94f --- /dev/null +++ b/cmd/nerdctl/search/search_test.go @@ -0,0 +1,27 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package search + +import ( + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +func TestMain(m *testing.M) { + testutil.M(m) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 3f9f415051d..e0d7b41f895 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -66,6 +66,7 @@ - [Registry](#registry) - [:whale: nerdctl login](#whale-nerdctl-login) - [:whale: nerdctl logout](#whale-nerdctl-logout) + - [:whale: nerdctl search](#whale-nerdctl-search) - [Network management](#network-management) - [:whale: nerdctl network create](#whale-nerdctl-network-create) - [:whale: nerdctl network ls](#whale-nerdctl-network-ls) @@ -1209,6 +1210,19 @@ Log out from a container registry Usage: `nerdctl logout [SERVER]` +### :whale: nerdctl search + +Search Docker Hub or a registry for images + +Usage: `nerdctl search [OPTIONS] TERM` + +Flags: + +- :whale: `--limit`: Max number of search results (default: 0) +- :whale: `--no-trunc`: Don't truncate output (default: false) +- :whale: `--filter, -f`: Filter output based on conditions provided +- :whale: `--format`: Format the output using the given Go template + ## Network management ### :whale: nerdctl network create @@ -1978,10 +1992,6 @@ Network management: - `docker network connect` - `docker network disconnect` -Registry: - -- `docker search` - Compose: - `docker-compose events|scale` diff --git a/pkg/api/types/search_types.go b/pkg/api/types/search_types.go new file mode 100644 index 00000000000..645335a72c3 --- /dev/null +++ b/pkg/api/types/search_types.go @@ -0,0 +1,36 @@ +/* + 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 types + +import ( + "io" +) + +type SearchOptions struct { + Stdout io.Writer + // GOptions is the global options + GOptions GlobalCommandOptions + + // NoTrunc don't truncate output + NoTrunc bool + // Limit the number of results + Limit int + // Filter output based on conditions provided, for the --filter argument + Filters []string + // Format the output using the given Go template, e.g, '{{json .}}' + Format string +} diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go new file mode 100644 index 00000000000..e0db9206ddc --- /dev/null +++ b/pkg/cmd/search/search.go @@ -0,0 +1,271 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package search + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "text/tabwriter" + + dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +type SearchResult struct { + Description string `json:"description"` + IsOfficial bool `json:"is_official"` + Name string `json:"name"` + StarCount int `json:"star_count"` +} + +func Search(ctx context.Context, term string, options types.SearchOptions) error { + // Validate filters before making HTTP request + filterMap, err := validateAndParseFilters(options.Filters) + if err != nil { + return err + } + + registryHost, searchTerm := splitReposSearchTerm(term) + + parsedRef, err := referenceutil.Parse(registryHost) + if err != nil { + log.G(ctx).WithError(err).Debugf("failed to parse registry host %q, using as-is", registryHost) + } else { + registryHost = parsedRef.Domain + } + + var dOpts []dockerconfigresolver.Opt + + if options.GOptions.InsecureRegistry { + log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", registryHost) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) + + hostOpts, err := dockerconfigresolver.NewHostOptions(ctx, registryHost, dOpts...) + if err != nil { + return fmt.Errorf("failed to create host options: %w", err) + } + + username, password, err := hostOpts.Credentials(registryHost) + if err != nil { + log.G(ctx).WithError(err).Debug("no credentials found, searching anonymously") + } + + scheme := "https" + if hostOpts.DefaultScheme != "" { + scheme = hostOpts.DefaultScheme + } + + searchURL := buildSearchURL(registryHost, searchTerm, scheme) + + req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil) + if err != nil { + return err + } + + if username != "" && password != "" { + req.SetBasicAuth(username, password) + } + + client := createHTTPClient(hostOpts) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("search failed with status %d: %s", resp.StatusCode, string(body)) + } + + var searchResp struct { + Results []SearchResult `json:"results"` + } + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return fmt.Errorf("failed to decode search response: %w", err) + } + + filteredResults := applyFilters(searchResp.Results, filterMap, options.Limit) + + return printSearchResults(options.Stdout, filteredResults, options) +} + +func splitReposSearchTerm(reposName string) (registryHost string, searchTerm string) { + nameParts := strings.SplitN(reposName, "/", 2) + if len(nameParts) == 1 || + (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && + nameParts[0] != "localhost") { + // No registry specified, use docker.io + // For "library/alpine", the search term should be "alpine" + // For "alpine", the search term should be "alpine" + if len(nameParts) == 2 && nameParts[0] == "library" { + return "docker.io", nameParts[1] + } + return "docker.io", reposName + } + return nameParts[0], nameParts[1] +} + +func buildSearchURL(registryHost, term, scheme string) string { + host := registryHost + if host == "docker.io" { + host = "index.docker.io" + } + + u := url.URL{ + Scheme: scheme, + Host: host, + Path: "/v1/search", + } + q := u.Query() + q.Set("q", term) + u.RawQuery = q.Encode() + + return u.String() +} + +func createHTTPClient(hostOpts *dockerconfig.HostOptions) *http.Client { + if hostOpts != nil && hostOpts.DefaultTLS != nil { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: hostOpts.DefaultTLS, + }, + } + } + return http.DefaultClient +} + +func validateFilterValue(key, value string) error { + switch key { + case "stars": + if _, err := strconv.Atoi(value); err != nil { + return fmt.Errorf("invalid filter 'stars=%s'", value) + } + case "is-official": + if _, err := strconv.ParseBool(value); err != nil { + return fmt.Errorf("invalid filter 'is-official=%s'", value) + } + default: + return fmt.Errorf("invalid filter '%s'", key) + } + return nil +} + +// validateAndParseFilters validates and parses filters before making HTTP request +func validateAndParseFilters(filters []string) (map[string]string, error) { + filterMap := make(map[string]string) + for _, f := range filters { + parts := strings.SplitN(f, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("bad format of filter (expected name=value)") + } + key := parts[0] + value := parts[1] + if err := validateFilterValue(key, value); err != nil { + return nil, err + } + filterMap[key] = value + } + return filterMap, nil +} + +func applyFilters(results []SearchResult, filterMap map[string]string, limit int) []SearchResult { + filtered := make([]SearchResult, 0, len(results)) + + for _, r := range results { + if val, ok := filterMap["is-official"]; ok { + b, _ := strconv.ParseBool(val) + if b != r.IsOfficial { + continue + } + } + + if val, ok := filterMap["stars"]; ok { + stars, _ := strconv.Atoi(val) + if r.StarCount < stars { + continue + } + } + + filtered = append(filtered, r) + } + + // Apply limit after filtering, but maintain original order from API + if limit > 0 && len(filtered) > limit { + filtered = filtered[:limit] + } + + return filtered +} + +func truncateDescription(desc string, noTrunc bool) string { + if !noTrunc && len(desc) > 45 { + return formatter.Ellipsis(desc, 45) + } + return desc +} + +func printSearchResults(stdout io.Writer, results []SearchResult, options types.SearchOptions) error { + for i := range results { + results[i].Description = truncateDescription(results[i].Description, options.NoTrunc) + } + + if options.Format != "" { + tmpl, err := formatter.ParseTemplate(options.Format) + if err != nil { + return err + } + for _, r := range results { + if err := tmpl.Execute(stdout, r); err != nil { + return err + } + fmt.Fprintln(stdout) + } + return nil + } + + w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL") + + for _, r := range results { + desc := strings.ReplaceAll(r.Description, "\n", " ") + desc = strings.ReplaceAll(desc, "\t", " ") + + official := "" + if r.IsOfficial { + official = "[OK]" + } + fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", r.Name, desc, r.StarCount, official) + } + return w.Flush() +} From a858677c1aaecac11f78da7af77c7dc7452f40d6 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Fri, 26 Dec 2025 19:14:59 +0900 Subject: [PATCH 371/868] test: refactor compose_kill_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- .../compose/compose_kill_linux_test.go | 71 +++++++++++++++---- mod/tigron/expect/exit.go | 2 + pkg/testutil/nerdtest/utilities.go | 43 +++++++++++ 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/compose/compose_kill_linux_test.go b/cmd/nerdctl/compose/compose_kill_linux_test.go index 8c4687045b5..1d2813e7c1b 100644 --- a/cmd/nerdctl/compose/compose_kill_linux_test.go +++ b/cmd/nerdctl/compose/compose_kill_linux_test.go @@ -18,15 +18,23 @@ package compose import ( "fmt" + "path/filepath" + "regexp" "testing" - "time" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeKill(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` services: wordpress: @@ -54,17 +62,52 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + wordpressContainerName := serviceparser.DefaultContainerName(projectName, "wordpress", "1") + dbContainerName := serviceparser.DefaultContainerName(projectName, "db", "1") + + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("wordpressContainer", wordpressContainerName) + data.Labels().Set("dbContainer", dbContainerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, wordpressContainerName) + nerdtest.EnsureContainerStarted(helpers, dbContainerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "kill db container and exit with 137", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "kill", "db") + nerdtest.EnsureContainerExited(helpers, data.Labels().Get("dbContainer"), expect.ExitCodeSigkill) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "db", "-a") + }, + // Docker Compose v1: "Exit 137", v2: "exited (137)" + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile(` 137|\(137\)`))), + }, + { + Description: "wordpress container is still running", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "wordpress") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile("Up|running"))), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "db").AssertOK() - time.Sleep(3 * time.Second) - // Docker Compose v1: "Exit 137", v2: "exited (137)" - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny(" 137", "(137)") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") + testCase.Run(t) } diff --git a/mod/tigron/expect/exit.go b/mod/tigron/expect/exit.go index 4ebdf0df594..897bc16d464 100644 --- a/mod/tigron/expect/exit.go +++ b/mod/tigron/expect/exit.go @@ -19,6 +19,8 @@ package expect const ( // ExitCodeSuccess will ensure that the command effectively ran returned with exit code zero. ExitCodeSuccess = 0 + // ExitCodeSigkill verifies a container exited due to SIGKILL. + ExitCodeSigkill = 137 // ExitCodeGenericFail will verify that the command ran and exited with a non-zero error code. // This does NOT include timeouts, cancellation, or signals. ExitCodeGenericFail = -10 diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index b3f1d15ac2e..a012aed201f 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -151,6 +151,49 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { } } +func EnsureContainerExited(helpers test.Helpers, con string, exitCode int) { + helpers.T().Helper() + exited := false + for i := 0; i < maxRetry && !exited; i++ { + helpers.Command("container", "inspect", con). + Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + if err != nil || len(dc) == 0 || (len(dc) > 0 && dc[0].State == nil) { + return + } + assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") + state := dc[0].State + if state.Running { + return + } + if state.Status != "exited" && state.Status != "dead" { + return + } + // Use a negative exitCode to ignore the exit code and only verify exited/dead state. + if exitCode >= 0 && state.ExitCode != exitCode { + return + } + exited = true + }, + }) + time.Sleep(sleep) + } + + if !exited { + ins := helpers.Capture("container", "inspect", con) + lgs := helpers.Capture("logs", con) + ps := helpers.Capture("ps", "-a") + helpers.T().Log(ins) + helpers.T().Log(lgs) + helpers.T().Log(ps) + helpers.T().Log(fmt.Sprintf("container %s still not exited after %d retries", con, maxRetry)) + helpers.T().FailNow() + } +} + func GenerateJWEKeyPair(data test.Data, helpers test.Helpers) (string, string) { helpers.T().Helper() From 6d07fd73cff753be2a2a33cc487726a388c1dd6d Mon Sep 17 00:00:00 2001 From: Nuwed Munshi Date: Wed, 24 Dec 2025 17:29:00 +0000 Subject: [PATCH 372/868] Refactor container_start_test.go to use Tigron Updates tests to use nerdtest.Setup and the Tigron testing framework as per issue #4613. Signed-off-by: Nuwed Munshi --- cmd/nerdctl/container/container_start_test.go | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/cmd/nerdctl/container/container_start_test.go b/cmd/nerdctl/container/container_start_test.go index 60369433d24..d3898bf9011 100644 --- a/cmd/nerdctl/container/container_start_test.go +++ b/cmd/nerdctl/container/container_start_test.go @@ -17,31 +17,58 @@ package container import ( - "runtime" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestStart(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + nerdtest.Setup() - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "--name", containerName, testutil.CommonImage).AssertOK() - base.Cmd("start", containerName).AssertOutContains(containerName) + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", + "--name", data.Identifier(), + testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return test.Expects(0, nil, expect.Contains(data.Identifier()))(data, helpers) + }, + } + testCase.Run(t) } func TestStartAttach(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("start attach test is not yet implemented on Windows") - } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo").AssertOK() - base.Cmd("start", "-a", containerName).AssertOutContains("foo") + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", + "--name", data.Identifier(), + testutil.CommonImage, "sh", "-euxc", "echo foo") + }, + 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("start", "-a", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return test.Expects(0, nil, expect.Contains("foo"))(data, helpers) + }, + } + testCase.Run(t) } From ec739b5fabd34b37444e9b1794d4a0860d524a4f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 7 Jan 2026 22:25:00 +0900 Subject: [PATCH 373/868] CI: lint: increase timeout `lint / go / linux (go canary)` was often timing out Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index b133b7fe6cf..26f9a321f59 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -34,7 +34,7 @@ jobs: goos: linux canary: true with: - timeout: 5 + timeout: 10 go-version: "1.25" runner: ubuntu-24.04 # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job From 829b1acb6dd2fbc250a23a12bb4694ece7d3a13a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 24 Dec 2025 00:45:11 +0900 Subject: [PATCH 374/868] MAINTAINERS: add Hayato Kiwata (haytok) as a REVIEWER Signed-off-by: Akihiro Suda --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index 07009b5a5aa..be8add49510 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23,6 +23,7 @@ # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" +"haytok","Hayato Kiwata","haytok@amazon.co.jp","B485 C5AA 6220 0A06 78FD 294D FA4F 2421 1D65 269F" # EMERITUS # See EMERITUS.md From 1f9ab6d3c4205d95b542a2a4c4331a2fe2a2f820 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:35:05 +0000 Subject: [PATCH 375/868] build(deps): bump golang.org/x/sys in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/sys](https://github.com/golang/sys). Updates `golang.org/x/sys` from 0.39.0 to 0.40.0 - [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c1e4617b23c..1913bafb3ee 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( golang.org/x/crypto v0.46.0 golang.org/x/net v0.48.0 golang.org/x/sync v0.19.0 //gomodjail:unconfined - golang.org/x/sys v0.39.0 //gomodjail:unconfined + golang.org/x/sys v0.40.0 //gomodjail:unconfined golang.org/x/term v0.38.0 //gomodjail:unconfined golang.org/x/text v0.32.0 gotest.tools/v3 v3.5.2 diff --git a/go.sum b/go.sum index f41abd7b0bc..bdf41bf5077 100644 --- a/go.sum +++ b/go.sum @@ -432,8 +432,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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= From 5ced25a540a8dadd13ecb29d93da49a53d9350f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:35:45 +0000 Subject: [PATCH 376/868] build(deps): bump the golang-x group with 2 updates Bumps the golang-x group with 2 updates: [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/term` from 0.38.0 to 0.39.0 - [Commits](https://github.com/golang/term/compare/v0.38.0...v0.39.0) Updates `golang.org/x/text` from 0.32.0 to 0.33.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.32.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 1913bafb3ee..c2a8a6e969a 100644 --- a/go.mod +++ b/go.mod @@ -67,8 +67,8 @@ require ( golang.org/x/net v0.48.0 golang.org/x/sync v0.19.0 //gomodjail:unconfined golang.org/x/sys v0.40.0 //gomodjail:unconfined - golang.org/x/term v0.38.0 //gomodjail:unconfined - golang.org/x/text v0.32.0 + golang.org/x/term v0.39.0 //gomodjail:unconfined + golang.org/x/text v0.33.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) @@ -135,7 +135,7 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined google.golang.org/grpc v1.76.0 // indirect diff --git a/go.sum b/go.sum index bdf41bf5077..4aece166a28 100644 --- a/go.sum +++ b/go.sum @@ -376,8 +376,8 @@ 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -443,8 +443,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.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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,8 +454,8 @@ 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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 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= @@ -468,8 +468,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= From 2f4e137464da9a4aaf3fd99fd2e49375341182c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:35:50 +0000 Subject: [PATCH 377/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.3+incompatible to 29.1.4+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.3...v29.1.4) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.1.4+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1913bafb3ee..9065ae1b130 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.3+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.4+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index bdf41bf5077..128d8417ad6 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c= -github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M= +github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 66de1b01c247dd9b963fb611ef2f99b2c3b312c1 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 10 Jan 2026 12:44:08 +0900 Subject: [PATCH 378/868] fix: return error for invalid --pull option in nerdctl compose create In the current implementation, specifying an invalid string for the --pull option of the nerdctl compose create command causes it to fall back to missing, allowing the image to be pulled as shown below. ``` $ sudo nerdctl compose create --pull foo WARN[0000] Ignoring: service svc0: pull_policy: "foo" INFO[0000] Ensuring image alpine ... INFO[0004] Creating container fix-compose-pull-policy-with-invalid-option-svc0-1 ``` On the other hand, docker compose returns the following error in a similar situation. ``` $ docker compose create --pull foo invalid --pull option "foo" ``` This commit fixes it to be compatible with docker compose. Signed-off-by: Hayato Kiwata --- .../compose/compose_create_linux_test.go | 31 +++++++++++++++++++ pkg/composer/serviceparser/serviceparser.go | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 581681e00e8..26557dd7ca0 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -17,6 +17,7 @@ package compose import ( + "errors" "fmt" "path/filepath" "regexp" @@ -217,6 +218,36 @@ services: testCase.Run(t) } +func TestComposeCreatePullInvalidOption(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` +services: + svc0: + image: %s +`, testutil.CommonImage) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + data.Labels().Set("composeYAML", composePath) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // nerver isn't never. + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "create", "--pull", "nerver") + } + + testCase.Expected = test.Expects(1, []error{errors.New(`invalid --pull option \"nerver\"`)}, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if path := data.Labels().Get("composeYAML"); path != "" { + helpers.Anyhow("compose", "-f", path, "down", "-v") + } + } + + testCase.Run(t) +} + func TestComposeCreateBuild(t *testing.T) { testCase := nerdtest.Setup() diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 93e5c9953ff..afd665fca6f 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -458,7 +458,7 @@ func Parse(project *types.Project, svc types.ServiceConfig) (*Service, error) { parsed.Build.Force = true parsed.PullMode = "never" default: - log.L.Warnf("Ignoring: service %s: pull_policy: %q", svc.Name, svc.PullPolicy) + return nil, fmt.Errorf("invalid --pull option %q", svc.PullPolicy) } for i := 0; i < replicas; i++ { From f4381734d79670ed2a876c0237e3a736ea10d276 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Tue, 13 Jan 2026 00:30:32 +0900 Subject: [PATCH 379/868] docs/command-reference.md: fix anchors without blue_square Signed-off-by: Hayato Kiwata --- docs/command-reference.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index e0d7b41f895..3ad8cc421e2 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -13,12 +13,12 @@ - [Container management](#container-management) - - [:whale: nerdctl run](#whale-blue_square-nerdctl-run) - - [:whale: nerdctl exec](#whale-blue_square-nerdctl-exec) - - [:whale: nerdctl create](#whale-blue_square-nerdctl-create) + - [:whale: nerdctl run](#whale-nerdctl-run) + - [:whale: nerdctl exec](#whale-nerdctl-exec) + - [:whale: nerdctl create](#whale-nerdctl-create) - [:whale: nerdctl cp](#whale-nerdctl-cp) - - [:whale: nerdctl ps](#whale-blue_square-nerdctl-ps) - - [:whale: nerdctl inspect](#whale-blue_square-nerdctl-inspect) + - [:whale: nerdctl ps](#whale-nerdctl-ps) + - [:whale: nerdctl inspect](#whale-nerdctl-inspect) - [:whale: nerdctl logs](#whale-nerdctl-logs) - [:whale: nerdctl port](#whale-nerdctl-port) - [:whale: nerdctl rm](#whale-nerdctl-rm) @@ -39,8 +39,8 @@ - [:whale: nerdctl build](#whale-nerdctl-build) - [:whale: nerdctl commit](#whale-nerdctl-commit) - [Image management](#image-management) - - [:whale: nerdctl images](#whale-blue_square-nerdctl-images) - - [:whale: nerdctl pull](#whale-blue_square-nerdctl-pull) + - [:whale: nerdctl images](#whale-nerdctl-images) + - [:whale: nerdctl pull](#whale-nerdctl-pull) - [:whale: nerdctl push](#whale-nerdctl-push) - [:whale: nerdctl load](#whale-nerdctl-load) - [:whale: nerdctl save](#whale-nerdctl-save) From 1b8087de323e399f5a2b4531525277ee2e29ca6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 02:54:11 +0000 Subject: [PATCH 380/868] build(deps): bump actions/setup-go from 6.1.0 to 6.2.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4dc6199c7b1a012772edbd06daecab0f50c9053c...7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 42078e59c95..824fc6f0514 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index e6f528e9bdf..7aeff3ad851 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 1af262636c3..a8aa3185fd5 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 8cba6f34c90..a5c0c001240 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 316916809ef..2963e0c28c0 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 879ae162430..585c6e7b5f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: "Install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 91511735664..4717a117df2 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From 8270c5e28d93389a34c879ca52e9c2833edbcba0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 02:54:26 +0000 Subject: [PATCH 381/868] build(deps): bump github.com/go-viper/mapstructure/v2 Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.4.0 to 2.5.0. - [Release notes](https://github.com/go-viper/mapstructure/releases) - [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-viper/mapstructure/compare/v2.4.0...v2.5.0) --- updated-dependencies: - dependency-name: github.com/go-viper/mapstructure/v2 dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c85aaed76cb..485c0dab2e2 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/fatih/color v1.18.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined - github.com/go-viper/mapstructure/v2 v2.4.0 + github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.0 github.com/klauspost/compress v1.18.2 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined diff --git a/go.sum b/go.sum index f46ead09765..7c1ccb09a06 100644 --- a/go.sum +++ b/go.sum @@ -125,8 +125,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= From a24342a6b4c16bd56bf0106926fce9a80f9b1090 Mon Sep 17 00:00:00 2001 From: Hajime Ogi Date: Tue, 13 Jan 2026 22:08:37 +0900 Subject: [PATCH 382/868] test: refactor container_run_runtime_linux_test.go to use Tigron Signed-off-by: Hajime Ogi --- .../container_run_runtime_linux_test.go | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/cmd/nerdctl/container/container_run_runtime_linux_test.go b/cmd/nerdctl/container/container_run_runtime_linux_test.go index 2d42734ec8a..9c1791b71d8 100644 --- a/cmd/nerdctl/container/container_run_runtime_linux_test.go +++ b/cmd/nerdctl/container/container_run_runtime_linux_test.go @@ -19,39 +19,41 @@ package container import ( "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunSysctl(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "--sysctl", "net.ipv4.ip_forward=1", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_forward").AssertOutExactly("1\n") + testCase := nerdtest.Setup() + + testCase.Command = test.Command("run", "--rm", "--sysctl", "net.ipv4.ip_forward=1", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_forward") + testCase.Expected = test.Expects(0, nil, expect.Equals("1\n")) + + testCase.Run(t) } func TestRunSysctl_DefaultUnprivilegedPortStart(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() // No --sysctl flags, default network mode (non-host). // We expect net.ipv4.ip_unprivileged_port_start=0 inside the container, // because withDefaultUnprivilegedPortSysctl should apply the default. - base.Cmd( - "run", "--rm", - testutil.AlpineImage, - "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", - ).AssertOutExactly("0\n") + testCase.Command = test.Command("run", "--rm", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start") + testCase.Expected = test.Expects(0, nil, expect.Equals("0\n")) + + testCase.Run(t) } func TestRunSysctl_UnprivilegedPortStartOverride(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() // User explicitly sets net.ipv4.ip_unprivileged_port_start=1000. // We must NOT override this; the container should see "1000". - base.Cmd( - "run", "--rm", - "--sysctl", "net.ipv4.ip_unprivileged_port_start=1000", - testutil.AlpineImage, - "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", - ).AssertOutExactly("1000\n") + testCase.Command = test.Command("run", "--rm", "--sysctl", "net.ipv4.ip_unprivileged_port_start=1000", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start") + testCase.Expected = test.Expects(0, nil, expect.Equals("1000\n")) + + testCase.Run(t) } From f219a22f0848f4480edc32b7b6155be516977397 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 14 Jan 2026 00:10:44 +0900 Subject: [PATCH 383/868] test: refactor compose_pause_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- .../compose/compose_pause_linux_test.go | 74 ++++++++++++++----- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/compose/compose_pause_linux_test.go b/cmd/nerdctl/compose/compose_pause_linux_test.go index 14624633f39..b15830920bf 100644 --- a/cmd/nerdctl/compose/compose_pause_linux_test.go +++ b/cmd/nerdctl/compose/compose_pause_linux_test.go @@ -18,19 +18,26 @@ package compose import ( "fmt" + "path/filepath" + "regexp" "testing" + "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/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposePauseAndUnpause(t *testing.T) { - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") - } + testCase := nerdtest.Setup() - var dockerComposeYAML = fmt.Sprintf(` + testCase.Require = nerdtest.CGroup + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s @@ -40,20 +47,49 @@ services: command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + svc0Container := serviceparser.DefaultContainerName(projectName, "svc0", "1") + svc1Container := serviceparser.DefaultContainerName(projectName, "svc1", "1") + + data.Labels().Set("composeYAML", composePath) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, svc0Container) + nerdtest.EnsureContainerStarted(helpers, svc1Container) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // pause a service should (only) pause its own container + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "pause", "svc0") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + svc0Paused := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0", "-a") + expect.Match(regexp.MustCompile("Paused|paused"))(svc0Paused, t) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + svc1Running := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc1") + expect.Match(regexp.MustCompile("Up|running"))(svc1Running, t) - // pause a service should (only) pause its own container - base.ComposeCmd("-f", comp.YAMLFullPath(), "pause", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Paused", "paused") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Up", "running") + // unpause should be able to recover the paused service container + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "unpause", "svc0") + svc0Running := helpers.Capture("compose", "-f", data.Labels().Get("composeYAML"), "ps", "svc0") + expect.Match(regexp.MustCompile("Up|running"))(svc0Running, t) + }, + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - // unpause should be able to recover the paused service container - base.ComposeCmd("-f", comp.YAMLFullPath(), "unpause", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Up", "running") + testCase.Run(t) } From c405edacc882708e8501fd6f26b7caf220d66584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:32:49 +0000 Subject: [PATCH 384/868] build(deps): bump github.com/containerd/accelerated-container-image Bumps [github.com/containerd/accelerated-container-image](https://github.com/containerd/accelerated-container-image) from 1.3.0 to 1.4.0. - [Release notes](https://github.com/containerd/accelerated-container-image/releases) - [Commits](https://github.com/containerd/accelerated-container-image/compare/v1.3.0...v1.4.0) --- updated-dependencies: - dependency-name: github.com/containerd/accelerated-container-image dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c85aaed76cb..11ab18e7d16 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.14.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.0 //gomodjail:unconfined - github.com/containerd/accelerated-container-image v1.3.0 + github.com/containerd/accelerated-container-image v1.4.0 github.com/containerd/cgroups/v3 v3.1.2 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.10.0 @@ -134,7 +134,7 @@ require ( go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined diff --git a/go.sum b/go.sum index f46ead09765..1007ebc7b2c 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAirA9btoTh/gB0Y= github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= -github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= -github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= +github.com/containerd/accelerated-container-image v1.4.0 h1:95Z3dO6Z5yVEHwIXsM583jeQwXEoSLLbNaYtdS/u8EI= +github.com/containerd/accelerated-container-image v1.4.0/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= @@ -364,8 +364,8 @@ golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/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= From b0c3b0a9387f630efe605f46781de4163817770a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:55:49 +0000 Subject: [PATCH 385/868] build(deps): bump the golang-x group with 2 updates Bumps the golang-x group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0) Updates `golang.org/x/net` from 0.48.0 to 0.49.0 - [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index fcb48242392..904732757aa 100644 --- a/go.mod +++ b/go.mod @@ -63,8 +63,8 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 + golang.org/x/crypto v0.47.0 + golang.org/x/net v0.49.0 golang.org/x/sync v0.19.0 //gomodjail:unconfined golang.org/x/sys v0.40.0 //gomodjail:unconfined golang.org/x/term v0.39.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 8a91757b6c7..876c5e45f8c 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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= @@ -394,8 +394,8 @@ 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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 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= From 16bac577e547544969c372aedbf3a49767f3e117 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:33:16 +0000 Subject: [PATCH 386/868] build(deps): bump github.com/containerd/accelerated-container-image Bumps [github.com/containerd/accelerated-container-image](https://github.com/containerd/accelerated-container-image) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/containerd/accelerated-container-image/releases) - [Commits](https://github.com/containerd/accelerated-container-image/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: github.com/containerd/accelerated-container-image dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 904732757aa..0f25118a027 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.14.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.0 //gomodjail:unconfined - github.com/containerd/accelerated-container-image v1.4.0 + github.com/containerd/accelerated-container-image v1.4.1 github.com/containerd/cgroups/v3 v3.1.2 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.10.0 diff --git a/go.sum b/go.sum index 876c5e45f8c..6c2cba2c8c7 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAirA9btoTh/gB0Y= github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= -github.com/containerd/accelerated-container-image v1.4.0 h1:95Z3dO6Z5yVEHwIXsM583jeQwXEoSLLbNaYtdS/u8EI= -github.com/containerd/accelerated-container-image v1.4.0/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= +github.com/containerd/accelerated-container-image v1.4.1 h1:jeZYAaq5pMCeyRZ0I916OjJsEb2TGjAQmfAZyQLi3ec= +github.com/containerd/accelerated-container-image v1.4.1/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= From 95ac12e51b29730036011fe6e28262c769137aa5 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Thu, 15 Jan 2026 20:05:17 +0900 Subject: [PATCH 387/868] test: refactor container_rename_windows_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_rename_windows_test.go | 134 +++++++++++++++--- 1 file changed, 114 insertions(+), 20 deletions(-) diff --git a/cmd/nerdctl/container/container_rename_windows_test.go b/cmd/nerdctl/container/container_rename_windows_test.go index 7532b22573f..fcd28e151e1 100644 --- a/cmd/nerdctl/container/container_rename_windows_test.go +++ b/cmd/nerdctl/container/container_rename_windows_test.go @@ -19,38 +19,132 @@ package container import ( "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRenameProcessContainer(t *testing.T) { - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainerName := testutil.Identifier(t) + data.Labels().Set("containerName", testContainerName) + + helpers.Ensure("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testContainerName) + } - defer base.Cmd("rm", "-f", testContainerName).Run() - base.Cmd("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", testContainerName) + helpers.Anyhow("rm", "-f", testContainerName+"_new") + } - defer base.Cmd("rm", "-f", testContainerName+"_new").Run() - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK() - base.Cmd("ps", "-a").AssertOutContains(testContainerName + "_new") - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertFail() - base.Cmd("rename", testContainerName+"_new", testContainerName+"_new").AssertFail() + testCase.SubTests = []*test.Case{ + { + Description: "`rename` should work", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "`rename` should have updated container name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) + }, + }, + { + Description: "`rename` should fail to rename not existing container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "`rename` should fail to rename to existing name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + } + + testCase.Run(t) } func TestRenameHyperVContainer(t *testing.T) { - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + testCase.Require = nerdtest.HyperV + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainerName := testutil.Identifier(t) + data.Labels().Set("containerName", testContainerName) - if !testutil.HyperVSupported() { - t.Skip("HyperV is not enabled, skipping test") + helpers.Ensure("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testContainerName) } - defer base.Cmd("rm", "-f", testContainerName).Run() - base.Cmd("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", testContainerName) + helpers.Anyhow("rm", "-f", testContainerName+"_new") + } + + testCase.SubTests = []*test.Case{ + { + Description: "`rename` should work", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "`rename` should have updated container name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) + }, + }, + { + Description: "`rename` should fail to rename not existing container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "`rename` should fail to rename to existing name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + } - defer base.Cmd("rm", "-f", testContainerName+"_new").Run() - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK() - base.Cmd("ps", "-a").AssertOutContains(testContainerName + "_new") - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertFail() - base.Cmd("rename", testContainerName+"_new", testContainerName+"_new").AssertFail() + testCase.Run(t) } From e3a1e1920937635260ade53f084021083351840d Mon Sep 17 00:00:00 2001 From: Hajime Ogi Date: Thu, 15 Jan 2026 22:00:36 +0900 Subject: [PATCH 388/868] test: refactor container_exec_linux_test.go to use Tigron Signed-off-by: Hajime Ogi --- .../container/container_exec_linux_test.go | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/cmd/nerdctl/container/container_exec_linux_test.go b/cmd/nerdctl/container/container_exec_linux_test.go index 9eafe7939bd..660c3bb3fb9 100644 --- a/cmd/nerdctl/container/container_exec_linux_test.go +++ b/cmd/nerdctl/container/container_exec_linux_test.go @@ -27,31 +27,66 @@ import ( ) func TestExecWithUser(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testContainer := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", testContainer).Run() - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - base.EnsureContainerStarted(testContainer) - - testCases := map[string]string{ - "": "uid=0(root) gid=0(root)", - "1000": "uid=1000 gid=0(root)", - "1000:users": "uid=1000 gid=100(users)", - "guest": "uid=405(guest) gid=100(users)", - "nobody": "uid=65534(nobody) gid=65534(nobody)", - "nobody:users": "uid=65534(nobody) gid=100(users)", + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rm", "-f", data.Identifier()) } - for userStr, expected := range testCases { - cmd := []string{"exec"} - if userStr != "" { - cmd = append(cmd, "--user", userStr) - } - cmd = append(cmd, testContainer, "id") - base.Cmd(cmd...).AssertOutContains(expected) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + data.Labels().Set("container_name", data.Identifier()) } + + testCase.SubTests = []*test.Case{ + { + Description: "with no user flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=0(root) gid=0(root)")), + }, + { + Description: "with --user 1000", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", "--user", "1000", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=1000 gid=0(root)")), + }, + { + Description: "with --user 1000:users", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", "--user", "1000:users", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=1000 gid=100(users)")), + }, + { + Description: "with --user guest", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", "--user", "guest", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=405(guest) gid=100(users)")), + }, + { + Description: "with --user nobody", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", "--user", "nobody", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=65534(nobody) gid=65534(nobody)")), + }, + { + Description: "with --user nobody:users", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", "--user", "nobody:users", data.Labels().Get("container_name"), "id") + }, + Expected: test.Expects(0, nil, expect.Contains("uid=65534(nobody) gid=100(users)")), + }, + } + + testCase.Run(t) } func TestExecTTY(t *testing.T) { From 3ffd43fd4bd4fe5c59208d03a17c27c135ab0097 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Fri, 16 Jan 2026 10:27:21 +0900 Subject: [PATCH 389/868] test: refactor container_rename_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_rename_linux_test.go | 161 ++++++++++++++---- 1 file changed, 131 insertions(+), 30 deletions(-) diff --git a/cmd/nerdctl/container/container_rename_linux_test.go b/cmd/nerdctl/container/container_rename_linux_test.go index cc8a6733d5f..6807dd0ab8f 100644 --- a/cmd/nerdctl/container/container_rename_linux_test.go +++ b/cmd/nerdctl/container/container_rename_linux_test.go @@ -19,42 +19,143 @@ package container import ( "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRename(t *testing.T) { - t.Parallel() - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) - - defer base.Cmd("rm", "-f", testContainerName).Run() - base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - - defer base.Cmd("rm", "-f", testContainerName+"_new").Run() - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK() - base.Cmd("ps", "-a").AssertOutContains(testContainerName + "_new") - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertFail() - base.Cmd("rename", testContainerName+"_new", testContainerName+"_new").AssertFail() + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Identifier() + data.Labels().Set("containerName", testContainerName) + helpers.Ensure("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testContainerName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", testContainerName) + helpers.Anyhow("rm", "-f", testContainerName+"_new") + } + + testCase.SubTests = []*test.Case{ + { + Description: "`rename` should work", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "`rename` should have updated container name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) + }, + }, + { + Description: "`rename` should fail to rename not existing container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "`rename` should fail to rename to existing name", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName+"_new", testContainerName+"_new") + }, + Expected: test.Expects(1, nil, nil), + }, + } + + testCase.Run(t) } func TestRenameUpdateHosts(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) - - defer base.Cmd("rm", "-f", testContainerName).Run() - base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - base.EnsureContainerStarted(testContainerName) - - defer base.Cmd("rm", "-f", testContainerName+"_1").Run() - base.Cmd("run", "-d", "--name", testContainerName+"_1", testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - base.EnsureContainerStarted(testContainerName + "_1") - - defer base.Cmd("rm", "-f", testContainerName+"_new").Run() - base.Cmd("exec", testContainerName, "cat", "/etc/hosts").AssertOutContains(testContainerName + "_1") - base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK() - base.Cmd("exec", testContainerName+"_new", "cat", "/etc/hosts").AssertOutContains(testContainerName + "_new") - base.Cmd("exec", testContainerName+"_1", "cat", "/etc/hosts").AssertOutContains(testContainerName + "_new") + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Identifier() + data.Labels().Set("containerName", testContainerName) + + helpers.Ensure("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("run", "-d", "--name", testContainerName+"_1", testutil.CommonImage, "sleep", nerdtest.Infinity) + + nerdtest.EnsureContainerStarted(helpers, testContainerName) + nerdtest.EnsureContainerStarted(helpers, testContainerName+"_1") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testContainerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", testContainerName) + helpers.Anyhow("rm", "-f", testContainerName+"_1") + helpers.Anyhow("rm", "-f", testContainerName+"_new") + } + + testCase.SubTests = []*test.Case{ + { + Description: "check '/etc/hosts' for sibling container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("exec", testContainerName, "cat", "/etc/hosts") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_1"))(data, helpers) + }, + }, + { + Description: "rename container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("rename", testContainerName, testContainerName+"_new") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "check '/etc/hosts' for renamed container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("exec", testContainerName+"_new", "cat", "/etc/hosts") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) + }, + }, + { + Description: "check sibling's '/etc/hosts' for renamed container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get("containerName") + return helpers.Command("exec", testContainerName+"_1", "cat", "/etc/hosts") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + testContainerName := data.Labels().Get("containerName") + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testContainerName+"_new"))(data, helpers) + }, + }, + } + + testCase.Run(t) } From 940bc86e04020bef384804348531f6e99f9f7045 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:32:28 +0000 Subject: [PATCH 390/868] build(deps): bump actions/cache from 5.0.1 to 5.0.2 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/9255dc7a253b0ccc959486e2bca901246202afeb...8b402f58fbc84540c8b491a91e594a4576fec3d7) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index ebfddd0dbab..b3c48abdae7 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 37ea275605d..10ae6c5d922 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: /root/.vagrant.d key: vagrant From 7029628bc22a2a3c737eed49646f76c7eb149d17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:32:39 +0000 Subject: [PATCH 391/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.4+incompatible to 29.1.5+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.4...v29.1.5) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.1.5+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0f25118a027..60289bfd7da 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.4+incompatible //gomodjail:unconfined + github.com/docker/cli v29.1.5+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 6c2cba2c8c7..2665cf05f60 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M= -github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao= +github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From b3a44d97227eb825c3962a8241ee48947ef2fcf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:32:54 +0000 Subject: [PATCH 392/868] build(deps): bump github.com/klauspost/compress from 1.18.2 to 1.18.3 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.2 to 1.18.3. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.2...v1.18.3) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0f25118a027..9b69637bf35 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.0 - github.com/klauspost/compress v1.18.2 + github.com/klauspost/compress v1.18.3 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 diff --git a/go.sum b/go.sum index 6c2cba2c8c7..d498f4528aa 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 43d35360950d2cad99bc80a7e7349ec14e7a2419 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Mon, 19 Jan 2026 23:28:01 +0900 Subject: [PATCH 393/868] test: refactor container_run_systemd_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_run_systemd_linux_test.go | 261 +++++++++++++----- 1 file changed, 196 insertions(+), 65 deletions(-) diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go index e1a459eaf61..178c5a7ed9c 100644 --- a/cmd/nerdctl/container/container_run_systemd_linux_test.go +++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go @@ -17,115 +17,246 @@ package container import ( - "runtime" + "errors" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunWithSystemdAlways(t *testing.T) { - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) - base.Cmd("run", "--name", containerName, "--systemd=always", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(rw,") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", testutil.Identifier(t)) + } - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } + testCase.SubTests = []*test.Case{ + { + Description: "should mount cgroup filesystem as rw", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("run", "--name", containerName, "--systemd=always", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(rw,")), + }, + { + Description: "should expose SIGTERM+3 stop signal label", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")), + }, + } + + testCase.Run(t) } func TestRunWithSystemdTrueEnabled(t *testing.T) { - if runtime.GOARCH != "amd64" { - t.Skip("This test is currently broken on arm with no emulation, as the Systemd image being used is amd64 only") + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Amd64, + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) + nerdtest.EnsureContainerStarted(helpers, containerName) } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } - base.Cmd("run", "-d", "--name", containerName, "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "should expose SIGTERM+3 stop signal labels", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")), + }, + { + Description: "waits for systemd to become ready and lists systemd jobs", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("exec", containerName, "sh", "-c", "--", `tries=0 - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3") + until systemctl is-system-running >/dev/null 2>&1; do - base.Cmd("exec", containerName, "sh", "-c", "--", `tries=0 -until systemctl is-system-running >/dev/null 2>&1; do - >&2 printf "Waiting for systemd to come up...\n" - sleep 1s - tries=$(( tries + 1)) - [ $tries -lt 10 ] || { - >&2 printf "systemd failed to come up in a reasonable amount of time\n" - exit 1 + >&2 printf "Waiting for systemd to come up...\n" + sleep 1s + tries=$(( tries + 1)) + [ $tries -lt 10 ] || { + >&2 printf "systemd failed to come up in a reasonable amount of time\n" + exit 1 + } + done + systemctl list-jobs`) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("jobs")), + }, } -done -systemctl list-jobs`).AssertOutContains("jobs") + + testCase.Run(t) } func TestRunWithSystemdTrueDisabled(t *testing.T) { - if runtime.GOARCH != "amd64" { - t.Skip("This test is currently broken on arm with no emulation, as the Systemd image being used is amd64 only") + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Amd64, + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", testutil.Identifier(t)) } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } - base.Cmd("run", "--name", containerName, "--systemd=true", "--entrypoint=/bin/bash", testutil.SystemdImage, "-c", "systemctl list-jobs || true").AssertCombinedOutContains("System has not been booted with systemd as init system") + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("run", "--name", containerName, "--systemd=true", "--entrypoint=/bin/bash", testutil.SystemdImage, "-c", "systemctl list-jobs") + } + testCase.Expected = test.Expects(1, []error{errors.New("System has not been booted with systemd as init system")}, nil) + + testCase.Run(t) } func TestRunWithSystemdFalse(t *testing.T) { - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) - base.Cmd("run", "--name", containerName, "--systemd=false", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", testutil.Identifier(t)) + } - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "should mount cgroup filesystem as ro", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("run", "--name", containerName, "--systemd=false", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(ro,")), + }, + { + Description: "should expose SIGTERM stop signal label", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGTERM")), + }, + } + + testCase.Run(t) } func TestRunWithNoSystemd(t *testing.T) { - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) - base.Cmd("run", "--name", containerName, "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", testutil.Identifier(t)) + } - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "should mount cgroup filesystem as ro", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("run", "--name", containerName, "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("(ro,")), + }, + { + Description: "should expose SIGTERM stop signal label", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGTERM")), + }, + } + + testCase.Run(t) } func TestRunWithSystemdPrivilegedError(t *testing.T) { - if runtime.GOARCH != "amd64" { - t.Skip("This test is currently broken on arm with no emulation, as the Systemd image being used is amd64 only") - } + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Amd64, + require.Not(nerdtest.Docker), + ) - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) + testCase.Command = test.Command("run", "--privileged", "--rm", "--systemd=always", "--entrypoint=/sbin/init", testutil.SystemdImage) + testCase.Expected = test.Expects(1, []error{errors.New("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")}, nil) - base.Cmd("run", "--privileged", "--rm", "--systemd=always", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertCombinedOutContains("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used") + testCase.Run(t) } func TestRunWithSystemdPrivilegedSuccess(t *testing.T) { - if runtime.GOARCH != "amd64" { - t.Skip("This test is currently broken on arm with no emulation, as the Systemd image being used is amd64 only") + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Amd64, + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) + nerdtest.EnsureContainerStarted(helpers, containerName) } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("container", "rm", "-f", containerName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) + } - base.Cmd("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK() + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")) - base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3") + testCase.Run(t) } From a10ede70879ce32b90e1f6118774cb49aed44188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:17:27 +0000 Subject: [PATCH 394/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.10.0 to 2.10.1. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.10.0...v2.10.1) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.10.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8ac38f668e9..5e2b920b064 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.14.0-rc.1 - github.com/compose-spec/compose-go/v2 v2.10.0 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.10.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.1 github.com/containerd/cgroups/v3 v3.1.2 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 42754ecc702..cfcadab33de 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAirA9btoTh/gB0Y= -github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= +github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU= +github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= github.com/containerd/accelerated-container-image v1.4.1 h1:jeZYAaq5pMCeyRZ0I916OjJsEb2TGjAQmfAZyQLi3ec= github.com/containerd/accelerated-container-image v1.4.1/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= From 3fa39ae71a74fda5223152233ef1ab7db4c9a246 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Tue, 20 Jan 2026 23:49:25 +0900 Subject: [PATCH 395/868] test: refactor container_update_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_update_linux_test.go | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/cmd/nerdctl/container/container_update_linux_test.go b/cmd/nerdctl/container/container_update_linux_test.go index a4091f4156a..f51b277bbaf 100644 --- a/cmd/nerdctl/container/container_update_linux_test.go +++ b/cmd/nerdctl/container/container_update_linux_test.go @@ -17,17 +17,53 @@ package container import ( + "errors" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestUpdateContainer(t *testing.T) { - testutil.DockerIncompatible(t) - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) - base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", testContainerName).Run() - base.Cmd("update", "--memory", "999999999", "--restart", "123", testContainerName).AssertFail() - base.Cmd("inspect", "--mode=native", testContainerName).AssertOutNotContains(`"limit": 999999999,`) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, containerName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", containerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "should fail on unsupported restart policy value", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("update", "--memory", "999999999", "--restart", "123", containerName) + }, + Expected: test.Expects(1, []error{errors.New("unsupported restart policy")}, nil), + }, + { + Description: "should not update memory in inspect", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", "--mode=native", containerName) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain(`"limit": 999999999,`)), + }, + } + + testCase.Run(t) } From b98ae8bddf106eeecfe2743d91d068923bf16980 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Wed, 21 Jan 2026 11:37:43 +0100 Subject: [PATCH 396/868] feat: add --source-policy-file flag to nerdctl build Add support for BuildKit source policies via `nerdctl build --source-policy-file`. This enables reproducible and policy-driven builds (pin base images to digests, deny/allow sources, enforce HTTP checksums) without modifying Dockerfiles. The implementation: - Adds --source-policy-file flag that passes through to buildctl - Supports EXPERIMENTAL_BUILDKIT_SOURCE_POLICY env var for Docker Buildx compatibility - Flag takes precedence over env var when both are set This is a minimal passthrough to BuildKit - nerdctl does not validate the policy file; BuildKit handles all validation and error messages. See: https://github.com/moby/buildkit/blob/master/docs/build-repro.md Signed-off-by: Konstantin Vyatkin Co-Authored-By: Claude Opus 4.5 --- cmd/nerdctl/builder/builder_build.go | 6 ++++ docs/command-reference.md | 2 ++ pkg/api/types/builder_types.go | 3 ++ pkg/cmd/builder/build.go | 15 +++++++++ pkg/cmd/builder/build_test.go | 46 ++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+) diff --git a/cmd/nerdctl/builder/builder_build.go b/cmd/nerdctl/builder/builder_build.go index 32a2937ed75..fae8a00b4ba 100644 --- a/cmd/nerdctl/builder/builder_build.go +++ b/cmd/nerdctl/builder/builder_build.go @@ -81,6 +81,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container cmd.Flags().String("iidfile", "", "Write the image ID to the file") cmd.Flags().StringArray("label", nil, "Set metadata for an image") + cmd.Flags().String("source-policy-file", "", "BuildKit source policy file (see https://github.com/moby/buildkit/blob/master/docs/build-repro.md)") return cmd } @@ -209,6 +210,10 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu if err != nil { return types.BuilderBuildOptions{}, err } + sourcePolicyFile, err := cmd.Flags().GetString("source-policy-file") + if err != nil { + return types.BuilderBuildOptions{}, err + } usernsRemap, err := cmd.Flags().GetString("userns-remap") if err != nil { @@ -246,6 +251,7 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu NetworkMode: network, ExtendedBuildContext: extendedBuildCtx, ExtraHosts: extraHosts, + SourcePolicyFile: sourcePolicyFile, }, nil } diff --git a/docs/command-reference.md b/docs/command-reference.md index 3ad8cc421e2..5eed939114e 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -780,6 +780,8 @@ Flags: - :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`) - :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp) - :whale: `--add-host`: Add a custom host-to-IP mapping (format: `host:ip`) +- :nerd_face: `--source-policy-file`: BuildKit source policy JSON file for reproducible builds. See [BuildKit build-repro docs](https://github.com/moby/buildkit/blob/master/docs/build-repro.md). + For compatibility with Docker Buildx, the `EXPERIMENTAL_BUILDKIT_SOURCE_POLICY` environment variable is also supported. Example no-op policy: `{"rules":[]}` Unimplemented `docker build` flags: `--squash` diff --git a/pkg/api/types/builder_types.go b/pkg/api/types/builder_types.go index b9574aebcc6..0d9445be505 100644 --- a/pkg/api/types/builder_types.go +++ b/pkg/api/types/builder_types.go @@ -73,6 +73,9 @@ type BuilderBuildOptions struct { Pull *bool // ExtraHosts is a set of custom host-to-IP mappings. ExtraHosts []string + // SourcePolicyFile is the path to a BuildKit source policy file. + // Passed through to buildctl as --source-policy-file. + SourcePolicyFile string } // BuilderPruneOptions specifies options for `nerdctl builder prune`. diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index 835b2ece8f1..9cd9ce15d5f 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -194,6 +194,16 @@ func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotte return nil } +// GetEffectiveSourcePolicyFile returns the effective source policy file path. +// If optionValue is set, it takes precedence. Otherwise, the EXPERIMENTAL_BUILDKIT_SOURCE_POLICY +// environment variable is used for Docker Buildx compatibility. +func GetEffectiveSourcePolicyFile(optionValue string) string { + if optionValue != "" { + return optionValue + } + return os.Getenv("EXPERIMENTAL_BUILDKIT_SOURCE_POLICY") +} + func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (buildCtlBinary string, buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) { @@ -472,6 +482,11 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option buildctlArgs = append(buildctlArgs, "--opt=add-hosts="+strings.Join(extraHosts, ",")) } + // Source policy file: use explicit option if set, otherwise fallback to env var for Buildx compatibility + if sourcePolicyFile := GetEffectiveSourcePolicyFile(options.SourcePolicyFile); sourcePolicyFile != "" { + buildctlArgs = append(buildctlArgs, "--source-policy-file="+sourcePolicyFile) + } + return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil } diff --git a/pkg/cmd/builder/build_test.go b/pkg/cmd/builder/build_test.go index 081d899d7f3..246c64efc17 100644 --- a/pkg/cmd/builder/build_test.go +++ b/pkg/cmd/builder/build_test.go @@ -238,3 +238,49 @@ func TestParseBuildctlArgsForOCILayout(t *testing.T) { }) } } + +func TestGetEffectiveSourcePolicyFile(t *testing.T) { + // Cannot use t.Parallel() since subtests modify environment variables + + tests := []struct { + name string + optionValue string + envValue string + expected string + }{ + { + name: "option value takes precedence over env var", + optionValue: "/path/from/flag.json", + envValue: "/path/from/env.json", + expected: "/path/from/flag.json", + }, + { + name: "env var is used when option is empty", + optionValue: "", + envValue: "/path/from/env.json", + expected: "/path/from/env.json", + }, + { + name: "empty when both are unset", + optionValue: "", + envValue: "", + expected: "", + }, + { + name: "option value used when env var is empty", + optionValue: "/path/from/flag.json", + envValue: "", + expected: "/path/from/flag.json", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Set up the environment variable for this test + t.Setenv("EXPERIMENTAL_BUILDKIT_SOURCE_POLICY", tc.envValue) + + result := GetEffectiveSourcePolicyFile(tc.optionValue) + assert.Equal(t, result, tc.expected) + }) + } +} From 97facf2e5657cbf5ea27d7a745c731b2f28102e3 Mon Sep 17 00:00:00 2001 From: David Son Date: Fri, 16 Jan 2026 02:07:36 +0000 Subject: [PATCH 397/868] feat: add support for container cp with tarballs Signed-off-by: David Son --- cmd/nerdctl/container/container_cp_linux.go | 14 +- .../container/container_cp_linux_test.go | 213 ++++++++++++++++-- docs/command-reference.md | 2 + pkg/api/types/container_types.go | 4 + pkg/containerutil/cp_linux.go | 71 ++++-- pkg/containerutil/cp_resolve_linux.go | 9 +- 6 files changed, 266 insertions(+), 47 deletions(-) diff --git a/cmd/nerdctl/container/container_cp_linux.go b/cmd/nerdctl/container/container_cp_linux.go index 50479df2a45..e9124fbab66 100644 --- a/cmd/nerdctl/container/container_cp_linux.go +++ b/cmd/nerdctl/container/container_cp_linux.go @@ -107,12 +107,6 @@ func copyOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, e if srcSpec.Container == nil && destSpec.Container == nil { return types.ContainerCpOptions{}, fmt.Errorf("one of src or dest must be a container file specification") } - if srcSpec.Path == "-" { - return types.ContainerCpOptions{}, fmt.Errorf("support for reading a tar archive from stdin is not implemented yet") - } - if destSpec.Path == "-" { - return types.ContainerCpOptions{}, fmt.Errorf("support for writing a tar archive to stdout is not implemented yet") - } container2host := srcSpec.Container != nil var containerReq string @@ -128,6 +122,8 @@ func copyOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, e DestPath: destSpec.Path, SrcPath: srcSpec.Path, FollowSymLink: flagL, + FromStdin: srcSpec.Path == "-", + ToStdout: destSpec.Path == "-", }, nil } @@ -138,6 +134,12 @@ func AddCpCommand(rootCmd *cobra.Command) { var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path") func parseCpFileSpec(arg string) (*copyFileSpec, error) { + if arg == "" { + return ©FileSpec{ + Path: "-", + }, nil + } + i := strings.Index(arg, ":") // filespec starting with a semicolon is invalid diff --git a/cmd/nerdctl/container/container_cp_linux_test.go b/cmd/nerdctl/container/container_cp_linux_test.go index 1a268caa60e..6235df02ed3 100644 --- a/cmd/nerdctl/container/container_cp_linux_test.go +++ b/cmd/nerdctl/container/container_cp_linux_test.go @@ -19,6 +19,7 @@ package container import ( "fmt" "os" + "os/exec" "path/filepath" "strings" "syscall" @@ -29,6 +30,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/tarutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -51,7 +53,9 @@ const ( pathIsADirAbsolute = string(os.PathSeparator) + "is-a-dir" + complexify pathIsAVolumeMount = string(os.PathSeparator) + "is-a-volume-mount" + complexify - srcFileName = "test-file" + complexify + srcFileName = "test-file" + complexify + tarballName = "test-tar" + complexify + cpFolderName = "nerdctl-cp-test" // Since nerdctl cp must NOT obey container wd, but instead resolve paths against the root, we set this // explicitly to ensure we do the right thing wrt that. @@ -400,6 +404,49 @@ func TestCopyToContainer(t *testing.T) { }, }, }, + { + description: "Copying to container, SRC_PATH is stdin", + sourceSpec: "-", + sourceIsAFile: true, + toContainer: true, + testCases: []testcases{ + { + description: "DEST_PATH is a directory, relative", + destinationSpec: pathIsADirRelative, + catFile: filepath.Join(pathIsADirRelative, srcFileName), + setup: func(base *testutil.Base, container string, destPath string) { + base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + }, + }, + { + description: "DEST_PATH is a directory, absolute", + destinationSpec: pathIsADirAbsolute, + catFile: filepath.Join(pathIsADirAbsolute, srcFileName), + setup: func(base *testutil.Base, container string, destPath string) { + base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + }, + }, + { + description: "DEST_PATH is stdout", + destinationSpec: "-", + expect: icmd.Expected{ + ExitCode: 1, + Err: "one of src or dest must be a container file specification", + }, + }, + { + description: "DEST_PATH is a file", + destinationSpec: pathIsAFileAbsolute, + setup: func(base *testutil.Base, container string, destPath string) { + base.Cmd("exec", container, "touch", destPath).AssertOK() + }, + expect: icmd.Expected{ + ExitCode: 1, + Err: containerutil.ErrCannotCopyDirToFile.Error(), + }, + }, + }, + }, } for _, tg := range testGroups { @@ -540,6 +587,19 @@ func TestCopyFromContainer(t *testing.T) { assert.NilError(t, err) }, }, + { + description: "DEST_PATH is stdout", + destinationSpec: "-", + // Extra dir to account for folder created from extracted tar file + catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName), + expect: icmd.Expected{ + ExitCode: 0, + }, + setup: func(base *testutil.Base, container string, destPath string) { + err := os.MkdirAll(destPath, dirPerm) + assert.NilError(t, err) + }, + }, }, }, { @@ -682,6 +742,19 @@ func TestCopyFromContainer(t *testing.T) { assert.NilError(t, err) }, }, + { + description: "DEST_PATH is stdout", + destinationSpec: "-", + catFile: filepath.Join(pathIsADirAbsolute, srcDirName, srcFileName), + expect: icmd.Expected{ + ExitCode: 0, + }, + setup: func(base *testutil.Base, container string, destPath string) { + // Don't make the topmost dir as this is where the tarball must extract + err := os.MkdirAll(filepath.Dir(destPath), dirPerm) + assert.NilError(t, err) + }, + }, }, }, @@ -713,6 +786,18 @@ func TestCopyFromContainer(t *testing.T) { assert.NilError(t, err) }, }, + { + description: "DEST_PATH is stdout", + destinationSpec: "-", + catFile: filepath.Join(pathIsADirAbsolute, srcFileName), + expect: icmd.Expected{ + ExitCode: 0, + }, + setup: func(base *testutil.Base, container string, destPath string) { + err := os.MkdirAll(destPath, dirPerm) + assert.NilError(t, err) + }, + }, }, }, } @@ -749,7 +834,12 @@ func cpTestHelper(t *testing.T, tg *testgroup) { // Get the source path groupSourceSpec := tg.sourceSpec groupSourceDir := groupSourceSpec - if tg.sourceIsAFile { + fromStdin := false + if tg.sourceSpec == "-" { + groupSourceSpec = filepath.Join(srcDirName, tarballName) + groupSourceDir = srcDirName + fromStdin = true + } else if tg.sourceIsAFile { groupSourceDir = filepath.Dir(groupSourceSpec) } @@ -794,25 +884,37 @@ func cpTestHelper(t *testing.T, tg *testgroup) { // Prepare the specs and derived variables sourceSpec := groupSourceSpec + catFile := testCase.catFile + destinationSpec := testCase.destinationSpec + toStdout := false + // tarball destination just sets up the dir to extract to + if destinationSpec == "-" { + toStdout = true + destinationSpec = filepath.Dir(catFile) + } // If the test case does not specify a catFile, start with the destination spec - catFile := testCase.catFile if catFile == "" { catFile = destinationSpec } sourceFile := filepath.Join(groupSourceDir, srcFileName) if copyToContainer { - // Use an absolute path for evaluation if !filepath.IsAbs(catFile) { catFile = filepath.Join(string(os.PathSeparator), catFile) } - // If the sourceFile is still relative, make it absolute to the temp - sourceFile = filepath.Join(tempDir, sourceFile) - // If the spec path for source on the host was absolute, make sure we put that under tempDir - if filepath.IsAbs(sourceSpec) { - sourceSpec = tempDir + sourceSpec + + if fromStdin { + sourceFile = filepath.Join(tempDir, groupSourceDir, tarballName) + } else { + // Use an absolute path for evaluation + // If the sourceFile is still relative, make it absolute to the temp + sourceFile = filepath.Join(tempDir, sourceFile) + // If the spec path for source on the host was absolute, make sure we put that under tempDir + if filepath.IsAbs(sourceSpec) { + sourceSpec = tempDir + sourceSpec + } } } else { // If we are copying to host, we need to make sure we have an absolute path to cat, relative to temp, @@ -835,11 +937,29 @@ func cpTestHelper(t *testing.T, tg *testgroup) { } createFileOnHost := func() { - // Create file on the host - err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(sourceFile, sourceFileContent, filePerm) - assert.NilError(t, err) + switch fromStdin { + case true: + d := filepath.Dir(sourceFile) + tarCpFolder := filepath.Join(d, cpFolderName) + tarBinary, _, err := tarutil.FindTarBinary() + assert.NilError(t, err) + + err = os.MkdirAll(tarCpFolder, dirPerm) + assert.NilError(t, err) + err = os.WriteFile(filepath.Join(tarCpFolder, srcFileName), sourceFileContent, filePerm) + assert.NilError(t, err) + + err = exec.Command(tarBinary, "-cf", sourceFile, "-C", tarCpFolder, ".").Run() + assert.NilError(t, err) + err = os.RemoveAll(tarCpFolder) + assert.NilError(t, err) + case false: + // Create file on the host + err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm) + assert.NilError(t, err) + err = os.WriteFile(sourceFile, sourceFileContent, filePerm) + assert.NilError(t, err) + } } // Setup: create volume, containers, create the source file @@ -906,10 +1026,46 @@ func cpTestHelper(t *testing.T, tg *testgroup) { // Build the final src and dest specifiers, including `containerXYZ:` container := "" if copyToContainer { + if fromStdin { + if toStdout { + nerdctlCmd := base.Cmd("cp", "-", "-") + nerdctlCmd.Run() + nerdctlCmd.Assert(testCase.expect) + } else { + sourceSpec = "-" + f, err := os.Open(sourceFile) + assert.NilError(t, err) + nerdctlCmd := base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec) + nerdctlCmd.Stdin = f + + nerdctlCmd.Run() + nerdctlCmd.Assert(testCase.expect) + f.Close() + } + } else { + base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect) + } container = containerRunning - base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect) } else { - base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec).Assert(testCase.expect) + nerdctlCmd := base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec) + if toStdout { + out := nerdctlCmd.Out() + nerdctlCmd.Assert(testCase.expect) + + // Since we can't check tar file directly easily, extract to the same destination + tarDst := filepath.Dir(catFile) + tarBinary, _, err := tarutil.FindTarBinary() + assert.NilError(t, err) + + tarCmd := exec.Command(tarBinary, "-C", tarDst, "-xf", "-") + tarCmd.Stdin = strings.NewReader(out) + tarCmd.Stdout = os.Stdout + + tarCmd.Run() + assert.NilError(t, tarCmd.Err) + } else { + nerdctlCmd.Assert(testCase.expect) + } } // Run the actual test for the running container @@ -932,19 +1088,32 @@ func cpTestHelper(t *testing.T, tg *testgroup) { // ... and for the stopped container container = "" var cmd *testutil.Cmd - if copyToContainer { + if fromStdin && toStdout { + cmd = base.Cmd("cp", "-", "-") + } else if copyToContainer { container = containerStopped cmd = base.Cmd("cp", sourceSpec, containerStopped+":"+destinationSpec) + if fromStdin { + f, err := os.Open(sourceFile) + assert.NilError(t, err) + defer f.Close() + cmd.Stdin = f + } } else { cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec) } if rootlessutil.IsRootless() && !nerdtest.IsDocker() { - cmd.Assert( - icmd.Expected{ - ExitCode: 1, - Err: containerutil.ErrRootlessCannotCp.Error(), - }) + if fromStdin && toStdout { + // Regular assert test case should work fine if src and dst are invalid + cmd.Assert(testCase.expect) + } else { + cmd.Assert( + icmd.Expected{ + ExitCode: 1, + Err: containerutil.ErrRootlessCannotCp.Error(), + }) + } return } diff --git a/docs/command-reference.md b/docs/command-reference.md index 3ad8cc421e2..2ae8b406042 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -494,6 +494,8 @@ Usage: - `nerdctl cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-` - `nerdctl cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH` +Using `-` as the `SRC_PATH` streams the contents of `STDIN` as a tar archive. The command extracts the content of the tar to the `DEST_PATH` in container's filesystem. In this case, `DEST_PATH` must specify a directory. Using `-` as the `DEST_PATH` streams the contents of the resource as a tar archive to `STDOUT`. + :warning: `nerdctl cp` is designed only for use with trusted, cooperating containers. Using `nerdctl cp` with untrusted or malicious containers is unsupported and may not provide protection against unexpected behavior. diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index ac893c1b080..8dd099eb53d 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -541,6 +541,10 @@ type ContainerCpOptions struct { SrcPath string // Follow symbolic links in SRC_PATH FollowSymLink bool + // true if copying to container from tarball in stdin + FromStdin bool + // true if copying from container to stdout in tarball format + ToStdout bool } // ContainerStatsOptions specifies options for `nerdctl stats`. diff --git a/pkg/containerutil/cp_linux.go b/pkg/containerutil/cp_linux.go index 77425aa57be..6c2646e3c51 100644 --- a/pkg/containerutil/cp_linux.go +++ b/pkg/containerutil/cp_linux.go @@ -17,6 +17,7 @@ package containerutil import ( + "bufio" "bytes" "context" "errors" @@ -147,9 +148,25 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain var sourceErr, destErr error if options.Container2Host { sourceSpec, sourceErr = getPathSpecFromContainer(options.SrcPath, conSpec, root) - destinationSpec, destErr = getPathSpecFromHost(options.DestPath) + if options.ToStdout { + destinationSpec = &pathSpecifier{ + exists: true, + isADir: true, + toStdout: true, + } + } else { + destinationSpec, destErr = getPathSpecFromHost(options.DestPath) + } } else { - sourceSpec, sourceErr = getPathSpecFromHost(options.SrcPath) + if options.FromStdin { + sourceSpec = &pathSpecifier{ + exists: true, + isADir: true, + fromStdin: true, + } + } else { + sourceSpec, sourceErr = getPathSpecFromHost(options.SrcPath) + } destinationSpec, destErr = getPathSpecFromContainer(options.DestPath, conSpec, root) } @@ -212,7 +229,7 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain tarCDir = filepath.Dir(sourceSpec.resolvedPath) tarCArg = filepath.Base(sourceSpec.resolvedPath) } - } else { + } else if !sourceSpec.fromStdin { // Prepare a single-file directory to create an archive of the source file td, err := os.MkdirTemp("", "nerdctl-cp") if err != nil { @@ -224,7 +241,7 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain if options.FollowSymLink { cp = append(cp, "-L") } - if destinationSpec.endsWithSeparator || (destinationSpec.exists && destinationSpec.isADir) { + if destinationSpec.toStdout || destinationSpec.endsWithSeparator || (destinationSpec.exists && destinationSpec.isADir) { tarCArg = filepath.Base(sourceSpec.resolvedPath) } else { // Handle `nerdctl cp /path/to/file some-container:/path/to/file-with-another-name` @@ -237,21 +254,31 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain return fmt.Errorf("failed to execute %v: %w (out=%q)", cpCmd.Args, err, string(out)) } } - tarC := []string{tarBinary} - if options.FollowSymLink { - tarC = append(tarC, "-h") + var tarC []string + if sourceSpec.fromStdin { + tarC = []string{"echo", "reading tar from stdin"} + } else { + tarC = []string{tarBinary} + if options.FollowSymLink { + tarC = append(tarC, "-h") + } + tarC = append(tarC, "-c", "-f", "-", tarCArg) } - tarC = append(tarC, "-c", "-f", "-", tarCArg) tarXDir := destinationSpec.resolvedPath if !sourceSpec.isADir && !destinationSpec.endsWithSeparator && !(destinationSpec.exists && destinationSpec.isADir) { tarXDir = filepath.Dir(destinationSpec.resolvedPath) } - tarX := []string{tarBinary, "-x"} - if options.Container2Host && isGNUTar { - tarX = append(tarX, "--no-same-owner") + var tarX []string + if destinationSpec.toStdout { + tarX = []string{"echo", "writing tar to stdout"} + } else { + tarX = []string{tarBinary, "-x"} + if options.Container2Host && isGNUTar { + tarX = append(tarX, "--no-same-owner") + } + tarX = append(tarX, "-f", "-") } - tarX = append(tarX, "-f", "-") if rootlessutil.IsRootless() { nsenter := []string{"nsenter", "-t", strconv.Itoa(pid), "-U", "--preserve-credentials", "--"} @@ -272,11 +299,23 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...) tarXCmd.Dir = tarXDir - tarXCmd.Stdin, err = tarCCmd.StdoutPipe() - if err != nil { - return err + if sourceSpec.fromStdin { + // Reading from tar should pipe stdin into dst + tarXCmd.Stdin = bufio.NewReader(os.Stdin) + tarXCmd.Stdout = os.Stderr + } else if destinationSpec.toStdout { + // Writing to tar should just write output to stdout. (Really we don't even need tarXCmd for this case.) + tarXCmd.Stdin = nil + tarXCmd.Stdout = nil + tarCCmd.Stdout = os.Stdout + } else { + tarXCmd.Stdin, err = tarCCmd.StdoutPipe() + if err != nil { + return err + } + tarXCmd.Stdout = tarCCmd.Stderr } - tarXCmd.Stdout = os.Stderr + var tarErr bytes.Buffer tarXCmd.Stderr = &tarErr diff --git a/pkg/containerutil/cp_resolve_linux.go b/pkg/containerutil/cp_resolve_linux.go index ab22abaf38e..1ee747730c2 100644 --- a/pkg/containerutil/cp_resolve_linux.go +++ b/pkg/containerutil/cp_resolve_linux.go @@ -48,13 +48,16 @@ var ( // besides exposing relevant properties (endsWithSeparator, etc), it also provides a fully resolved *host* path to // access the resource type pathSpecifier struct { - originalPath string + originalPath string + resolvedPath string + endsWithSeparator bool endsWithSeparatorDot bool exists bool isADir bool readOnly bool - resolvedPath string + fromStdin bool + toStdout bool } // getPathSpecFromHost builds a pathSpecifier from a host location @@ -132,7 +135,7 @@ func getPathSpecFromHost(originalPath string) (*pathSpecifier, error) { return pathSpec, nil } -// getPathSpecFromHost builds a pathSpecifier from a container location +// getPathSpecFromContainer builds a pathSpecifier from a container location func getPathSpecFromContainer(originalPath string, conSpec *oci.Spec, containerHostRoot string) (*pathSpecifier, error) { pathSpec := &pathSpecifier{ originalPath: originalPath, From fc31363ab9053d2fdfb37a8d4e1e33404f5c55e3 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Mon, 26 Jan 2026 22:13:53 +0800 Subject: [PATCH 398/868] tests: fix race condition in TestRunWithSystemdTrueEnabled consolidate subtests into sequential commands to prevent runc exec conflicts. Signed-off-by: ChengyuZhu6 --- .../container_run_systemd_linux_test.go | 63 ++++++++----------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go index 178c5a7ed9c..5ddc734464e 100644 --- a/cmd/nerdctl/container/container_run_systemd_linux_test.go +++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go @@ -73,49 +73,36 @@ func TestRunWithSystemdTrueEnabled(t *testing.T) { ) testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) - data.Labels().Set("containerName", containerName) - helpers.Ensure("run", "-d", "--name", containerName, "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) - nerdtest.EnsureContainerStarted(helpers, containerName) + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - containerName := data.Labels().Get("containerName") - helpers.Anyhow("container", "rm", "-f", containerName) + helpers.Anyhow("container", "rm", "-f", data.Identifier()) } - testCase.SubTests = []*test.Case{ - { - Description: "should expose SIGTERM+3 stop signal labels", - NoParallel: true, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName := data.Labels().Get("containerName") - return helpers.Command("inspect", "--format", "{{json .Config.Labels}}", containerName) - }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("SIGRTMIN+3")), - }, - { - Description: "waits for systemd to become ready and lists systemd jobs", - NoParallel: true, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName := data.Labels().Get("containerName") - return helpers.Command("exec", containerName, "sh", "-c", "--", `tries=0 - - until systemctl is-system-running >/dev/null 2>&1; do - - >&2 printf "Waiting for systemd to come up...\n" - sleep 1s - tries=$(( tries + 1)) - [ $tries -lt 10 ] || { - >&2 printf "systemd failed to come up in a reasonable amount of time\n" - exit 1 - } - done - systemctl list-jobs`) - }, - Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("jobs")), - }, - } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // should expose SIGTERM+3 stop signal labels + helpers.Command("inspect", "--format", "{{json .Config.Labels}}", data.Identifier()). + Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("SIGRTMIN+3"), + }) + + // waits for systemd to become ready and lists systemd jobs + return helpers.Command("exec", data.Identifier(), "sh", "-c", "--", `tries=0 +until systemctl is-system-running >/dev/null 2>&1; do + >&2 printf "Waiting for systemd to come up...\n" + sleep 1s + tries=$(( tries + 1)) + [ $tries -lt 10 ] || { + >&2 printf "systemd failed to come up in a reasonable amount of time\n" + exit 1 + } +done +systemctl list-jobs`) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("jobs")) testCase.Run(t) } From a91bb8d0459c73d3be283f19159f82fd50632bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:54:15 +0000 Subject: [PATCH 399/868] build(deps): bump actions/checkout from 6.0.1 to 6.0.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index c66a77c7a24..da4ce7a40a7 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 824fc6f0514..6951e057b80 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 7aeff3ad851..44b701af882 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 4de2f1457b8..25826f41226 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index a8aa3185fd5..2878b376559 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 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 61c55559ae0..5392a8d5c86 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 43790b93e4d..8e57d90cda2 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index a5c0c001240..f61676bfe8a 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -71,7 +71,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index b3c48abdae7..621c38cb6ee 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -30,7 +30,7 @@ jobs: TARGET: ${{ inputs.target }} steps: - name: "Init: checkout" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 10ae6c5d922..34024ff7e67 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 2963e0c28c0..63f1cc3836c 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 585c6e7b5f0..6ba888e1100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index f6c6d890090..5640cb337a7 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -46,7 +46,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: "Run" diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 4717a117df2..6b868c3deed 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -32,7 +32,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 100 - if: ${{ matrix.canary }} From 5b0f270b6df679f9b06b9ebf185cc4ead66b2077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:03:52 +0000 Subject: [PATCH 400/868] build(deps): bump github.com/coreos/go-systemd/v22 from 22.6.0 to 22.7.0 Bumps [github.com/coreos/go-systemd/v22](https://github.com/coreos/go-systemd) from 22.6.0 to 22.7.0. - [Release notes](https://github.com/coreos/go-systemd/releases) - [Commits](https://github.com/coreos/go-systemd/compare/v22.6.0...v22.7.0) --- updated-dependencies: - dependency-name: github.com/coreos/go-systemd/v22 dependency-version: 22.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5e2b920b064..01234ef58b7 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.9.0 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined - github.com/coreos/go-systemd/v22 v22.6.0 + github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.1.5+incompatible //gomodjail:unconfined diff --git a/go.sum b/go.sum index cfcadab33de..f1cf4630c4e 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpV github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= 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= From 175966e22673bea4e51c37ab3e30c165f2d2ddac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:04:05 +0000 Subject: [PATCH 401/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.10 to 0.15.11. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.10...v0.15.11) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5e2b920b064..125151a3645 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.10 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.11 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index cfcadab33de..6d6838fc1c8 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.10 h1:hphjuKOqSHLGznNJiAvmsOWkdu4qFXjf4DzGrWSuIsM= -github.com/containerd/nydus-snapshotter v0.15.10/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= +github.com/containerd/nydus-snapshotter v0.15.11 h1:YTdF4rsjFRsfyaIhnWVUSLz8FqJwOyRZ5FhvFjHh7Uc= +github.com/containerd/nydus-snapshotter v0.15.11/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 02aec5957db32f725918e602783e2648323e96f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:10:34 +0000 Subject: [PATCH 402/868] build(deps): bump docker/login-action from 3.6.0 to 3.7.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/5e57cd118135c172c3672efd75eb46360885c0ef...c94ce9fb468520275223c153574b00df6fe4bcc9) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 3.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index c66a77c7a24..7c0554d4576 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 0759e926037b18113bead0ef1766844dfc9afc5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:10:54 +0000 Subject: [PATCH 403/868] build(deps): bump actions/cache from 5.0.2 to 5.0.3 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.2 to 5.0.3. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/8b402f58fbc84540c8b491a91e594a4576fec3d7...cdf6c1fa76f9f475f3d7449005a359c84ca0f306) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index b3c48abdae7..dd4124bb78b 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 10ae6c5d922..77bf840109b 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: /root/.vagrant.d key: vagrant From f8cefd27a399ae3c420cde99c663a21538e16891 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:11:00 +0000 Subject: [PATCH 404/868] build(deps): bump actions/attest-build-provenance from 3.1.0 to 3.2.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8...96278af6caaf10aea03fd8d33a09a777ca52d62f) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 585c6e7b5f0..035bab2b7b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From 4f30dba6f67c4ae46e563efd19577300ae0a0835 Mon Sep 17 00:00:00 2001 From: Hajime Ogi Date: Fri, 30 Jan 2026 22:41:49 +0900 Subject: [PATCH 405/868] test: refactor compose_port_linux_test.go to use Tigron Signed-off-by: Hajime Ogi --- .../compose/compose_port_linux_test.go | 153 ++++++++++++++---- 1 file changed, 121 insertions(+), 32 deletions(-) diff --git a/cmd/nerdctl/compose/compose_port_linux_test.go b/cmd/nerdctl/compose/compose_port_linux_test.go index 514740be130..e9dbbbc7b20 100644 --- a/cmd/nerdctl/compose/compose_port_linux_test.go +++ b/cmd/nerdctl/compose/compose_port_linux_test.go @@ -18,6 +18,8 @@ package compose import ( "fmt" + "path/filepath" + "strconv" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" @@ -25,59 +27,146 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestComposePort(t *testing.T) { - base := testutil.NewBase(t) + const portCount = 2 - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + for i := 0; i < portCount; i++ { + 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(fmt.Sprintf("hostPort%d", i), strconv.Itoa(port)) + } + + dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" ports: - - "12345:10000" - - "12346:10001/udp" -`, testutil.CommonImage) + - "%s:10000" + - "%s:10001/udp" +`, testutil.CommonImage, data.Labels().Get("hostPort0"), data.Labels().Get("hostPort1")) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", compYamlPath) + projectName := filepath.Base(filepath.Dir(compYamlPath)) + t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") + } - // `port` should work for given port and protocol - base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "svc0", "10000").AssertOutExactly("0.0.0.0:12345\n") - base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "udp", "svc0", "10001").AssertOutExactly("0.0.0.0:12346\n") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + for i := 0; i < portCount; i++ { + port, _ := strconv.Atoi(data.Labels().Get(fmt.Sprintf("hostPort%d", i))) + _ = portlock.Release(port) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "port should return host port for TCP", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "10000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(fmt.Sprintf("0.0.0.0:%s\n", data.Labels().Get("hostPort0"))), + } + }, + }, + { + Description: "port should return host port for UDP", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "udp", "svc0", "10001") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(fmt.Sprintf("0.0.0.0:%s\n", data.Labels().Get("hostPort1"))), + } + }, + }, + } + + testCase.Run(t) } func TestComposePortFailure(t *testing.T) { - base := testutil.NewBase(t) + const portCount = 2 - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + for i := 0; i < portCount; i++ { + 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(fmt.Sprintf("hostPort%d", i), strconv.Itoa(port)) + } + + dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s command: "sleep infinity" ports: - - "12345:10000" - - "12346:10001/udp" -`, testutil.CommonImage) - - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // `port` should fail if given port and protocol don't exist - base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "svc0", "9999").AssertFail() - base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "udp", "svc0", "10000").AssertFail() - base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "tcp", "svc0", "10001").AssertFail() + - "%s:10000" + - "%s:10001/udp" +`, testutil.CommonImage, data.Labels().Get("hostPort0"), data.Labels().Get("hostPort1")) + + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", compYamlPath) + projectName := filepath.Base(filepath.Dir(compYamlPath)) + t.Logf("projectName=%q", projectName) + + helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + for i := 0; i < portCount; i++ { + port, _ := strconv.Atoi(data.Labels().Get(fmt.Sprintf("hostPort%d", i))) + _ = portlock.Release(port) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "port should fail for non-existent port", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "9999") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "port should fail for wrong protocol (UDP on TCP port)", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "udp", "svc0", "10000") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "port should fail for wrong protocol (TCP on UDP port)", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "--protocol", "tcp", "svc0", "10001") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + + testCase.Run(t) } // TestComposeMultiplePorts tests whether it is possible to allocate a large From 248a714261b6710c3c4b9470e16ef1de2aa906c5 Mon Sep 17 00:00:00 2001 From: Hajime Ogi Date: Fri, 30 Jan 2026 22:45:50 +0900 Subject: [PATCH 406/868] test: improve TestComposeMultiplePorts setup and parallel handling Signed-off-by: Hajime Ogi --- cmd/nerdctl/compose/compose_port_linux_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/compose/compose_port_linux_test.go b/cmd/nerdctl/compose/compose_port_linux_test.go index e9dbbbc7b20..34942ccafda 100644 --- a/cmd/nerdctl/compose/compose_port_linux_test.go +++ b/cmd/nerdctl/compose/compose_port_linux_test.go @@ -172,7 +172,12 @@ services: // TestComposeMultiplePorts tests whether it is possible to allocate a large // number of ports. (https://github.com/containerd/nerdctl/issues/4027) func TestComposeMultiplePorts(t *testing.T) { - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` services: svc0: image: %s @@ -181,11 +186,10 @@ services: - '32000-32060:32000-32060' `, testutil.AlpineImage) - testCase := nerdtest.Setup() - - testCase.Setup = func(data test.Data, helpers test.Helpers) { compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") data.Labels().Set("composeYaml", compYamlPath) + projectName := filepath.Base(filepath.Dir(compYamlPath)) + t.Logf("projectName=%q", projectName) helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") } @@ -197,7 +201,6 @@ services: testCase.SubTests = []*test.Case{ { Description: "Issue #4027 - Allocate a large number of ports.", - NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "port", "svc0", "32000") }, From 09660a19c4ba6b49c3fbf68c6ad76102cd3b5d22 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 1 Feb 2026 17:26:10 +0900 Subject: [PATCH 407/868] fix: add missing --ipc options in help text and shell completion The docs already documented `--ipc=(host|private|shareable|container:)`. - https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-run However, the implementation only had `host` and `private` in help text and completion. This adds the missing options and smart completion for `container:` Signed-off-by: Hayato Kiwata --- cmd/nerdctl/container/container_run.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 81904491256..5daead7b8f4 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -26,6 +26,7 @@ import ( "golang.org/x/term" "github.com/containerd/console" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" @@ -138,9 +139,19 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().String("mac-address", "", "MAC address to assign to the container") // #endregion - cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private")`) + cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private"|"shareable"|"container:")`) cmd.RegisterFlagCompletionFunc("ipc", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp + if strings.HasPrefix(toComplete, "container:") { + names, directive := completion.ContainerNames(cmd, func(st containerd.ProcessStatus) bool { + return st == containerd.Running + }) + var candidates []string + for _, name := range names { + candidates = append(candidates, "container:"+name) + } + return candidates, directive + } + return []string{"host", "private", "shareable", "container:"}, cobra.ShellCompDirectiveNoSpace }) // #region cgroups, namespaces, and ulimits flags cmd.Flags().Float64("cpus", 0.0, "Number of CPUs") From 5c1a71e9a0a36b1370575b95e3e943061218427d Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Sun, 25 Jan 2026 17:26:18 +0900 Subject: [PATCH 408/868] test: refactor container_run_user_windows_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_run_user_windows_test.go | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/cmd/nerdctl/container/container_run_user_windows_test.go b/cmd/nerdctl/container/container_run_user_windows_test.go index e92dc595598..05712f83b29 100644 --- a/cmd/nerdctl/container/container_run_user_windows_test.go +++ b/cmd/nerdctl/container/container_run_user_windows_test.go @@ -19,27 +19,31 @@ package container import ( "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunUserName(t *testing.T) { - base := testutil.NewBase(t) - testCases := map[string]string{ - "": "ContainerAdministrator", - "ContainerAdministrator": "ContainerAdministrator", - "ContainerUser": "ContainerUser", - } - for userStr, expected := range testCases { - userStr := userStr - expected := expected - t.Run(userStr, func(t *testing.T) { - t.Parallel() - cmd := []string{"run", "--rm"} - if userStr != "" { - cmd = append(cmd, "--user", userStr) - } - cmd = append(cmd, testutil.WindowsNano, "whoami") - base.Cmd(cmd...).AssertOutContains(expected) - }) + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "should run Windows container as ContainerAdministrator by default", + Command: test.Command("run", "--rm", testutil.WindowsNano, "whoami"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerAdministrator")), + }, + { + Description: "should run Windows container as ContainerAdministrator when user is set to ContainerAdministrator", + Command: test.Command("run", "--rm", "--user", "ContainerAdministrator", testutil.WindowsNano, "whoami"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerAdministrator")), + }, + { + Description: "should run Windows container as ContainerUser when user is set to ContainerUser", + Command: test.Command("run", "--rm", "--user", "ContainerUser", testutil.WindowsNano, "whoami"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("ContainerUser")), + }, } + testCase.Run(t) } From 4519b52b34e68b5a6b3403a59bb87328552985c5 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Sat, 24 Jan 2026 09:39:19 +0900 Subject: [PATCH 409/868] test: refactor container_inspect_windows_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_inspect_windows_test.go | 102 +++++++++++++----- 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_windows_test.go b/cmd/nerdctl/container/container_inspect_windows_test.go index 8feb7fcd52d..0712319c8b6 100644 --- a/cmd/nerdctl/container/container_inspect_windows_test.go +++ b/cmd/nerdctl/container/container_inspect_windows_test.go @@ -17,50 +17,100 @@ package container import ( + "encoding/json" "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 TestInspectProcessContainerContainsLabel(t *testing.T) { - testContainer := testutil.Identifier(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, containerName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", containerName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", containerName) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Container - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err) + assert.Equal(t, 1, len(dc)) - base.Cmd("run", "-d", "--name", testContainer, "--label", "foo=foo", "--label", "bar=bar", testutil.NginxAlpineImage).AssertOK() - base.EnsureContainerStarted(testContainer) - inspect := base.InspectContainer(testContainer) - lbs := inspect.Config.Labels + assert.Equal(t, "foo", dc[0].Config.Labels["foo"]) + assert.Equal(t, "bar", dc[0].Config.Labels["bar"]) + }, + } + } - assert.Equal(base.T, "foo", lbs["foo"]) - assert.Equal(base.T, "bar", lbs["bar"]) + testCase.Run(t) } func TestInspectHyperVContainerContainsLabel(t *testing.T) { - if !testutil.HyperVSupported() { - t.Skip("HyperV is not enabled, skipping test") + testCase := nerdtest.Setup() + testCase.Require = nerdtest.HyperV + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, "--isolation", "hyperv", "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, containerName) } - testContainer := testutil.Identifier(t) - - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + containerName := data.Labels().Get("containerName") + helpers.Anyhow("rm", "-f", containerName) + } - base.Cmd("run", "-d", "--name", testContainer, "--isolation", "hyperv", "--label", "foo=foo", "--label", "bar=bar", testutil.NginxAlpineImage).AssertOK() - base.EnsureContainerStarted(testContainer) - inspect := base.InspectContainer(testContainer) - lbs := inspect.Config.Labels + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + return helpers.Command("inspect", containerName) + } - //check with HCS if the container is ineed a VM - isHypervContainer, err := testutil.HyperVContainer(inspect) - if err != nil { - t.Fatalf("unable to list HCS containers: %s", err) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err) + assert.Equal(t, 1, len(dc)) + + //check with HCS if the container is ineed a VM + isHypervContainer, err := testutil.HyperVContainer(dc[0]) + assert.NilError(t, err) + assert.Equal(t, true, isHypervContainer) + + assert.Equal(t, "foo", dc[0].Config.Labels["foo"]) + assert.Equal(t, "bar", dc[0].Config.Labels["bar"]) + }, + } } - assert.Assert(t, isHypervContainer, true) - assert.Equal(base.T, "foo", lbs["foo"]) - assert.Equal(base.T, "bar", lbs["bar"]) + testCase.Run(t) } From 3174f3fb44b32b94533ec6a1b582cda1703e2cdd Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Thu, 29 Jan 2026 21:52:58 +0900 Subject: [PATCH 410/868] test: refactor container_run_user_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_run_user_linux_test.go | 195 ++++++++---------- 1 file changed, 83 insertions(+), 112 deletions(-) diff --git a/cmd/nerdctl/container/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go index f9fcf374c76..90e04f01da0 100644 --- a/cmd/nerdctl/container/container_run_user_linux_test.go +++ b/cmd/nerdctl/container/container_run_user_linux_test.go @@ -22,6 +22,7 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -31,163 +32,133 @@ import ( ) func TestRunUserGID(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testCases := map[string]string{ - "": "root bin daemon sys adm disk wheel floppy dialout tape video", - "1000": "root", - "guest": "users", - "nobody": "nobody", - } - for userStr, expected := range testCases { - userStr := userStr - expected := expected - t.Run(userStr, func(t *testing.T) { - t.Parallel() - cmd := []string{"run", "--rm"} - if userStr != "" { - cmd = append(cmd, "--user", userStr) - } - cmd = append(cmd, testutil.AlpineImage, "id", "-nG") - base.Cmd(cmd...).AssertOutContains(expected) - }) + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "Test container run as default user (root) and verify root belongs to standard system groups", + Command: test.Command("run", "--rm", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("root bin daemon sys adm disk wheel floppy dialout tape video")), + }, + { + Description: "Test container run with numeric UID (1000) and verify it resolves to root group inside the container", + Command: test.Command("run", "--rm", "--user", "1000", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("root")), + }, + { + Description: "Test container run as user (guest) and verify group membership is resolved correctly", + Command: test.Command("run", "--rm", "--user", "guest", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("users")), + }, + { + Description: "Test container run with well-known user 'nobody' and verify it belongs to the 'nobody' group", + Command: test.Command("run", "--rm", "--user", "nobody", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nobody")), + }, } + testCase.Run(t) } func TestRunUmask(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testutil.DockerIncompatible(t) - base.Cmd("run", "--rm", "--umask", "0200", testutil.AlpineImage, "sh", "-c", "umask").AssertOutContains("0200") + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = test.Command("run", "--rm", "--umask", "0200", testutil.AlpineImage, "sh", "-c", "umask") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("0200")) + testCase.Run(t) } func TestRunAddGroup(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testCases := []struct { - user string - groups []string - expected string - }{ + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ { - user: "", - groups: []string{}, - expected: "root bin daemon sys adm disk wheel floppy dialout tape video", + Description: "Test container run as default root user and its inherited system groups", + Command: test.Command("run", "--rm", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root bin daemon sys adm disk wheel floppy dialout tape video\n")), }, { - user: "1000", - groups: []string{}, - expected: "root", + Description: "Test container run as numeric UID only and its fallback to root group", + Command: test.Command("run", "--rm", "--user", "1000", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root\n")), }, { - user: "1000", - groups: []string{"nogroup"}, - expected: "root nogroup", + Description: "Test container run as numeric UID with extra group addition", + Command: test.Command("run", "--rm", "--user", "1000", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root nogroup\n")), }, { - user: "1000:wheel", - groups: []string{"nogroup"}, - expected: "wheel nogroup", + Description: "Test container run as UID:GID pair with extra group addition", + Command: test.Command("run", "--rm", "--user", "1000:wheel", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("wheel nogroup\n")), }, { - user: "root", - groups: []string{"nogroup"}, - expected: "root bin daemon sys adm disk wheel floppy dialout tape video nogroup", + Description: "Test container run as root with extra group addition and system group persistence", + Command: test.Command("run", "--rm", "--user", "root", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("root bin daemon sys adm disk wheel floppy dialout tape video nogroup\n")), }, { - user: "root:nogroup", - groups: []string{"nogroup"}, - expected: "nogroup", + Description: "Test container run as root:group override and its effect on supplementary groups", + Command: test.Command("run", "--rm", "--user", "root:nogroup", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nogroup\n")), }, { - user: "guest", - groups: []string{"root", "nogroup"}, - expected: "users root nogroup", + Description: "Test container run as named non-root user with multiple group additions", + Command: test.Command("run", "--rm", "--user", "guest", "--group-add", "root", "--group-add", "nogroup", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("users root nogroup\n")), }, { - user: "guest:nogroup", - groups: []string{"0"}, - expected: "nogroup root", + Description: "Test container run as named user:group with numeric GID resolution", + Command: test.Command("run", "--rm", "--user", "guest:nogroup", "--group-add", "0", testutil.AlpineImage, "id", "-nG"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nogroup root\n")), }, } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.user, func(t *testing.T) { - t.Parallel() - cmd := []string{"run", "--rm"} - if testCase.user != "" { - cmd = append(cmd, "--user", testCase.user) - } - for _, group := range testCase.groups { - cmd = append(cmd, "--group-add", group) - } - cmd = append(cmd, testutil.AlpineImage, "id", "-nG") - base.Cmd(cmd...).AssertOutExactly(testCase.expected + "\n") - }) - } + testCase.Run(t) } // TestRunAddGroup_CVE_2023_25173 tests https://github.com/advisories/GHSA-hmfx-3pcx-653p // // Equates to https://github.com/containerd/containerd/commit/286a01f350a2298b4fdd7e2a0b31c04db3937ea8 func TestRunAddGroup_CVE_2023_25173(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testCases := []struct { - user string - groups []string - expected string - }{ + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) + } + testCase.SubTests = []*test.Case{ { - user: "", - groups: nil, - expected: "groups=0(root),10(wheel)", + Description: "Test container run as default root user", + Command: test.Command("run", "--rm", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),10(wheel)\n")), }, { - user: "", - groups: []string{"1", "1234"}, - expected: "groups=0(root),1(daemon),10(wheel),1234", + Description: "Test container run as root with additional groups", + Command: test.Command("run", "--rm", "--group-add", "1", "--group-add", "1234", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),1(daemon),10(wheel),1234\n")), }, { - user: "1234", - groups: nil, - expected: "groups=0(root)", + Description: "Test container run as custom UID with inherited root group", + Command: test.Command("run", "--rm", "--user", "1234", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root)\n")), }, { - user: "1234:1234", - groups: nil, - expected: "groups=1234", + Description: "Test container run as custom UID and GID pair", + Command: test.Command("run", "--rm", "--user", "1234:1234", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1234\n")), }, { - user: "1234", - groups: []string{"1234"}, - expected: "groups=0(root),1234", + Description: "Test container run as custom UID with explicit group add", + Command: test.Command("run", "--rm", "--user", "1234", "--group-add", "1234", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=0(root),1234\n")), }, { - user: "daemon", - groups: nil, - expected: "groups=1(daemon)", + Description: "Test container run as named non-root user (daemon)", + Command: test.Command("run", "--rm", "--user", "daemon", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1(daemon)\n")), }, { - user: "daemon", - groups: []string{"1234"}, - expected: "groups=1(daemon),1234", + Description: "Test container run as named user with extra groups", + Command: test.Command("run", "--rm", "--user", "daemon", "--group-add", "1234", testutil.BusyboxImage, "id"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("groups=1(daemon),1234\n")), }, } - - base.Cmd("pull", "--quiet", testutil.BusyboxImage).AssertOK() - for _, testCase := range testCases { - cmd := []string{"run", "--rm"} - if testCase.user != "" { - cmd = append(cmd, "--user", testCase.user) - } - for _, group := range testCase.groups { - cmd = append(cmd, "--group-add", group) - } - cmd = append(cmd, testutil.BusyboxImage, "id") - base.Cmd(cmd...).AssertOutContains(testCase.expected + "\n") - } + testCase.Run(t) } func TestUsernsMappingRunCmd(t *testing.T) { From 4129b8084a57ee9aed750e8a0d595f70fc58fd5c Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Sat, 31 Jan 2026 09:31:01 +0900 Subject: [PATCH 411/868] test: refactor container_run_windows_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_run_windows_test.go | 145 +++++++++++------- 1 file changed, 87 insertions(+), 58 deletions(-) diff --git a/cmd/nerdctl/container/container_run_windows_test.go b/cmd/nerdctl/container/container_run_windows_test.go index 6c054292ec2..e4430cded8b 100644 --- a/cmd/nerdctl/container/container_run_windows_test.go +++ b/cmd/nerdctl/container/container_run_windows_test.go @@ -19,12 +19,12 @@ package container import ( "bytes" "os/exec" - "strings" "testing" "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -32,94 +32,123 @@ import ( ) func TestRunHostProcessContainer(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - hostname, err := exec.Command("hostname").Output() - if err != nil { - t.Fatalf("unable to get hostname: %s", err) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostname, err := exec.Command("hostname").Output() + if err != nil { + t.Fatalf("unable to get hostname: %s", err) + } + data.Labels().Set("hostname", string(bytes.TrimSpace(hostname))) + + whoami := helpers.Capture("run", "--rm", "--isolation=host", testutil.WindowsNano, "whoami") + t.Logf("whoami %s", whoami) } - hostname = bytes.TrimSpace(hostname) - base.Cmd("run", "--rm", "--isolation=host", testutil.WindowsNano, "hostname").AssertOutContains(string(hostname)) - output := base.Cmd("run", "--rm", "--isolation=host", testutil.WindowsNano, "whoami").Out() - t.Logf("whoami %s", output) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--isolation=host", testutil.WindowsNano, "hostname") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(data.Labels().Get("hostname")))(data, helpers) + } + testCase.Run(t) } func TestRunHostProcessContainerAsUser(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - hostuser := "nt authority\\system" - base.Cmd("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\SYSTEM", testutil.WindowsNano, "whoami").AssertOutContains(hostuser) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\SYSTEM", testutil.WindowsNano, "whoami") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\system")) + testCase.Run(t) } -func TestRunHostProcessContainerAsService(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - hostuser := "nt authority\\local service" - base.Cmd("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Local Service", testutil.WindowsNano, "whoami").AssertOutContains(hostuser) +func TestRunHostProcessContainerAsLocalService(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Local Service", testutil.WindowsNano, "whoami") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\local service")) + testCase.Run(t) } -func TestRunHostProcessContainerAslocalService(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - hostuser := "nt authority\\network service" - base.Cmd("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Network Service", testutil.WindowsNano, "whoami").AssertOutContains(hostuser) +func TestRunHostProcessContainerAsNetworkService(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = test.Command("run", "--rm", "--isolation=host", "-u", "NT AUTHORITY\\Network Service", testutil.WindowsNano, "whoami") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nt authority\\network service")) + testCase.Run(t) } func TestRunProcessIsolated(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - containerUser := "ContainerUser" - base.Cmd("run", "--rm", "--isolation=process", "-u", containerUser, testutil.WindowsNano, "whoami").AssertOutContains(containerUser) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containeruser", "ContainerUser") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--isolation=process", "-u", data.Labels().Get("containeruser"), testutil.WindowsNano, "whoami") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(data.Labels().Get("containeruser")))(data, helpers) + } + testCase.Run(t) } func TestRunHyperVContainer(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - if !testutil.HyperVSupported() { - t.Skip("HyperV is not enabled, skipping test") + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.HyperV, + ) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // hyperv must not be in the name for this test, the output is parsed for it + containerName := "nerdctl-testwcowcontainer" + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "--isolation", "hyperv", "--name", containerName, testutil.WindowsNano) } - - // hyperv must not be in the name for this test, the output is parsed for it - containerName := "nerdctl-testwcowcontainer" - base.Cmd("run", "--isolation", "hyperv", "--name", containerName, testutil.WindowsNano).Out() - defer base.Cmd("rm", "-f", containerName).AssertOK() - inspectOutput := base.Cmd("container", "inspect", "--mode", "native", containerName).Out() - - assert.Assert(t, strings.Contains(inspectOutput, "hyperv")) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "inspect", "--mode", "native", data.Labels().Get("containerName")) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hyperv"))(data, helpers) + } + testCase.Run(t) } func TestRunProcessContainer(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - base.Cmd("run", "--isolation", "process", "--name", containerName, testutil.WindowsNano).Out() - defer base.Cmd("rm", "-f", containerName).AssertOK() - inspectOutput := base.Cmd("container", "inspect", "--mode", "native", containerName).Out() - t.Log(inspectOutput) - - assert.Assert(t, !strings.Contains(inspectOutput, "hyperv")) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--isolation", "process", "--name", data.Identifier(), testutil.WindowsNano) + } + 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("container", "inspect", "--mode", "native", data.Identifier()) + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("hyperv")) + testCase.Run(t) } // Note that the current implementation of this test is not ideal, since it relies on internal HCS details that // Microsoft could decide to change in the future (breaking both this unit test and the one in containerd itself): // https://github.com/containerd/containerd/pull/6618#discussion_r823302852 func TestRunProcessContainerWithDevice(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - base.Cmd( + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = test.Command( "run", "--rm", "--isolation=process", "--device", "class://5B45201D-F2F2-4F3B-85BB-30FF1F953599", testutil.WindowsNano, "cmd", "/S", "/C", "dir C:\\Windows\\System32\\HostDriverStore", - ).AssertOutContains("FileRepository") + ) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("FileRepository")) + testCase.Run(t) } func TestRunWithTtyAndDetached(t *testing.T) { From da99d3561f71381c195afd858cd4890c19fcf8dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:12:13 +0000 Subject: [PATCH 412/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.1.5+incompatible to 29.2.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.1.5...v29.2.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.2.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f6c63275af..2f3b3cbd75d 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.1.5+incompatible //gomodjail:unconfined + github.com/docker/cli v29.2.0+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index d2f130e6cd8..3bd2a70fb5d 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao= -github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= +github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 9599f349f9717f2f9fee8c9b343db2cec3b527dd Mon Sep 17 00:00:00 2001 From: Nivesh Dandyan Date: Tue, 3 Feb 2026 11:47:37 +0000 Subject: [PATCH 413/868] fix: correct usage display to show [command] instead of [flags] When running nerdctl without any command, the usage text incorrectly showed "Usage: nerdctl [flags]" instead of "Usage: nerdctl [command]". This happened because the usage function checked c.Runnable() to decide the format, but the root command has RunE set (for handling unknown subcommands), making it "runnable" even though it semantically requires a subcommand. The fix prioritizes checking HasSubCommands() first, which correctly displays "[command]" for commands that have subcommands, regardless of whether they also have a Run function defined. Fixes #3185 Signed-off-by: Nivesh Dandyan --- cmd/nerdctl/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index f8ab56799bd..1fb96f47cc3 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -64,7 +64,9 @@ var ( // usage was derived from https://github.com/spf13/cobra/blob/v1.2.1/command.go#L491-L514 func usage(c *cobra.Command) error { s := "Usage: " - if c.Runnable() { + if c.HasSubCommands() { + s += c.CommandPath() + " [command]\n" + } else if c.Runnable() { s += c.UseLine() + "\n" } else { s += c.CommandPath() + " [command]\n" From 9838db9c1d34133245a0267ac1cebf5cdb929a5e Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Tue, 3 Feb 2026 23:40:13 +0900 Subject: [PATCH 414/868] test: refactor container_remove_windows_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container_remove_windows_test.go | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/container/container_remove_windows_test.go b/cmd/nerdctl/container/container_remove_windows_test.go index c6733ca1e9b..91d9f786337 100644 --- a/cmd/nerdctl/container/container_remove_windows_test.go +++ b/cmd/nerdctl/container/container_remove_windows_test.go @@ -21,34 +21,53 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRemoveHyperVContainer(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) + testCase := nerdtest.Setup() - if !testutil.HyperVSupported() { - t.Skip("HyperV is not enabled, skipping test") - } + testCase.Require = nerdtest.HyperV - // ignore error - base.Cmd("rm", tID, "-f").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--isolation", "hyperv", "--name", testutil.Identifier(t), testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) - base.Cmd("run", "-d", "--isolation", "hyperv", "--name", tID, testutil.NginxAlpineImage).AssertOK() - defer base.Cmd("rm", tID, "-f").AssertOK() + inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) + //check with HCS if the container is ineed a VM + isHypervContainer, err := testutil.HyperVContainer(inspect) + assert.NilError(t, err) + assert.Assert(t, isHypervContainer, true) + } - base.EnsureContainerStarted(tID) - inspect := base.InspectContainer(tID) - //check with HCS if the container is ineed a VM - isHypervContainer, err := testutil.HyperVContainer(inspect) - if err != nil { - t.Fatalf("unable to list HCS containers: %s", err) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", testutil.Identifier(t)) } - assert.Assert(t, isHypervContainer, true) - base.Cmd("rm", tID).AssertFail() + testCase.SubTests = []*test.Case{ + { + Description: "should fail to remove when still running", + NoParallel: true, + Command: test.Command("rm", testutil.Identifier(t)), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "should kill the container", + NoParallel: true, + Command: test.Command("kill", testutil.Identifier(t)), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "should remove the container when terminated", + NoParallel: true, + Command: test.Command("rm", testutil.Identifier(t)), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } - base.Cmd("kill", tID).AssertOK() - base.Cmd("rm", tID).AssertOK() + testCase.Run(t) } From 2fdd3d5feb429c49cc05a1eb160591869928b8a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:32:40 +0000 Subject: [PATCH 415/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.2.0+incompatible to 29.2.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.2.0...v29.2.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.2.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2f3b3cbd75d..134b7b8f24d 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.2.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.2.1+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 3bd2a70fb5d..a8f9af9ee5e 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,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.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= -github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 2200818c1d5126a08d5cd5f95fda6e0e6dd84b03 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Thu, 5 Feb 2026 23:45:46 +0900 Subject: [PATCH 416/868] test: refactor container_cp_acid_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_cp_acid_linux_test.go | 295 ++++++++++-------- 1 file changed, 163 insertions(+), 132 deletions(-) diff --git a/cmd/nerdctl/container/container_cp_acid_linux_test.go b/cmd/nerdctl/container/container_cp_acid_linux_test.go index c5a92ee70bd..2b1c22d5ecc 100644 --- a/cmd/nerdctl/container/container_cp_acid_linux_test.go +++ b/cmd/nerdctl/container/container_cp_acid_linux_test.go @@ -17,16 +17,17 @@ package container import ( + "errors" "fmt" "os" "path/filepath" "testing" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" + + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/containerutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -35,14 +36,11 @@ import ( // because of their complexity func TestCopyAcid(t *testing.T) { - t.Parallel() + testCase := nerdtest.Setup() - t.Run("Travelling along volumes w/o read-only", func(t *testing.T) { - t.Parallel() - testID := testutil.Identifier(t) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testID := data.Identifier() tempDir := t.TempDir() - base := testutil.NewBase(t) - base.Dir = tempDir sourceFile := filepath.Join(tempDir, "hostfile") sourceFileContent := []byte(testID) @@ -50,133 +48,166 @@ func TestCopyAcid(t *testing.T) { roContainer := testID + "-ro" rwContainer := testID + "-rw" - setup := func() { - base.Cmd("volume", "create", testID+"-1-ro").AssertOK() - base.Cmd("volume", "create", testID+"-2-rw").AssertOK() - base.Cmd("volume", "create", testID+"-3-rw").AssertOK() - base.Cmd("run", "-d", "-w", containerCwd, "--name", roContainer, "--read-only", - "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), - "-v", fmt.Sprintf("%s:%s", testID+"-2-rw", "/vol2/dir2/rw"), - testutil.CommonImage, "sleep", "Inf", - ).AssertOK() - base.Cmd("run", "-d", "-w", containerCwd, "--name", rwContainer, - "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), - "-v", fmt.Sprintf("%s:%s", testID+"-3-rw", "/vol3/dir3/rw"), - testutil.CommonImage, "sleep", "Inf", - ).AssertOK() - - base.Cmd("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s ../../../ relativelinktoroot").AssertOK() - base.Cmd("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s / absolutelinktoroot").AssertOK() - base.Cmd("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s ../../../ relativelinktoroot").AssertOK() - base.Cmd("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s / absolutelinktoroot").AssertOK() - // Create file on the host - err := os.WriteFile(sourceFile, sourceFileContent, filePerm) - assert.NilError(t, err) - } - - tearDown := func() { - base.Cmd("rm", "-f", roContainer).Run() - base.Cmd("rm", "-f", rwContainer).Run() - base.Cmd("volume", "rm", testID+"-1-ro").Run() - base.Cmd("volume", "rm", testID+"-2-rw").Run() - base.Cmd("volume", "rm", testID+"-3-rw").Run() - } - - t.Cleanup(tearDown) - tearDown() - - setup() + data.Labels().Set("sourceFile", sourceFile) + data.Labels().Set("sourceFileContent", string(sourceFileContent)) + data.Labels().Set("roContainer", roContainer) + data.Labels().Set("rwContainer", rwContainer) + + helpers.Ensure("volume", "create", testID+"-1-ro") + helpers.Ensure("volume", "create", testID+"-2-ro") + helpers.Ensure("volume", "create", testID+"-3-ro") + + helpers.Ensure("run", "-d", "-w", containerCwd, "--name", roContainer, "--read-only", + "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), + "-v", fmt.Sprintf("%s:%s", testID+"-2-rw", "/vol2/dir2/rw"), + testutil.CommonImage, "sleep", nerdtest.Infinity, + ) + nerdtest.EnsureContainerStarted(helpers, roContainer) + + helpers.Ensure("run", "-d", "-w", containerCwd, "--name", rwContainer, + "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"), + "-v", fmt.Sprintf("%s:%s", testID+"-3-rw", "/vol3/dir3/rw"), + testutil.CommonImage, "sleep", nerdtest.Infinity, + ) + nerdtest.EnsureContainerStarted(helpers, rwContainer) + + helpers.Ensure("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s ../../../ relativelinktoroot") + helpers.Ensure("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s / absolutelinktoroot") + helpers.Ensure("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s ../../../ relativelinktoroot") + helpers.Ensure("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s / absolutelinktoroot") + + // Create file on the host + err := os.WriteFile(sourceFile, sourceFileContent, filePerm) + assert.NilError(t, err) expectedErr := containerutil.ErrTargetIsReadOnly.Error() if nerdtest.IsDocker() { expectedErr = "" } - - t.Run("Cannot copy into a read-only root", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, roContainer+":/").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Cannot copy into a read-only mount, in a rw container", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, rwContainer+":/vol1/dir1/ro").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Can copy into a read-write mount in a read-only container", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw").Assert(icmd.Expected{ - ExitCode: 0, - }) - }) - - t.Run("Traverse read-only locations to a read-write location", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, roContainer+":/vol1/dir1/ro/../../../vol2/dir2/rw").Assert(icmd.Expected{ - ExitCode: 0, - }) - }) - - t.Run("Follow an absolute symlink inside a read-write mount to a read-only root", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw/absolutelinktoroot").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Follow am absolute symlink inside a read-write mount to a read-only mount", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, rwContainer+":/vol3/dir3/rw/absolutelinktoroot/vol1/dir1/ro").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Follow a relative symlink inside a read-write location to a read-only root", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw/relativelinktoroot").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Follow a relative symlink inside a read-write location to a read-only mount", func(t *testing.T) { - t.Parallel() - - base.Cmd("cp", sourceFile, rwContainer+":/vol3/dir3/rw/relativelinktoroot/vol1/dir1/ro").Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - t.Run("Cannot copy into a HOST read-only location", func(t *testing.T) { - t.Parallel() - - // Root will just ignore the 000 permission on the host directory. - if !rootlessutil.IsRootless() { - t.Skip("This test does not work rootful") - } - - err := os.MkdirAll(filepath.Join(tempDir, "rotest"), 0o000) - assert.NilError(t, err) - base.Cmd("cp", roContainer+":/etc/issue", filepath.Join(tempDir, "rotest")).Assert(icmd.Expected{ - ExitCode: 1, - Err: expectedErr, - }) - }) - - }) + data.Labels().Set("expectedErr", expectedErr) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testID := data.Identifier() + helpers.Anyhow("rm", "-f", testID+"-ro") + helpers.Anyhow("rm", "-f", testID+"-rw") + helpers.Anyhow("volume", "rm", testID+"-1-ro") + helpers.Anyhow("volume", "rm", testID+"-2-rw") + helpers.Anyhow("volume", "rm", testID+"-3-rw") + } + + testCase.SubTests = []*test.Case{ + { + Description: "Cannot copy into a read-only root", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Cannot copy into a read-only mount, in a rw container", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol1/dir1/ro") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Can copy into a read-write mount in a read-only container", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + { + Description: "Traverse read-only locations to a read-write location", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol1/dir1/ro/../../../vol2/dir2/rw") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + { + Description: "Follow an absolute symlink inside a read-write mount to a read-only root", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw/absolutelinktoroot") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Follow am absolute symlink inside a read-write mount to a read-only mount", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol3/dir3/rw/absolutelinktoroot/vol1/dir1/ro") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Follow a relative symlink inside a read-write location to a read-only root", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("roContainer")+":/vol2/dir2/rw/relativelinktoroot") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Follow a relative symlink inside a read-write location to a read-only mount", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("cp", data.Labels().Get("sourceFile"), data.Labels().Get("rwContainer")+":/vol3/dir3/rw/relativelinktoroot/vol1/dir1/ro") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + { + Description: "Cannot copy into a HOST read-only location", + Require: nerdtest.Rootless, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + tempDir := t.TempDir() + err := os.MkdirAll(filepath.Join(tempDir, "rotest"), 0o000) + assert.NilError(t, err) + return helpers.Command("cp", data.Labels().Get("roContainer")+":/etc/issue", filepath.Join(tempDir, "rotest")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Labels().Get("expectedErr"))}, + } + }, + }, + } + + testCase.Run(t) } From 4649baaef797cf6214ce44893c1535642ccb3504 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Fri, 6 Feb 2026 15:10:54 +0000 Subject: [PATCH 417/868] Detect vendor before crafting cdiDeviceIDs for --gpus This detects the GPU vendor from the CDI spec files while generating devicesIDs corresponding to the values passed to --gpus option. With this, the users can also use AMD gpus if a corresponding CDI spec is present. Signed-off-by: Shiv Tyagi --- .../container/container_run_linux_test.go | 173 +++++++++++++++++- docs/gpu.md | 34 ++-- pkg/cmd/container/create.go | 9 +- pkg/cmd/container/run_cdi.go | 63 ++++++- pkg/cmd/container/run_gpus.go | 44 +++++ pkg/cmd/container/run_linux.go | 39 ---- 6 files changed, 295 insertions(+), 67 deletions(-) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index 4210be47ece..0188a7be7f9 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -674,7 +674,16 @@ func TestRunDeviceCDI(t *testing.T) { // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) cdiSpecDir := filepath.Join(t.TempDir(), "cdi") - writeTestCDISpec(t, cdiSpecDir) + const testCDIVendor1 = ` +cdiVersion: "0.3.0" +kind: "vendor1.com/device" +devices: +- name: foo + containerEdits: + env: + - FOO=injected +` + writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) base := testutil.NewBase(t) base.Cmd("--cdi-spec-dirs", cdiSpecDir, "run", @@ -689,7 +698,16 @@ func TestRunDeviceCDIWithNerdctlConfig(t *testing.T) { // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. testutil.DockerIncompatible(t) cdiSpecDir := filepath.Join(t.TempDir(), "cdi") - writeTestCDISpec(t, cdiSpecDir) + const testCDIVendor1 = ` +cdiVersion: "0.3.0" +kind: "vendor1.com/device" +devices: +- name: foo + containerEdits: + env: + - FOO=injected +` + writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") err := os.WriteFile(tomlPath, []byte(fmt.Sprintf(` @@ -706,8 +724,128 @@ cdi_spec_dirs = ["%s"] ).AssertOutContains("FOO=injected") } -func writeTestCDISpec(t *testing.T, cdiSpecDir string) { - const testCDIVendor1 = ` +// TestRunGPU tests GPU injection using the --gpus flag. +func TestRunGPU(t *testing.T) { + t.Parallel() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testutil.DockerIncompatible(t) + const nvidiaSpec = ` +cdiVersion: "0.5.0" +kind: "nvidia.com/gpu" +devices: +- name: "0" + containerEdits: + env: + - NVIDIA_GPU_0=injected +- name: "1" + containerEdits: + env: + - NVIDIA_GPU_1=injected +` + const amdSpec = ` +cdiVersion: "0.5.0" +kind: "amd.com/gpu" +devices: +- name: "0" + containerEdits: + env: + - AMD_GPU_0=injected +- name: "1" + containerEdits: + env: + - AMD_GPU_1=injected +` + const unknownSpec = ` +cdiVersion: "0.5.0" +kind: "unknown.com/gpu" +devices: +- name: "0" + containerEdits: + env: + - UNKNOWN_GPU_0=injected +` + + testCases := []struct { + name string + specs map[string]string + gpuFlags []string + expectedEnvs []string + expectFail bool + }{ + { + name: "nvidia device injection", + specs: map[string]string{"nvidia.yaml": nvidiaSpec}, + gpuFlags: []string{"--gpus", "2"}, + expectedEnvs: []string{"NVIDIA_GPU_0=injected", "NVIDIA_GPU_1=injected"}, + }, + { + name: "amd device injection", + specs: map[string]string{"amd.yaml": amdSpec}, + gpuFlags: []string{"--gpus", "2"}, + expectedEnvs: []string{"AMD_GPU_0=injected", "AMD_GPU_1=injected"}, + }, + { + name: "multiple vendors", + specs: map[string]string{"nvidia.yaml": nvidiaSpec, "amd.yaml": amdSpec}, + gpuFlags: []string{"--gpus", "1"}, + expectedEnvs: []string{"NVIDIA_GPU_0=injected"}, + }, + { + name: "unknown vendor fails", + specs: map[string]string{"unknown.yaml": unknownSpec}, + gpuFlags: []string{"--gpus", "1"}, + expectFail: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + for fileName, spec := range tc.specs { + writeTestCDISpec(t, spec, fileName, tmpDir) + } + + base := testutil.NewBase(t) + args := []string{"--cdi-spec-dirs", tmpDir, "run", "--rm"} + args = append(args, tc.gpuFlags...) + args = append(args, testutil.AlpineImage, "env") + + if tc.expectFail { + base.Cmd(args...).AssertFail() + } else { + base.Cmd(args...).AssertOutWithFunc(func(stdout string) error { + for _, expectedEnv := range tc.expectedEnvs { + if !strings.Contains(stdout, expectedEnv) { + return fmt.Errorf("%s not found", expectedEnv) + } + } + return nil + }) + } + }) + } +} + +// TestRunGPUWithOtherCDIDevices tests GPU CDI injection along with other CDI devices. +func TestRunGPUWithOtherCDIDevices(t *testing.T) { + t.Parallel() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testutil.DockerIncompatible(t) + const amdSpec = ` +cdiVersion: "0.5.0" +kind: "amd.com/gpu" +devices: +- name: "0" + containerEdits: + env: + - AMD_GPU_0=injected +- name: "1" + containerEdits: + env: + - AMD_GPU_1=injected +` + const vendor1Spec = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" devices: @@ -716,10 +854,33 @@ devices: env: - FOO=injected ` + tmpDir := t.TempDir() + writeTestCDISpec(t, amdSpec, "amd.yaml", tmpDir) + writeTestCDISpec(t, vendor1Spec, "vendor1.yaml", tmpDir) + + base := testutil.NewBase(t) + base.Cmd("--cdi-spec-dirs", tmpDir, "run", "--rm", + "--gpus", "2", + "--device", "vendor1.com/device=foo", + testutil.AlpineImage, "env", + ).AssertOutWithFunc(func(stdout string) error { + if !strings.Contains(stdout, "AMD_GPU_0=injected") { + return errors.New("AMD_GPU_0=injected not found") + } + if !strings.Contains(stdout, "AMD_GPU_1=injected") { + return errors.New("AMD_GPU_1=injected not found") + } + if !strings.Contains(stdout, "FOO=injected") { + return errors.New("FOO=injected not found") + } + return nil + }) +} +func writeTestCDISpec(t *testing.T, spec string, fileName string, cdiSpecDir string) { err := os.MkdirAll(cdiSpecDir, 0700) assert.NilError(t, err) - cdiSpecPath := filepath.Join(cdiSpecDir, "vendor1.yaml") - err = os.WriteFile(cdiSpecPath, []byte(testCDIVendor1), 0400) + cdiSpecPath := filepath.Join(cdiSpecDir, fileName) + err = os.WriteFile(cdiSpecPath, []byte(spec), 0400) assert.NilError(t, err) } diff --git a/docs/gpu.md b/docs/gpu.md index cc21247a238..536e7c50cd3 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -7,32 +7,40 @@ > The description in this section applies to nerdctl v2.3 or later. > Users of prior releases of nerdctl should refer to -nerdctl provides docker-compatible NVIDIA GPU support. +nerdctl provides docker-compatible NVIDIA and AMD GPU support. ## Prerequisites -- NVIDIA Drivers - - Same requirement as when you use GPUs on Docker. For details, please refer to [the doc by NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#pre-requisites). -- The NVIDIA Container Toolkit - - containerd relies on the NVIDIA Container Toolkit to make GPUs usable inside a container. You can install the NVIDIA Container Toolkit by following the [official installation instructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). +- GPU Drivers + - Same requirement as when you use GPUs on Docker. For details, please refer to these docs by [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#pre-requisites) and [AMD](https://instinct.docs.amd.com/projects/container-toolkit/en/latest/container-runtime/quick-start-guide.html#step-2-install-the-amdgpu-driver). +- Container Toolkit + - containerd relies on vendor Container Toolkits to make GPUs available to the containers. You can install those by following the official installation instructions from [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and [AMD](https://instinct.docs.amd.com/projects/container-toolkit/en/latest/container-runtime/quick-start-guide.html). +- CDI Specification + - Container Device Interface (CDI) specification for the GPU devices is required for the GPU support to work. Follow the official documentation from [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html) and [AMD](https://instinct.docs.amd.com/projects/container-toolkit/en/latest/container-runtime/cdi-guide.html) to ensure that the required CDI specifications are present on the system. ## Options for `nerdctl run --gpus` `nerdctl run --gpus` is compatible to [`docker run --gpus`](https://docs.docker.com/engine/reference/commandline/run/#access-an-nvidia-gpu). You can specify number of GPUs to use via `--gpus` option. -The following example exposes all available GPUs. +The following examples expose all available GPUs to the container. ``` nerdctl run -it --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi ``` +or + +``` +nerdctl run -it --rm --gpus=all rocm/rocm-terminal rocm-smi +``` + You can also pass detailed configuration to `--gpus` option as a list of key-value pairs. The following options are provided. - `count`: number of GPUs to use. `all` exposes all available GPUs. -- `device`: IDs of GPUs to use. UUID or numbers of GPUs can be specified. +- `device`: IDs of GPUs to use. UUID or numbers of GPUs can be specified. This only works for NVIDIA GPUs. -The following example exposes a specific GPU to the container. +The following example exposes a specific NVIDIA GPU to the container. ``` nerdctl run -it --rm --gpus 'device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi @@ -72,9 +80,9 @@ services: ### `nerdctl run --gpus` fails due to an unresolvable CDI device -If the required CDI specifications for NVIDIA devices are not available on the +If the required CDI specifications for your GPU devices are not available on the system, the `nerdctl run` command will fail with an error similar to: `CDI device injection failed: unresolvable CDI devices nvidia.com/gpu=all` (the -exact error message will depend on the device(s) requested). +exact error message will depend on the vendor and the device(s) requested). This should be the same error message that is reported when the `--device` flag is used to request a CDI device: @@ -82,7 +90,7 @@ is used to request a CDI device: nerdctl run --device=nvidia.com/gpu=all ``` -Ensure that the NVIDIA Container Toolkit (>= v1.18.0 is recommended) is installed and the requested CDI devices are present in the ouptut of `nvidia-ctk cdi list`: +Ensure that the NVIDIA (or AMD) Container Toolkit is installed and the requested CDI devices are present in the ouptut of `nvidia-ctk cdi list` (or `amd-ctk cdi list` for AMD GPUs): ``` $ nvidia-ctk cdi list @@ -92,7 +100,9 @@ nvidia.com/gpu=GPU-3eb87630-93d5-b2b6-b8ff-9b359caf4ee2 nvidia.com/gpu=all ``` -See the NVIDIA Container Toolkit [CDI documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html) for more information. +For NVIDIA Container Toolkit, version >= v1.18.0 is recommended. See the NVIDIA Container Toolkit [CDI documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html) for more information. + +For AMD Container Toolkit, version >= v1.2.0 is recommended. See the AMD Container Toolkit [CDI documentation](https://instinct.docs.amd.com/projects/container-toolkit/en/latest/container-runtime/cdi-guide.html) for more information. ### `nerdctl run --gpus` fails when using the Nvidia gpu-operator diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 064ad89395e..7ebc3b702d6 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -129,7 +129,14 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } opts = append(opts, platformOpts...) - opts = append(opts, withCDIDevices(options.GOptions.CDISpecDirs, options.CDIDevices...)) + if len(options.CDIDevices) > 0 || len(options.GPUs) > 0 { + opts = append(opts, withStaticCDIRegistry(options.GOptions.CDISpecDirs)) + } + + opts = append(opts, + withGPUs(options.GPUs...), + withCDIDevices(options.CDIDevices...), + ) if _, err := referenceutil.Parse(args[0]); errors.Is(err, referenceutil.ErrLoadOCIArchiveRequired) { imageRef := args[0] diff --git a/pkg/cmd/container/run_cdi.go b/pkg/cmd/container/run_cdi.go index da49ffb9925..1583d722304 100644 --- a/pkg/cmd/container/run_cdi.go +++ b/pkg/cmd/container/run_cdi.go @@ -24,23 +24,68 @@ import ( "github.com/containerd/containerd/v2/core/containers" cdispec "github.com/containerd/containerd/v2/pkg/cdi" "github.com/containerd/containerd/v2/pkg/oci" + "github.com/containerd/log" ) -// withCDIDevices creates the OCI runtime spec options for injecting CDI devices. -// Two options are returned: The first ensures that the CDI registry is initialized with -// refresh disabled, and the second injects the devices into the container. -func withCDIDevices(cdiSpecDirs []string, devices ...string) oci.SpecOpts { - return func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { - if len(devices) == 0 { - return nil +// detectGPUVendorFromCDI detects the first available GPU vendor from CDI cache. +// Returns empty string if no known vendor is found. +func detectGPUVendorFromCDI() string { + cache := cdi.GetDefaultCache() + availableVendors := cache.ListVendors() + knownGPUVendors := []string{"nvidia.com", "amd.com"} + for _, known := range knownGPUVendors { + for _, available := range availableVendors { + if known == available { + return known + } } + } - // We configure the CDI registry with the configured spec dirs and disable refresh. - cdi.Configure( + return "" +} + +// withStaticCDIRegistry inits the CDI registry with given spec dirs +// and disables auto-refresh. +func withStaticCDIRegistry(cdiSpecDirs []string) oci.SpecOpts { + return func(ctx context.Context, _ oci.Client, _ *containers.Container, _ *oci.Spec) error { + _ = cdi.Configure( cdi.WithSpecDirs(cdiSpecDirs...), cdi.WithAutoRefresh(false), ) + if err := cdi.Refresh(); err != nil { + // We don't consider registry refresh failure a fatal error. + // For instance, a dynamically generated invalid CDI Spec file for + // any particular vendor shouldn't prevent injection of devices of + // different vendors. CDI itself knows better and it will fail the + // injection if necessary. + log.L.Warnf("CDI cache refresh failed: %v", err) + } + return nil + } +} +// withCDIDevices creates the OCI runtime spec options for injecting CDI devices. +func withCDIDevices(devices ...string) oci.SpecOpts { + return func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { + if len(devices) == 0 { + return nil + } return cdispec.WithCDIDevices(devices...)(ctx, client, c, s) } } + +// withGPUs creates the OCI runtime spec options for injecting GPUs via CDI. +// It parses the given GPU options and converts them to CDI device IDs. +// withCDIDevices is then used to perform the actual injection. +func withGPUs(gpuOpts ...string) oci.SpecOpts { + return func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { + if len(gpuOpts) == 0 { + return nil + } + cdiDevices, err := parseGPUOpts(gpuOpts) + if err != nil { + return err + } + return withCDIDevices(cdiDevices...)(ctx, client, c, s) + } +} diff --git a/pkg/cmd/container/run_gpus.go b/pkg/cmd/container/run_gpus.go index 967858b6bf5..4287f1b8e73 100644 --- a/pkg/cmd/container/run_gpus.go +++ b/pkg/cmd/container/run_gpus.go @@ -31,6 +31,29 @@ type GPUReq struct { Capabilities []string } +func (req *GPUReq) toCDIDeviceIDs(vendor string) []string { + var cdiDeviceIDs []string + for _, id := range req.normalizeDeviceIDs() { + cdiDeviceIDs = append(cdiDeviceIDs, vendor+"/gpu="+id) + } + return cdiDeviceIDs +} + +func (req *GPUReq) normalizeDeviceIDs() []string { + if len(req.DeviceIDs) > 0 { + return req.DeviceIDs + } + if req.Count < 0 { + return []string{"all"} + } + var ids []string + for i := 0; i < req.Count; i++ { + ids = append(ids, fmt.Sprintf("%d", i)) + } + + return ids +} + // ParseGPUOptCSV parses a GPU option from CSV. func ParseGPUOptCSV(value string) (*GPUReq, error) { csvReader := csv.NewReader(strings.NewReader(value)) @@ -93,6 +116,27 @@ func ParseGPUOptCSV(value string) (*GPUReq, error) { return &req, nil } +func parseGPUOpts(gpuOpts []string) ([]string, error) { + if len(gpuOpts) == 0 { + return nil, nil + } + + vendor := detectGPUVendorFromCDI() + if vendor == "" { + return nil, fmt.Errorf("no known GPU vendor found in CDI specs") + } + + gpuCDIDevices := []string{} + for _, gpu := range gpuOpts { + req, err := ParseGPUOptCSV(gpu) + if err != nil { + return nil, err + } + gpuCDIDevices = append(gpuCDIDevices, req.toCDIDeviceIDs(vendor)...) + } + return gpuCDIDevices, nil +} + func parseCount(s string) (int, error) { if s == "all" { return -1, nil diff --git a/pkg/cmd/container/run_linux.go b/pkg/cmd/container/run_linux.go index 851307d905e..f49aee4197e 100644 --- a/pkg/cmd/container/run_linux.go +++ b/pkg/cmd/container/run_linux.go @@ -98,11 +98,6 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts if options.Sysctl != nil { opts = append(opts, WithSysctls(strutil.ConvertKVStringsToMap(options.Sysctl))) } - gpuOpt, err := parseGPUOpts(options.GOptions.CDISpecDirs, options.GPUs) - if err != nil { - return nil, err - } - opts = append(opts, gpuOpt...) if options.RDTClass != "" { opts = append(opts, oci.WithRdt(options.RDTClass, "", "")) @@ -260,37 +255,3 @@ func withOOMScoreAdj(score int) oci.SpecOpts { return nil } } - -func parseGPUOpts(cdiSpecDirs []string, value []string) (res []oci.SpecOpts, _ error) { - for _, gpu := range value { - req, err := ParseGPUOptCSV(gpu) - if err != nil { - return nil, err - } - res = append(res, withCDIDevices(cdiSpecDirs, req.toCDIDeviceIDS()...)) - } - return res, nil -} - -func (req *GPUReq) toCDIDeviceIDS() []string { - var cdiDeviceIDs []string - for _, id := range req.normalizeDeviceIDs() { - cdiDeviceIDs = append(cdiDeviceIDs, "nvidia.com/gpu="+id) - } - return cdiDeviceIDs -} - -func (req *GPUReq) normalizeDeviceIDs() []string { - if len(req.DeviceIDs) > 0 { - return req.DeviceIDs - } - if req.Count < 0 { - return []string{"all"} - } - var ids []string - for i := 0; i < req.Count; i++ { - ids = append(ids, fmt.Sprintf("%d", i)) - } - - return ids -} From 5bd9041dcf261fbf6cf4298a7d3464463ad907ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:56:35 +0000 Subject: [PATCH 418/868] build(deps): bump the golang-x group with 5 updates Bumps the golang-x group with 5 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.47.0` | `0.48.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.49.0` | `0.50.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.40.0` | `0.41.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.39.0` | `0.40.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.33.0` | `0.34.0` | Updates `golang.org/x/crypto` from 0.47.0 to 0.48.0 - [Commits](https://github.com/golang/crypto/compare/v0.47.0...v0.48.0) Updates `golang.org/x/net` from 0.49.0 to 0.50.0 - [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0) Updates `golang.org/x/sys` from 0.40.0 to 0.41.0 - [Commits](https://github.com/golang/sys/compare/v0.40.0...v0.41.0) Updates `golang.org/x/term` from 0.39.0 to 0.40.0 - [Commits](https://github.com/golang/term/compare/v0.39.0...v0.40.0) Updates `golang.org/x/text` from 0.33.0 to 0.34.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.33.0...v0.34.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 134b7b8f24d..fc012109ca8 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.47.0 - golang.org/x/net v0.49.0 + golang.org/x/crypto v0.48.0 + golang.org/x/net v0.50.0 golang.org/x/sync v0.19.0 //gomodjail:unconfined - golang.org/x/sys v0.40.0 //gomodjail:unconfined - golang.org/x/term v0.39.0 //gomodjail:unconfined - golang.org/x/text v0.33.0 + golang.org/x/sys v0.41.0 //gomodjail:unconfined + golang.org/x/term v0.40.0 //gomodjail:unconfined + golang.org/x/text v0.34.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) @@ -135,7 +135,7 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.31.0 // indirect + golang.org/x/mod v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect //gomodjail:unconfined google.golang.org/grpc v1.76.0 // indirect diff --git a/go.sum b/go.sum index a8f9af9ee5e..82fa5a5a125 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 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= @@ -376,8 +376,8 @@ 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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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= @@ -394,8 +394,8 @@ 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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 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= @@ -432,8 +432,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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 +443,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.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= 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,8 +454,8 @@ 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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 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= @@ -468,8 +468,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= 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= From 7c822d1963e04da01215a589b8eb8bf33dc5f036 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:56:51 +0000 Subject: [PATCH 419/868] build(deps): bump github.com/klauspost/compress from 1.18.3 to 1.18.4 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.3 to 1.18.4. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.3...v1.18.4) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 134b7b8f24d..3dc2d87055a 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.0 - github.com/klauspost/compress v1.18.3 + github.com/klauspost/compress v1.18.4 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 diff --git a/go.sum b/go.sum index a8f9af9ee5e..000cf4198a8 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From af1a5427a3c42ccb8ce66ab84a80416457642786 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Tue, 10 Feb 2026 23:21:21 +0900 Subject: [PATCH 420/868] test: refactor container_restart_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_restart_linux_test.go | 215 +++++++++++++----- 1 file changed, 152 insertions(+), 63 deletions(-) diff --git a/cmd/nerdctl/container/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go index 954c9760db7..9e6ca2d0e09 100644 --- a/cmd/nerdctl/container/container_restart_linux_test.go +++ b/cmd/nerdctl/container/container_restart_linux_test.go @@ -17,6 +17,7 @@ package container import ( + "encoding/json" "fmt" "strconv" "strings" @@ -30,102 +31,190 @@ import ( "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 TestRestart(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", testutil.Identifier(t), testutil.NginxAlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) - base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - base.EnsureContainerStarted(tID) + inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) + data.Labels().Set("pid", strconv.Itoa(inspect.State.Pid)) + + helpers.Ensure("restart", testutil.Identifier(t)) + nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) + } - inspect := base.InspectContainer(tID) - pid := inspect.State.Pid + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", testutil.Identifier(t)) + } - base.Cmd("restart", tID).AssertOK() - base.EnsureContainerStarted(tID) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", testutil.Identifier(t)) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err) + assert.Equal(t, 1, len(dc)) + + assert.Assert(t, data.Labels().Get("pid") != strconv.Itoa(dc[0].State.Pid)) + }, + } + } - newInspect := base.InspectContainer(tID) - newPid := newInspect.State.Pid - assert.Assert(t, pid != newPid) + testCase.Run(t) } func TestRestartPIDContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + baseContainerName := testutil.Identifier(t) + helpers.Ensure("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, baseContainerName) + + sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) + helpers.Ensure("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, sharedContainerName) - baseContainerName := testutil.Identifier(t) - base.Cmd("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", baseContainerName).Run() + helpers.Ensure("restart", baseContainerName) + nerdtest.EnsureContainerStarted(helpers, baseContainerName) + helpers.Ensure("restart", sharedContainerName) + nerdtest.EnsureContainerStarted(helpers, sharedContainerName) - sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) - base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", sharedContainerName).Run() + // output format : /proc/1/ns/pid + // example output: 4026532581 /proc/1/ns/pid + basePSResult := helpers.Capture("exec", baseContainerName, "ls", "-Li", "/proc/1/ns/pid") + baseOutput := strings.TrimSpace(basePSResult) - base.Cmd("restart", baseContainerName).AssertOK() - base.Cmd("restart", sharedContainerName).AssertOK() + data.Labels().Set("baseContainerName", baseContainerName) + data.Labels().Set("sharedContainerName", sharedContainerName) + data.Labels().Set("baseOutput", baseOutput) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("baseContainerName")) + helpers.Anyhow("rm", "-f", data.Labels().Get("sharedContainerName")) + } - // output format : /proc/1/ns/pid - // example output: 4026532581 /proc/1/ns/pid - basePSResult := base.Cmd("exec", baseContainerName, "ls", "-Li", "/proc/1/ns/pid").Run() - baseOutput := strings.TrimSpace(basePSResult.Stdout()) - sharedPSResult := base.Cmd("exec", sharedContainerName, "ls", "-Li", "/proc/1/ns/pid").Run() - sharedOutput := strings.TrimSpace(sharedPSResult.Stdout()) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("sharedContainerName"), "ls", "-Li", "/proc/1/ns/pid") + } + + 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.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("baseOutput")) + }, + } + } - assert.Equal(t, baseOutput, sharedOutput) + testCase.Run(t) } func TestRestartIPCContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + const shmSize = "32m" + baseContainerName := testutil.Identifier(t) + helpers.Ensure("run", "-d", "--shm-size", shmSize, "--ipc", "shareable", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, baseContainerName) + + sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) + helpers.Ensure("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--ipc=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, sharedContainerName) - const shmSize = "32m" - baseContainerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", baseContainerName).Run() - base.Cmd("run", "-d", "--shm-size", shmSize, "--ipc", "shareable", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() + helpers.Ensure("stop", baseContainerName) + helpers.Ensure("stop", sharedContainerName) - sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName) - defer base.Cmd("rm", "-f", sharedContainerName).Run() - base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--ipc=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() + helpers.Ensure("restart", baseContainerName) + nerdtest.EnsureContainerStarted(helpers, baseContainerName) + helpers.Ensure("restart", sharedContainerName) + nerdtest.EnsureContainerStarted(helpers, sharedContainerName) - base.Cmd("stop", baseContainerName).Run() - base.Cmd("stop", sharedContainerName).Run() + baseShmSizeResult := helpers.Capture("exec", baseContainerName, "/bin/grep", "shm", "/proc/self/mounts") + baseOutput := strings.TrimSpace(baseShmSizeResult) - base.Cmd("restart", baseContainerName).AssertOK() - base.Cmd("restart", sharedContainerName).AssertOK() + data.Labels().Set("baseContainerName", baseContainerName) + data.Labels().Set("sharedContainerName", sharedContainerName) + data.Labels().Set("baseOutput", baseOutput) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("baseContainerName")) + helpers.Anyhow("rm", "-f", data.Labels().Get("sharedContainerName")) + } - baseShmSizeResult := base.Cmd("exec", baseContainerName, "/bin/grep", "shm", "/proc/self/mounts").Run() - baseOutput := strings.TrimSpace(baseShmSizeResult.Stdout()) - sharedShmSizeResult := base.Cmd("exec", sharedContainerName, "/bin/grep", "shm", "/proc/self/mounts").Run() - sharedOutput := strings.TrimSpace(sharedShmSizeResult.Stdout()) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("sharedContainerName"), "/bin/grep", "shm", "/proc/self/mounts") + } + + 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.Equal(t, strings.TrimSpace(stdout), data.Labels().Get("baseOutput")) + }, + } + } - assert.Equal(t, baseOutput, sharedOutput) + testCase.Run(t) } func TestRestartWithTime(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) + testCase := nerdtest.Setup() - base.Cmd("run", "-d", "--name", tID, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := testutil.Identifier(t) + helpers.Ensure("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, containerName) - inspect := base.InspectContainer(tID) - pid := inspect.State.Pid + inspect := nerdtest.InspectContainer(helpers, containerName) + pid := inspect.State.Pid + + data.Labels().Set("containerName", containerName) + data.Labels().Set("pid", strconv.Itoa(pid)) + } - timePreRestart := time.Now() - base.Cmd("restart", "-t", "5", tID).AssertOK() - timePostRestart := time.Now() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) + } - newInspect := base.InspectContainer(tID) - newPid := newInspect.State.Pid - assert.Assert(t, pid != newPid) - // ensure that stop took at least 5 seconds - assert.Assert(t, timePostRestart.Sub(timePreRestart) >= time.Second*5) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + data.Labels().Set("timePreRestart", time.Now().Format(time.RFC3339)) + return helpers.Command("restart", "-t", "5", data.Labels().Get("containerName")) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + timePostRestart := time.Now() + timePreRestart, err := time.Parse(time.RFC3339, data.Labels().Get("timePreRestart")) + assert.NilError(t, err) + // ensure that stop took at least 5 seconds + assert.Assert(t, timePostRestart.Sub(timePreRestart) >= time.Second*5) + + inspect := nerdtest.InspectContainer(helpers, data.Labels().Get("containerName")) + assert.Assert(t, strconv.Itoa(inspect.State.Pid) != data.Labels().Get("pid")) + }, + } + } + + testCase.Run(t) } func TestRestartWithSignal(t *testing.T) { From 7b734d79c931f4d2b12a126158cfdc5d67aa98a7 Mon Sep 17 00:00:00 2001 From: Yuhang Wei Date: Sat, 24 Jan 2026 13:54:29 +0800 Subject: [PATCH 421/868] style: format code on container_run_linux tests Signed-off-by: Yuhang Wei --- .../container/container_run_linux_test.go | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index 0188a7be7f9..648042dd286 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -189,7 +189,7 @@ func TestRunIpcHost(t *testing.T) { base := testutil.NewBase(t) testFilePath := filepath.Join("/dev/shm", fmt.Sprintf("%s-%d-%s", testutil.Identifier(t), os.Geteuid(), base.Target)) - err := os.WriteFile(testFilePath, []byte(""), 0644) + err := os.WriteFile(testFilePath, []byte(""), 0o644) assert.NilError(base.T, err) defer os.Remove(testFilePath) @@ -203,7 +203,7 @@ func TestRunAddHost(t *testing.T) { var found bool sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { - //removing spaces and tabs separating items + // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strings.Contains(line, "10.0.0.1testing.example.com") { @@ -219,7 +219,7 @@ func TestRunAddHost(t *testing.T) { var found int sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { - //removing spaces and tabs separating items + // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strutil.InStringSlice([]string{"10.0.0.1test", "10.0.0.1test1"}, line) { @@ -252,7 +252,7 @@ func TestRunAddHostWithCustomHostGatewayIP(t *testing.T) { var found bool sc := bufio.NewScanner(bytes.NewBufferString(stdout)) for sc.Scan() { - //removing spaces and tabs separating items + // removing spaces and tabs separating items line := strings.ReplaceAll(sc.Text(), " ", "") line = strings.ReplaceAll(line, "\t", "") if strings.Contains(line, "192.168.5.2test") { @@ -449,7 +449,7 @@ func TestRunSigProxy(t *testing.T) { func TestRunWithFluentdLogDriver(t *testing.T) { base := testutil.NewBase(t) tempDirectory := t.TempDir() - err := os.Chmod(tempDirectory, 0777) + err := os.Chmod(tempDirectory, 0o777) assert.NilError(t, err) containerName := testutil.Identifier(t) @@ -478,7 +478,7 @@ func TestRunWithFluentdLogDriver(t *testing.T) { func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) { base := testutil.NewBase(t) tempDirectory := t.TempDir() - err := os.Chmod(tempDirectory, 0777) + err := os.Chmod(tempDirectory, 0o777) assert.NilError(t, err) containerName := testutil.Identifier(t) @@ -510,7 +510,7 @@ func TestRunWithOOMScoreAdj(t *testing.T) { } t.Parallel() base := testutil.NewBase(t) - var score = "-42" + score := "-42" base.Cmd("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj").AssertOutContains(score) } @@ -712,7 +712,7 @@ devices: tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") err := os.WriteFile(tomlPath, []byte(fmt.Sprintf(` cdi_spec_dirs = ["%s"] -`, cdiSpecDir)), 0400) +`, cdiSpecDir)), 0o400) assert.NilError(t, err) base := testutil.NewBase(t) @@ -854,6 +854,7 @@ devices: env: - FOO=injected ` + tmpDir := t.TempDir() writeTestCDISpec(t, amdSpec, "amd.yaml", tmpDir) writeTestCDISpec(t, vendor1Spec, "vendor1.yaml", tmpDir) @@ -878,9 +879,9 @@ devices: } func writeTestCDISpec(t *testing.T, spec string, fileName string, cdiSpecDir string) { - err := os.MkdirAll(cdiSpecDir, 0700) + err := os.MkdirAll(cdiSpecDir, 0o700) assert.NilError(t, err) cdiSpecPath := filepath.Join(cdiSpecDir, fileName) - err = os.WriteFile(cdiSpecPath, []byte(spec), 0400) + err = os.WriteFile(cdiSpecPath, []byte(spec), 0o400) assert.NilError(t, err) } From 83807d39df93f7bf632487708cd9ab4ec68f2c15 Mon Sep 17 00:00:00 2001 From: Yuhang Wei Date: Sat, 24 Jan 2026 13:54:45 +0800 Subject: [PATCH 422/868] fix: shared IPC ns when setting shareable Signed-off-by: Yuhang Wei --- .../container/container_run_linux_test.go | 80 +++++++++++++++++++ pkg/ipcutil/ipcutil.go | 5 ++ 2 files changed, 85 insertions(+) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index 648042dd286..fe18f1a46ce 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -885,3 +885,83 @@ func writeTestCDISpec(t *testing.T, spec string, fileName string, cdiSpecDir str err = os.WriteFile(cdiSpecPath, []byte(spec), 0o400) assert.NilError(t, err) } + +func TestSharedIpcSetup(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set("container1", data.Identifier("container1")) + helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), "--ipc=shareable", + testutil.CommonImage, "sleep", "inf") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container1")) + }, + SubTests: []*test.Case{ + { + Description: "Test ipc is shared", + NoParallel: true, // The validation involves starting of the main container: container1 + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container2")) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure( + "run", "-d", "--name", data.Identifier("container2"), + "--ipc=container:"+data.Labels().Get("container1"), + testutil.NginxAlpineImage) + data.Labels().Set("container2", data.Identifier("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) + }, + SubTests: []*test.Case{ + { + NoParallel: true, + Description: "Test ipc is shared", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "readlink", "/proc/1/ns/ipc") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(stdout string, t tig.T) { + container1IPC := strings.TrimSpace(helpers.Capture("exec", data.Labels().Get("container1"), "readlink", "/proc/1/ns/ipc")) + container2IPC := strings.TrimSpace(stdout) + assert.Equal(t, container1IPC, container2IPC) + }, + ), + } + }, + }, + { + NoParallel: true, + Description: "Test ipc is shared after restart", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("restart", data.Labels().Get("container1")) + helpers.Ensure("stop", "--time=1", data.Labels().Get("container2")) + helpers.Ensure("start", data.Labels().Get("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("container2")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "readlink", "/proc/1/ns/ipc") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(stdout string, t tig.T) { + container1IPC := strings.TrimSpace(helpers.Capture("exec", data.Labels().Get("container1"), "readlink", "/proc/1/ns/ipc")) + container2IPC := strings.TrimSpace(stdout) + assert.Equal(t, container1IPC, container2IPC) + }, + ), + } + }, + }, + }, + }, + }, + } + testCase.Run(t) +} diff --git a/pkg/ipcutil/ipcutil.go b/pkg/ipcutil/ipcutil.go index 7fd3240f15c..d2f0ea25e1b 100644 --- a/pkg/ipcutil/ipcutil.go +++ b/pkg/ipcutil/ipcutil.go @@ -207,6 +207,11 @@ func GenerateIPCOpts(ctx context.Context, ipc IPC, client *containerd.Client) ([ } opts = append(opts, withBindMountHostOtherSourceIPC(*targetConIPC.HostShmPath)) + ns := specs.LinuxNamespace{ + Type: specs.IPCNamespace, + Path: fmt.Sprintf("/proc/%d/ns/ipc", task.Pid()), + } + opts = append(opts, oci.WithLinuxNamespace(ns)) } return opts, nil From 408d9585894edcee4e22f1ec4ff5cc69e02e5f61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:32:21 +0000 Subject: [PATCH 423/868] build(deps): bump docker/build-push-action from 6.18.0 to 6.19.1 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.1. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/263435318d21b8e681c14492fe198d362a7d2c83...601a80b39c9405e50806ae38af30926f9d957c47) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 6.19.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 1f380f8f01a..87343e12729 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 with: context: . platforms: linux/amd64,linux/arm64 From 563d9b9d83bd07be036eb905036645741b78a671 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:32:42 +0000 Subject: [PATCH 424/868] build(deps): bump docker/build-push-action from 6.19.1 to 6.19.2 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.1 to 6.19.2. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/601a80b39c9405e50806ae38af30926f9d957c47...10e90e3645eae34f1e60eeb005ba3a3d33f178e8) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 6.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 87343e12729..989cae1103e 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . platforms: linux/amd64,linux/arm64 From 6aa0d41f3658bd57325d0dd1f034b2da9d2e41b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:31:49 +0000 Subject: [PATCH 425/868] build(deps): bump github.com/containerd/cgroups/v3 from 3.1.2 to 3.1.3 Bumps [github.com/containerd/cgroups/v3](https://github.com/containerd/cgroups) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/containerd/cgroups/releases) - [Commits](https://github.com/containerd/cgroups/compare/v3.1.2...v3.1.3) --- updated-dependencies: - dependency-name: github.com/containerd/cgroups/v3 dependency-version: 3.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 07d6883fe13..6f8cfaf8074 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Microsoft/hcsshim v0.14.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.1 - github.com/containerd/cgroups/v3 v3.1.2 //gomodjail:unconfined + 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.10.0 github.com/containerd/containerd/v2 v2.2.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4b9092792ea..b9772686b2a 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyj github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= github.com/containerd/accelerated-container-image v1.4.1 h1:jeZYAaq5pMCeyRZ0I916OjJsEb2TGjAQmfAZyQLi3ec= github.com/containerd/accelerated-container-image v1.4.1/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= -github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= -github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= +github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= +github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= From 326193234c102e07369dfd75773a06d53a3bb114 Mon Sep 17 00:00:00 2001 From: rohansood10 Date: Thu, 19 Feb 2026 03:10:16 -0800 Subject: [PATCH 426/868] docs: update list of unimplemented Docker features Update command-reference.md to reflect the current state of feature parity with Docker: - Remove docker diff from unimplemented commands (now implemented) - Remove --isolation from unimplemented docker run flags (now implemented) - Remove --interactive from unimplemented docker start flags (now implemented) - Add --mount type=image as unimplemented mount type - Add :z and :Z (SELinux relabeling) as unimplemented volume options - Add docker buildx debug (buildx debugger) to unimplemented commands Fixes #3867 Signed-off-by: rohansood10 --- docs/command-reference.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 33b2736418b..a4a29958fd3 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -288,6 +288,7 @@ Volume flags: - :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation - :nerd_face: option `bind`: Not-recursively bind-mounted - :nerd_face: option `rbind`: Recursively bind-mounted + - unimplemented options: `:z` and `:Z` (SELinux relabeling) - :whale: `--tmpfs`: Mount a tmpfs directory, e.g. `--tmpfs /tmp:size=64m,exec`. - :whale: `--mount`: Attach a filesystem mount to the container. Consists of multiple key-value pairs, separated by commas and each @@ -296,6 +297,7 @@ Volume flags: - :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`. The default type will be set to `volume` if not specified. i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volume,src=vol-1,dst=/app,readonly` + - unimplemented type: `image` - Common Options: - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. - :whale: `dst`, `destination`, `target`: Mount destination spec. @@ -451,7 +453,7 @@ IPFS flags: - :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`) Unimplemented `docker run` flags: - `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, `--isolation`, + `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, `--link*`, `--publish-all`, `--storage-opt`, `--volume-driver` ### :whale: nerdctl exec @@ -623,8 +625,6 @@ Flags: - :whale: `--checkpoint`: checkpoint name - :whale: `--detach-keys`: checkpoint directory -Unimplemented `docker start` flags: `--interactive` - ### :whale: nerdctl restart Restart one or more running containers. @@ -1983,10 +1983,6 @@ See [`./config.md`](./config.md). ## Unimplemented Docker commands -Container management: - -- `docker diff` - Image: - `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign|notation` and `nerdctl push --sign=cosign|notation`. See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md).) @@ -2000,6 +1996,10 @@ Compose: - `docker-compose events|scale` +Builder: + +- `docker buildx debug` (buildx debugger) + Others: - `docker system df` From 6140a6b56ccb89bb20f000fa9e62e2c4edc90964 Mon Sep 17 00:00:00 2001 From: Shubhranshu Mahapatra Date: Mon, 23 Feb 2026 17:24:20 -0800 Subject: [PATCH 427/868] fix: TestRunWithSystemdTrueEnabled flakiness Signed-off-by: Shubhranshu Mahapatra --- cmd/nerdctl/container/container_run_systemd_linux_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go index 5ddc734464e..759b2962892 100644 --- a/cmd/nerdctl/container/container_run_systemd_linux_test.go +++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go @@ -74,6 +74,7 @@ func TestRunWithSystemdTrueEnabled(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { From b4af60eee4044e29f83dc300e9bf54dd82d2859e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 26 Feb 2026 22:09:53 +0000 Subject: [PATCH 428/868] CI: fix `fatal: Remote branch v not found in upstream origin` Fix issue 4753 NOTE: used Claude Code Signed-off-by: Akihiro Suda --- hack/build-integration-canary.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hack/build-integration-canary.sh b/hack/build-integration-canary.sh index 725628b962a..d94fdc91ea2 100755 --- a/hack/build-integration-canary.sh +++ b/hack/build-integration-canary.sh @@ -162,15 +162,16 @@ latest::release(){ while read -r line; do [ ! "$ignore" ] || ! grep -q "$ignore" <<<"$line" || continue - name="$(echo "$line" | jq -rc .name)" + # Use tag_name as the canonical version identifier (name is an optional display label and may be empty) + name="$(echo "$line" | jq -rc 'if .name != "" then .name else .tag_name end')" if [ "$name" == "" ] || [ "$name" == null ] ; then log::debug " > bogus release name ($name) ignored" continue fi log::debug " > found release: $name" - if version::compare <(echo "$line" | jq -rc .name); then + if version::compare <(echo "$name"); then higher_data="$line" - higher_readable="$(echo "$line" | jq -rc .name | sed -E 's/(.*[ ])?(v?[0-9][0-9.a-z-]+).*/\2/')" + higher_readable="$(echo "$name" | sed -E 's/(.*[ ])?(v?[0-9][0-9.a-z-]+).*/\2/')" fi done < <(github::releases "$repo") From 89dbffb6d0f5a3af62439de93565b7155a97dd8e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 27 Feb 2026 10:45:41 +0000 Subject: [PATCH 429/868] CI: show consistently failing test names at end of job log Improve gotestsum-reporter.sh to surface failing test names more prominently: - Replace the simple jq query with a jq|awk pipeline that identifies consistently failing tests (failed and never passed), excluding flaky tests that passed on retry, with deduplication and sorting - Print a plain-text banner to stdout at the end of the run so failing test names are visible in the job log without navigating to the step summary - Emit a ::error:: GitHub Actions annotation so failing tests also appear in the PR checks view and annotations panel Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Akihiro Suda --- hack/github/gotestsum-reporter.sh | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/hack/github/gotestsum-reporter.sh b/hack/github/gotestsum-reporter.sh index 872fc25f03d..edd357c0487 100755 --- a/hack/github/gotestsum-reporter.sh +++ b/hack/github/gotestsum-reporter.sh @@ -24,6 +24,19 @@ readonly root GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}" +# Identify consistently failing tests: those that failed but never passed, even on retry. +# Tests that failed then passed on retry (flaky) are excluded. +failing_tests="$(jq -rc 'select(.Test) | select(.Action == "fail" or .Action == "pass") | [.Action, .Test] | @tsv' < "$GOTESTSUM_JSONFILE" \ + | awk -F'\t' ' + $1 == "fail" { failed[$2] = 1 } + $1 == "pass" { passed[$2] = 1 } + END { + for (t in failed) { + if (!(t in passed)) print t + } + } + ' | sort)" + { github::md::h3 "Total number of tests: $TESTS_TOTAL" github::md::pie "Status" "Skipped" "$TESTS_SKIPPED" "Failed" "$TESTS_FAILED" "Passed" "$(( TESTS_TOTAL - TESTS_FAILED - TESTS_SKIPPED ))" @@ -34,7 +47,7 @@ GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}" github::md::h3 "Failing tests" echo '```' - jq -rc 'select(.Action == "fail") | select(.Test) | .Test' < "$GOTESTSUM_JSONFILE" + echo "${failing_tests:-}" echo '```' github::md::h3 "Tests taking more than 15 seconds" @@ -42,3 +55,12 @@ GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}" gotestsum tool slowest --threshold 15s --jsonfile "$GOTESTSUM_JSONFILE" echo '```' } >> "$GITHUB_STEP_SUMMARY" + +# Print failing tests to stdout so they are visible at the end of the job log. +if [ -n "${failing_tests:-}" ]; then + printf '\n=== Failing tests ===\n%s\n=====================\n' "$failing_tests" + # Also emit as a GitHub Actions error annotation (visible in PR checks and annotations panel). + # GitHub Actions uses %0A for newlines inside annotation messages. + encoded="${failing_tests//$'\n'/%0A}" + echo "::error title=Failing tests::${encoded}" +fi From 823b183635c8edb83c03c472c8489b9d763b4d7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:43:17 +0000 Subject: [PATCH 430/868] build(deps): bump actions/setup-go from 6.2.0 to 6.3.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.2.0 to 6.3.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5...4b73464bb391d4059bd26b0524d20df3927bd417) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 6951e057b80..37993d49576 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 44b701af882..083c06458d3 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 2878b376559..d17da7e5518 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index f61676bfe8a..60055343450 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 63f1cc3836c..f2f9fbae799 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c63dae62c5a..3bcfbbd130b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: "Install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 6b868c3deed..39ace93747c 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From 949d029d68cc3f859ee8d4d751c7eda83dcb83ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:32:27 +0000 Subject: [PATCH 431/868] build(deps): bump actions/attest-build-provenance from 3.2.0 to 4.1.0 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.2.0 to 4.1.0. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/96278af6caaf10aea03fd8d33a09a777ca52d62f...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c63dae62c5a..5964dd47953 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From a1cffd64ccc1ac6c261f10937e3b3fa57ccac90c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 1 Mar 2026 02:23:14 +0900 Subject: [PATCH 432/868] CNI: add Homebrew's installation path Signed-off-by: Akihiro Suda --- docs/faq.md | 1 + pkg/defaults/defaults_linux.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index c2313f9ce16..eef678ca9d2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -218,6 +218,7 @@ The default value is automatically detected by checking the following candidates - `~/opt/cni/bin` - `/usr/local/libexec/cni` - `/usr/local/lib/cni` +- `/home/linuxbrew/.linuxbrew/opt/cni-plugins/bin` - `/usr/libexec/cni` - `/usr/lib/cni` - `/opt/cni/bin` diff --git a/pkg/defaults/defaults_linux.go b/pkg/defaults/defaults_linux.go index 02975aff3b0..3edadaaeaa4 100644 --- a/pkg/defaults/defaults_linux.go +++ b/pkg/defaults/defaults_linux.go @@ -50,8 +50,9 @@ func CNIPath() string { cni.DefaultCNIDir, // /opt/cni/bin "/usr/local/libexec/cni", "/usr/local/lib/cni", - "/usr/libexec/cni", // Fedora - "/usr/lib/cni", // debian (containernetworking-plugins) + "/home/linuxbrew/.linuxbrew/opt/cni-plugins/bin", // Homebrew + "/usr/libexec/cni", // Fedora + "/usr/lib/cni", // debian (containernetworking-plugins) } if rootlessutil.IsRootless() { home := os.Getenv("HOME") From fa67cc252e051bacef4fecb306de99e91f4a0bb1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 2 Mar 2026 04:16:12 +0900 Subject: [PATCH 433/868] CI: temporarily disable release check for PRs Not likely compromised so far, but temporarily disable the check during investigation. Signed-off-by: Akihiro Suda --- .github/workflows/release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c63dae62c5a..c898e6ce4dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,9 +5,6 @@ on: tags: - 'v*' - 'test-action-release-*' - pull_request: - paths-ignore: - - '**.md' env: GOTOOLCHAIN: local From c4605fa189bf9870cdd67b50a2aa8d8f89b29def Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 2 Mar 2026 16:23:42 +0900 Subject: [PATCH 434/868] Revert "CI: temporarily disable release check for PRs" This reverts commit fa67cc252e051bacef4fecb306de99e91f4a0bb1. Signed-off-by: Akihiro Suda --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 606eab2d9b5..73d81575e1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' - 'test-action-release-*' + pull_request: + paths-ignore: + - '**.md' env: GOTOOLCHAIN: local From c6fe9fd26bbd7182fb6a2dcdf849bfe05ac6fa61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:32:30 +0000 Subject: [PATCH 435/868] build(deps): bump docker/login-action from 3.7.0 to 4.0.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 989cae1103e..dd2e2a7d510 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From b427cc78c449d261b00a97f16c12db2f25a48184 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:32:35 +0000 Subject: [PATCH 436/868] build(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.7.0 to 4.0.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/c7c53464625b32c7a7e944ae62b3e17d2b600130...ce360397dd3f832beb865e1373c09c0e9f86d70a) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 989cae1103e..ad64e6e53ea 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -35,7 +35,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 606eab2d9b5..30ed1623045 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: "Install go" uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: From 90092c67a0af369d5ad8f6a3d8267392e350dc0c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 5 Mar 2026 21:59:02 +0900 Subject: [PATCH 437/868] tests: fix flaky TestLogs since/until 1s subtests The `since_1s` and `until_1s` subtests raced against wall clock: if the container finished within 1 second before the command ran, both tests produced wrong results. Add a 2-second Setup sleep so the container logs are always >1s old by the time the timed-filter commands execute. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_logs_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 0110a9f4cdf..a1c483290d6 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -60,6 +60,11 @@ bar testCase.SubTests = []*test.Case{ { Description: "since 1s", + Setup: func(data test.Data, helpers test.Helpers) { + // Ensure at least 2 seconds have elapsed since the container ran, + // so that --since 1s does not include the container's output. + time.Sleep(2 * time.Second) + }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--since", "1s", data.Labels().Get("cID")) }, @@ -81,6 +86,11 @@ bar }, { Description: "until 1s", + Setup: func(data test.Data, helpers test.Helpers) { + // Ensure at least 2 seconds have elapsed since the container ran, + // so that --until 1s includes the container's output. + time.Sleep(2 * time.Second) + }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", "--until", "1s", data.Labels().Get("cID")) }, From 0abcf296c559f5638fd85a7612fa358297e10c97 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 5 Mar 2026 22:12:54 +0900 Subject: [PATCH 438/868] tests: de-parallelize TestImageConvert/soci An attempt to deflake it Signed-off-by: Akihiro Suda --- cmd/nerdctl/image/image_convert_linux_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index a13fc08d595..629db629920 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -92,6 +92,7 @@ func TestImageConvert(t *testing.T) { }, { Description: "soci", + NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), nerdtest.Soci, @@ -110,6 +111,7 @@ func TestImageConvert(t *testing.T) { }, { Description: "soci with all-platforms", + NoParallel: true, Require: require.All( require.Not(nerdtest.Docker), nerdtest.Soci, From 3e8b42c6740b92a673ecee61e1e9c94faa5bc638 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:07:04 +0000 Subject: [PATCH 439/868] build(deps): bump crazy-max/ghaction-github-runtime from 3.1.0 to 4.0.0 Bumps [crazy-max/ghaction-github-runtime](https://github.com/crazy-max/ghaction-github-runtime) from 3.1.0 to 4.0.0. - [Release notes](https://github.com/crazy-max/ghaction-github-runtime/releases) - [Commits](https://github.com/crazy-max/ghaction-github-runtime/compare/3cb05d89e1f492524af3d41a1c98c83bc3025124...04d248b84655b509d8c44dc1d6f990c879747487) --- updated-dependencies: - dependency-name: crazy-max/ghaction-github-runtime dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index 5392a8d5c86..be00e2c349b 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -36,7 +36,7 @@ jobs: fetch-depth: 1 - name: "Init: expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Run: build dependencies for the integration test environment image" env: diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 8e57d90cda2..794b04d2a7b 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -72,7 +72,7 @@ jobs: fetch-depth: 1 - name: "Init: expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Init: install br-netfilter" run: | diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 53275b445ee..e94a0715856 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -84,7 +84,7 @@ jobs: lima sudo modprobe br-netfilter - name: "Init: expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - name: "Init: prepare integration tests" env: From c740b77c931ceb28eb6ab7b7b74989e01012a7d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:36:00 +0000 Subject: [PATCH 440/868] build(deps): bump docker/build-push-action from 6.19.2 to 7.0.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...d08e5c354a6adb9ed34480a06d141179aa583294) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 989cae1103e..63713f537d7 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 From 509d08bfbf3d7d153877463576a891c9b1b1d351 Mon Sep 17 00:00:00 2001 From: Mustaeen Ahmed Date: Fri, 6 Mar 2026 18:32:07 -0600 Subject: [PATCH 441/868] Add progress bar Signed-off-by: Mustaeen Ahmed Resolve test issues Add progress bar Signed-off-by: Mustaeen Ahmed Revert changes on container_logs_test.go Signed-off-by: Mustaeen Ahmed Add progress bar --- cmd/nerdctl/image/image_convert.go | 7 ++++- pkg/api/types/image_types.go | 5 ++-- pkg/cmd/image/convert.go | 46 ++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index ebe86119e31..c253a2274e4 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -113,9 +113,13 @@ func convertCommand() *cobra.Command { func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { return types.ImageConvertOptions{}, err } + + progressOutput := cmd.ErrOrStderr() + format, err := cmd.Flags().GetString("format") if err != nil { return types.ImageConvertOptions{}, err @@ -313,7 +317,8 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { AllPlatforms: allPlatforms, }, }, - Stdout: cmd.OutOrStdout(), + ProgressOutput: progressOutput, + Stdout: cmd.OutOrStdout(), }, nil } diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 8999d659213..29ba08607d9 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -47,8 +47,9 @@ type ImageListOptions struct { // ImageConvertOptions specifies options for `nerdctl image convert`. type ImageConvertOptions struct { - Stdout io.Writer - GOptions GlobalCommandOptions + Stdout io.Writer + ProgressOutput io.Writer + GOptions GlobalCommandOptions // #region generic flags // Uncompress convert tar.gz layers to uncompressed tar layers diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index df022b90011..7bb0b926ebb 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -36,6 +36,7 @@ import ( "github.com/containerd/containerd/v2/core/images/converter/uncompress" "github.com/containerd/log" nydusconvert "github.com/containerd/nydus-snapshotter/pkg/converter" + "github.com/containerd/platforms" "github.com/containerd/stargz-snapshotter/estargz" estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" estargzexternaltocconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz/externaltoc" @@ -45,7 +46,9 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" converterutil "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" + "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" @@ -205,6 +208,25 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa convertOpts = append(convertOpts, converter.WithDockerToOCI(true)) } + if options.ProgressOutput != nil { + ongoing := jobs.New(targetRef) + if err := addDescriptorsToJobs(ctx, client, srcRef, platMC, ongoing); err != nil { + return err + } + + progressCtx, cancelProgress := context.WithCancel(ctx) + progressDone := make(chan struct{}) + + go func() { + jobs.ShowProgress(progressCtx, ongoing, client.ContentStore(), options.ProgressOutput) + close(progressDone) + }() + defer func() { + cancelProgress() + <-progressDone + }() + } + // converter.Convert() gains the lease by itself newImg, err := converterutil.Convert(ctx, client, targetRef, srcRef, convertOpts...) if err != nil { @@ -264,6 +286,30 @@ func getESGZConverter(options types.ImageConvertOptions) (convertFunc converter. return convertFunc, finalize, nil } +func addDescriptorsToJobs(ctx context.Context, client *containerd.Client, srcRef string, platMC platforms.MatchComparer, ongoing *jobs.Jobs) error { + imageService := client.ImageService() + img, err := imageService.Get(ctx, srcRef) + if err != nil { + return err + } + + provider := containerdutil.NewProvider(client) + handler := images.ChildrenHandler(provider) + if platMC != nil { + handler = images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if desc.Platform != nil && !platMC.Match(*desc.Platform) { + return nil, nil + } + return images.Children(ctx, provider, desc) + }) + } + + return images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + ongoing.Add(desc) + return handler(ctx, desc) + }), img.Target) +} + func getESGZConvertOpts(options types.ImageConvertOptions) ([]estargz.Option, error) { esgzOpts := []estargz.Option{ From f2ff7d92143630394feef3ba4e49dd4b7c90ce24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:47:49 +0000 Subject: [PATCH 442/868] build(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index c5bf546acab..ae35434844c 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -38,7 +38,7 @@ jobs: uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action From 5b781b3dfe3329ba4cb75842f2ab8c2618e0cea5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:55:23 +0000 Subject: [PATCH 443/868] build(deps): bump docker/metadata-action from 5.10.0 to 6.0.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.10.0 to 6.0.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/c299e40c65443455700f0fdfc63efafe5b349051...030e881283bb7a6894de51c315a6bfe6a94e05cf) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index c5bf546acab..ec37d6d38c8 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -54,7 +54,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From 93f26cb7b5344582f9d73b583d08785c15de7741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:32:58 +0000 Subject: [PATCH 444/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 44 ++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 6f8cfaf8074..78cb53cb05f 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.10.0 - github.com/containerd/containerd/v2 v2.2.1 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.2.2 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -85,7 +85,7 @@ require ( github.com/djherbis/times v1.6.0 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -128,17 +128,17 @@ require ( github.com/vbatts/tar-split v0.12.2 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect //gomodjail:unconfined - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/grpc v1.78.0 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b9772686b2a..171bedceea7 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,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.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= -github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk= -github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU= +github.com/containerd/containerd/v2 v2.2.2 h1:mjVQdtfryzT7lOqs5EYUFZm8ioPVjOpkSoG1GJPxEMY= +github.com/containerd/containerd/v2 v2.2.2/go.mod h1:5Jhevmv6/2J+Iu/A2xXAdUIdI5Ah/hfyO7okJ4AFIdY= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -114,8 +114,8 @@ github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOe github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -270,8 +270,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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/v2 v2.3.6 h1:m/26nAx0DbHZYaM46+uoQjfpu9G77QLzWj2jz25chO8= @@ -327,20 +327,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec 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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 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= @@ -481,15 +481,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-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 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= From 55513ee34ee9c1b7948b2c57ae7bda68b21d8de2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 15 Mar 2026 03:45:07 +0900 Subject: [PATCH 445/868] Dockerfile: runc: omit libpathrs Fix issue 4793 Signed-off-by: Akihiro Suda --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5ab9d77f08e..1ba5604cbc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,7 +97,8 @@ WORKDIR /go/src/github.com/opencontainers/runc RUN git-checkout-tag-with-hash.sh ${RUNC_VERSION} && \ mkdir -p /out ENV CGO_ENABLED=1 -RUN GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make static && \ +# FIXME: avoid omitting libpathrs +RUN set -x ; GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make BUILDTAGS="$(grep -oP "^BUILDTAGS := \K.*" Makefile | sed -e s/libpathrs//)" static && \ xx-verify --static runc && cp -v -a runc /out/runc.${TARGETARCH} FROM build-base AS build-bypass4netns From 6d2a7ad91bb4cd185f89c86e1486bb5b77e27107 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 7 Jan 2026 03:26:59 +0000 Subject: [PATCH 446/868] add MAC, IPv4, IPv6 addresses to nework inspect Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/network/network_inspect_test.go | 276 +++++++++++++++--- pkg/inspecttypes/dockercompat/dockercompat.go | 107 ++++++- 2 files changed, 330 insertions(+), 53 deletions(-) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 3b7e5276420..980ade0ad57 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -19,6 +19,7 @@ package network import ( "encoding/json" "errors" + "net" "os/exec" "runtime" "strings" @@ -37,7 +38,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -func TestNetworkInspect(t *testing.T) { +func TestNetworkInspectBasic(t *testing.T) { testCase := nerdtest.Setup() const ( @@ -46,15 +47,6 @@ func TestNetworkInspect(t *testing.T) { testIPRange = "10.24.24.0/25" ) - testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier("basenet")) - data.Labels().Set("basenet", data.Identifier("basenet")) - } - - testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier("basenet")) - } - testCase.SubTests = []*test.Case{ { Description: "non existent network", @@ -133,6 +125,59 @@ func TestNetworkInspect(t *testing.T) { assert.Equal(t, dc[0].Name, "custom") }), }, + { + Description: "basic", + // FIXME: IPAMConfig is not implemented on Windows yet + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", "--label", "tag=testNetwork", "--subnet", testSubnet, + "--gateway", testGateway, "--ip-range", testIPRange, data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Network + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n") + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") + got := dc[0] + + assert.Equal(t, got.Name, data.Identifier()) + assert.Equal(t, got.Labels["tag"], "testNetwork") + assert.Equal(t, len(got.IPAM.Config), 1) + assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet) + assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway) + assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange) + }, + } + }, + }, + } + + testCase.Run(t) +} + +func TestNetworkInspectByID(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("basenet")) + data.Labels().Set("basenet", data.Identifier("basenet")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("basenet")) + } + + testCase.SubTests = []*test.Case{ { Description: "match exact id", // See notes below @@ -201,41 +246,6 @@ func TestNetworkInspect(t *testing.T) { } }, }, - { - Description: "basic", - // FIXME: IPAMConfig is not implemented on Windows yet - Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", "--label", "tag=testNetwork", "--subnet", testSubnet, - "--gateway", testGateway, "--ip-range", testIPRange, data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "inspect", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, t tig.T) { - var dc []dockercompat.Network - - err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n") - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n") - got := dc[0] - - assert.Equal(t, got.Name, data.Identifier()) - assert.Equal(t, got.Labels["tag"], "testNetwork") - assert.Equal(t, len(got.IPAM.Config), 1) - assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet) - assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway) - assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange) - }, - } - }, - }, { Description: "with namespace", Require: require.Not(nerdtest.Docker), @@ -287,6 +297,15 @@ func TestNetworkInspect(t *testing.T) { } }, }, + } + + testCase.Run(t) +} + +func TestNetworkInspectWithContainers(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ { Description: "Verify that only active containers appear in the network inspect output", Setup: func(data test.Data, helpers test.Helpers) { @@ -397,6 +416,173 @@ func TestNetworkInspect(t *testing.T) { } }, }, + { + Description: "Test container network details", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("test-network")) + + // See https://github.com/containerd/nerdctl/issues/4322 + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + + // Create and start a container on this network + helpers.Ensure("run", "-d", "--name", data.Identifier("test-container"), + "--network", data.Identifier("test-network"), + testutil.CommonImage, "sleep", nerdtest.Infinity) + + // Get container ID for later use + containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n") + data.Labels().Set("containerID", containerID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("test-container")) + helpers.Anyhow("network", "remove", data.Identifier("test-network")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("test-network")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Network + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output") + assert.Equal(t, 1, len(dc), "Expected exactly one network") + + network := dc[0] + assert.Equal(t, network.Name, data.Identifier("test-network")) + assert.Equal(t, 1, len(network.Containers), "Expected exactly one container") + + // Get the container details + containerID := data.Labels().Get("containerID") + container := network.Containers[containerID] + + // Test container name + assert.Equal(t, container.Name, data.Identifier("test-container")) + + // Windows InspectNetNS is not implemented + if runtime.GOOS != "windows" { + // Verify IPv4Address is not empty and has CIDR notation + assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty") + assert.Assert(t, strings.Contains(container.IPv4Address, "/"), "IPv4Address should contain CIDR notation with /") + + // Verify IPv4Address is within the network's subnet + if len(network.IPAM.Config) > 0 && network.IPAM.Config[0].Subnet != "" { + _, subnet, err := net.ParseCIDR(network.IPAM.Config[0].Subnet) + assert.NilError(t, err, "Failed to parse network subnet") + + containerIP, _, err := net.ParseCIDR(container.IPv4Address) + assert.NilError(t, err, "Failed to parse container IPv4Address") + assert.Assert(t, subnet.Contains(containerIP), "IPv4Address should be within the network's subnet") + } + + // Test MacAddress is present and has valid format + assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty") + + // Test IPv6Address is empty for IPv4-only network + assert.Equal(t, "", container.IPv6Address, "IPv6Address should be empty for IPv4-only network") + } + }, + } + }, + }, + } + + testCase.Run(t) +} + +func TestNetworkInspectDualStack(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "Test dual-stack network with both IPv4 and IPv6", + Require: require.Not(require.Windows), // NetNS not implemented on Windows + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", + "--ipv6", + "--subnet", "10.1.0.0/24", + "--subnet", "fd00::/64", + data.Identifier("test-dual-stack")) + + // See https://github.com/containerd/nerdctl/issues/4322 + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + + // Create and start a container on this dual-stack network + helpers.Ensure("run", "-d", + "--name", data.Identifier("test-container"), + "--network", data.Identifier("test-dual-stack"), + testutil.CommonImage, "sleep", nerdtest.Infinity) + + // Get container ID for later use + containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n") + data.Labels().Set("containerID", containerID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("test-container")) + helpers.Anyhow("network", "remove", data.Identifier("test-dual-stack")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("test-dual-stack")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Network + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output") + assert.Equal(t, 1, len(dc), "Expected exactly one network") + + network := dc[0] + assert.Equal(t, network.Name, data.Identifier("test-dual-stack")) + assert.Equal(t, 2, len(network.IPAM.Config), "Expected two subnets (IPv4 and IPv6)") + + // Get the container details + containerID := data.Labels().Get("containerID") + container := network.Containers[containerID] + + // Test container name + assert.Equal(t, container.Name, data.Identifier("test-container")) + + // Parse both subnets + var ipv4Subnet, ipv6Subnet *net.IPNet + for _, config := range network.IPAM.Config { + if config.Subnet != "" { + _, subnet, err := net.ParseCIDR(config.Subnet) + assert.NilError(t, err, "Failed to parse subnet") + if subnet.IP.To4() != nil { + ipv4Subnet = subnet + } else { + ipv6Subnet = subnet + } + } + } + + // Verify IPv4 address is present and within subnet + assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty in dual-stack network") + ipv4, _, err := net.ParseCIDR(container.IPv4Address) + assert.NilError(t, err, "Failed to parse IPv4Address") + if ipv4Subnet != nil { + assert.Assert(t, ipv4Subnet.Contains(ipv4), "IPv4 address should be within the IPv4 subnet") + } + + // Verify IPv6 address is present and within subnet + assert.Assert(t, container.IPv6Address != "", "IPv6Address should not be empty in dual-stack network") + ipv6, _, err := net.ParseCIDR(container.IPv6Address) + assert.NilError(t, err, "Failed to parse IPv6Address") + if ipv6Subnet != nil { + assert.Assert(t, ipv6Subnet.Contains(ipv6), "IPv6 address should be within the IPv6 subnet") + } + + // Verify MAC address is present + assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty") + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 5ebfb0c2980..d684c1feced 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -929,9 +929,9 @@ type Network struct { type EndpointResource struct { Name string `json:"Name"` // EndpointID string `json:"EndpointID"` - // MacAddress string `json:"MacAddress"` - // IPv4Address string `json:"IPv4Address"` - // IPv6Address string `json:"IPv6Address"` + MacAddress string `json:"MacAddress"` + IPv4Address string `json:"IPv4Address"` + IPv6Address string `json:"IPv6Address"` } type structuredCNI struct { @@ -949,6 +949,92 @@ type MemorySetting struct { DisableOOMKiller bool `json:"disableOOMKiller"` } +// parseNetworkSubnets extracts and parses subnet configurations from IPAM config +func parseNetworkSubnets(ipamConfigs []IPAMConfig) []*net.IPNet { + var subnets []*net.IPNet + for _, config := range ipamConfigs { + if config.Subnet != "" { + _, subnet, err := net.ParseCIDR(config.Subnet) + if err != nil { + log.L.WithError(err).Warnf("failed to parse subnet %q", config.Subnet) + continue + } + subnets = append(subnets, subnet) + } + } + return subnets +} + +// isUsableInterface checks if a network interface is usable (not loopback and interface is up) +func isUsableInterface(iface *native.NetInterface) bool { + return iface.Interface.Flags&net.FlagLoopback == 0 && + iface.Interface.Flags&net.FlagUp != 0 +} + +// setIPAddresses assigns IPv4 or IPv6 addresses from CIDR notation to the endpoint +func setIPAddresses(endpoint *EndpointResource, cidr string) { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return + } + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return + } + + if ip.To4() != nil { + endpoint.IPv4Address = cidr + } else if ip.To16() != nil { + endpoint.IPv6Address = cidr + } +} + +// matchInterfaceToSubnets tries to match an interface to network subnets +func matchInterfaceToSubnets(endpoint *EndpointResource, iface *native.NetInterface, subnets []*net.IPNet) bool { + matched := false + for _, addr := range iface.Addrs { + ip, _, err := net.ParseCIDR(addr) + if err != nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() { + continue + } + + for _, subnet := range subnets { + if subnet.Contains(ip) { + if !matched { + endpoint.MacAddress = iface.HardwareAddr + matched = true + } + setIPAddresses(endpoint, addr) + break // Break inner loop, continue checking other addresses + } + } + } + return matched +} + +// populateEndpointFromNetNS finds and populates endpoint info from network namespace interfaces +func populateEndpointFromNetNS(endpoint *EndpointResource, interfaces []native.NetInterface, subnets []*net.IPNet) { + for _, iface := range interfaces { + if !isUsableInterface(&iface) { + continue + } + + if len(subnets) > 0 { + if matchInterfaceToSubnets(endpoint, &iface, subnets) { + return // Found matching interface + } + // Continue to next interface if this one doesn't match any subnets + continue + } + + // Fallback: use first usable interface (for networks without explicit subnets) + endpoint.MacAddress = iface.HardwareAddr + for _, addr := range iface.Addrs { + setIPAddresses(endpoint, addr) + } + return + } +} + func NetworkFromNative(n *native.Network) (*Network, error) { var res Network @@ -973,15 +1059,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) { res.Labels = *n.NerdctlLabels } + // Parse network subnets for interface matching + networkSubnets := parseNetworkSubnets(res.IPAM.Config) + res.Containers = make(map[string]EndpointResource) for _, container := range n.Containers { - res.Containers[container.ID] = EndpointResource{ + endpoint := EndpointResource{ Name: container.Labels[labels.Name], - // EndpointID: container.EndpointID, - // MacAddress: container.MacAddress, - // IPv4Address: container.IPv4Address, - // IPv6Address: container.IPv6Address, } + + if container.Process != nil && container.Process.NetNS != nil { + populateEndpointFromNetNS(&endpoint, container.Process.NetNS.Interfaces, networkSubnets) + } + + res.Containers[container.ID] = endpoint } return &res, nil From ee12ed93f1236cfa1865443c7d7b244da541f768 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:32:46 +0000 Subject: [PATCH 447/868] build(deps): bump github.com/containernetworking/plugins Bumps [github.com/containernetworking/plugins](https://github.com/containernetworking/plugins) from 1.9.0 to 1.9.1. - [Release notes](https://github.com/containernetworking/plugins/releases) - [Commits](https://github.com/containernetworking/plugins/compare/v1.9.0...v1.9.1) --- updated-dependencies: - dependency-name: github.com/containernetworking/plugins dependency-version: 1.9.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 78cb53cb05f..9ab03bb954d 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/containerd/stargz-snapshotter/ipfs v0.18.1 //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.2.3 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined - github.com/containernetworking/plugins v1.9.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 diff --git a/go.sum b/go.sum index 171bedceea7..ca993ef642f 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= 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.0 h1:Mg3SXBdRGkdXyFC4lcwr6u2ZB2SDeL6LC3U+QrEANuQ= -github.com/containernetworking/plugins v1.9.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= +github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= +github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= From 667c894cb096a2c1096595c55a8e2080b4a37bbb Mon Sep 17 00:00:00 2001 From: juannio Date: Mon, 16 Mar 2026 17:31:57 -0600 Subject: [PATCH 448/868] test: refactor compose_pull_linux_test.go to use nerdtest/tigron Signed-off-by: juannio --- .../compose/compose_pull_linux_test.go | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/cmd/nerdctl/compose/compose_pull_linux_test.go b/cmd/nerdctl/compose/compose_pull_linux_test.go index e0c79325326..3ba14eaf811 100644 --- a/cmd/nerdctl/compose/compose_pull_linux_test.go +++ b/cmd/nerdctl/compose/compose_pull_linux_test.go @@ -18,14 +18,21 @@ package compose import ( "fmt" + "path/filepath" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposePullWithService(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var composeYAML = fmt.Sprintf(` services: wordpress: @@ -53,10 +60,24 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + data.Labels().Set("composeYAML", composePath) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "pull", "db") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.DoesNotContain("wordpress"), + } + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "db").AssertOutNotContains("wordpress") + testCase.Run(t) } From cb88576c6f13e9dd8e352f386f9bbef8a7ef750c Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:01:36 -0700 Subject: [PATCH 449/868] Makefile: add uninstall target Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Makefile b/Makefile index ae9d04de5d6..1deca854a00 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,7 @@ help: @echo " * 'test' - Run basic unit testing." @echo " * 'binaries' - Build nerdctl." @echo " * 'install' - Install binaries to system locations." + @echo " * 'uninstall' - Remove installed binaries and documentation." @echo " * 'clean' - Clean artifacts." ########################## @@ -118,6 +119,14 @@ install: install -D -m 644 -t $(DESTDIR)$(DOCDIR)/nerdctl $(MAKEFILE_DIR)/docs/*.md $(call footer, $@) +uninstall: + $(call title, $@) + rm -f $(DESTDIR)$(BINDIR)/$(BINARY) + rm -f $(DESTDIR)$(BINDIR)/containerd-rootless.sh + rm -f $(DESTDIR)$(BINDIR)/containerd-rootless-setuptool.sh + rm -rf $(DESTDIR)$(DOCDIR)/nerdctl + $(call footer, $@) + clean: $(call title, $@) find . -name \*~ -delete @@ -308,6 +317,7 @@ artifacts: clean help \ binaries \ install \ + uninstall \ clean \ lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses lint-licenses-all \ fix-go fix-go-all fix-mod \ From 62a731447b8f2984765d2be82e956d3e85d431ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:19:22 +0000 Subject: [PATCH 450/868] build(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.78.0 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 11 ++++++----- go.sum | 30 ++++++++++++++++-------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 78cb53cb05f..ad35c52e11e 100644 --- a/go.mod +++ b/go.mod @@ -130,15 +130,15 @@ require ( 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.60.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect //gomodjail:unconfined - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.3 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -149,6 +149,7 @@ require ( require ( cyphar.com/go-pathrs v0.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/moby/moby/api v1.52.0 // indirect github.com/moby/moby/client v0.1.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect diff --git a/go.sum b/go.sum index 171bedceea7..76db0063e25 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boS github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -331,16 +333,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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= @@ -481,15 +483,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-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 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.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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= From 68bdc22c3c59742b6f9693039c9e16f0175b3be8 Mon Sep 17 00:00:00 2001 From: Kay Yan Date: Thu, 19 Mar 2026 13:29:19 +0000 Subject: [PATCH 451/868] fix: allow binding containers on different IPs to the same port Fixes #4786 Signed-off-by: Kay Yan Made-with: Cursor --- pkg/portutil/iptable/iptables.go | 50 +++++++++++++++++-------- pkg/portutil/iptable/iptables_linux.go | 41 +++++++++++++++++--- pkg/portutil/iptable/iptables_test.go | 52 ++++++++++++++++++++++---- pkg/portutil/port_allocate_linux.go | 15 ++++++-- 4 files changed, 127 insertions(+), 31 deletions(-) diff --git a/pkg/portutil/iptable/iptables.go b/pkg/portutil/iptable/iptables.go index 2c5daf01cef..aefea4d2cb6 100644 --- a/pkg/portutil/iptable/iptables.go +++ b/pkg/portutil/iptable/iptables.go @@ -22,26 +22,46 @@ import ( "strings" ) -// ParseIPTableRules takes a slice of iptables rules as input and returns a slice of -// uint64 containing the parsed destination port numbers from the rules. -func ParseIPTableRules(rules []string) []uint64 { - ports := []uint64{} +type PortRule struct { + IP string + Port uint64 +} + +// ParseIPTableRules takes a slice of iptables rules as input and returns a +// slice of PortRule containing the parsed destination IP and port from the +// rules. When a rule has no -d flag, IP is empty (meaning the rule applies to +// all addresses). +func ParseIPTableRules(rules []string) []PortRule { + portRules := []PortRule{} - // Regex to match the '--dports' option followed by the port number - dportRegex := regexp.MustCompile(`--dports ((,?\d+)+)`) + dportsRegex := regexp.MustCompile(`--dports ((,?\d+)+)`) + dportRegex := regexp.MustCompile(`--dport (\d+)`) + destRegex := regexp.MustCompile(`-d (\S+?)(?:/\d+)?\s`) for _, rule := range rules { - matches := dportRegex.FindStringSubmatch(rule) - if len(matches) > 1 { - for _, _match := range strings.Split(matches[1], ",") { - port64, err := strconv.ParseUint(_match, 10, 16) - if err != nil { - continue - } - ports = append(ports, port64) + var ports []string + + if matches := dportsRegex.FindStringSubmatch(rule); len(matches) > 1 { + ports = strings.Split(matches[1], ",") + } else if matches := dportRegex.FindStringSubmatch(rule); len(matches) > 1 { + ports = []string{matches[1]} + } else { + continue + } + + var ip string + if destMatches := destRegex.FindStringSubmatch(rule); len(destMatches) > 1 { + ip = destMatches[1] + } + + for _, portStr := range ports { + port64, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + continue } + portRules = append(portRules, PortRule{IP: ip, Port: port64}) } } - return ports + return portRules } diff --git a/pkg/portutil/iptable/iptables_linux.go b/pkg/portutil/iptable/iptables_linux.go index 45f3728d335..1fdc4481575 100644 --- a/pkg/portutil/iptable/iptables_linux.go +++ b/pkg/portutil/iptable/iptables_linux.go @@ -17,26 +17,57 @@ package iptable import ( + "strings" + "github.com/coreos/go-iptables/iptables" ) // Chain used for port forwarding rules: https://www.cni.dev/plugins/current/meta/portmap/#dnat const cniDnatChain = "CNI-HOSTPORT-DNAT" +// cniDNChainPrefix is the prefix for per-container DNAT sub-chains created by +// the CNI portmap plugin. These sub-chains contain the actual DNAT rules with +// destination IP filtering (e.g. -d 192.168.1.141/32 --dport 80 -j DNAT). +const cniDNChainPrefix = "CNI-DN-" + func ReadIPTables(table string) ([]string, error) { ipt, err := iptables.New() if err != nil { return nil, err } - var rules []string chainExists, _ := ipt.ChainExists(table, cniDnatChain) - if chainExists { - rules, err = ipt.List(table, cniDnatChain) - if err != nil { - return nil, err + if !chainExists { + return nil, nil + } + + parentRules, err := ipt.List(table, cniDnatChain) + if err != nil { + return nil, err + } + + // Read per-container DNAT sub-chains (CNI-DN-*) which contain the actual + // DNAT rules with both destination IP and port information. + // The parent chain only dispatches by port and does not include destination IP. + var rules []string + for _, rule := range parentRules { + fields := strings.Fields(rule) + for i, f := range fields { + if f == "-j" && i+1 < len(fields) && strings.HasPrefix(fields[i+1], cniDNChainPrefix) { + subRules, err := ipt.List(table, fields[i+1]) + if err != nil { + break + } + rules = append(rules, subRules...) + break + } } } + // Fall back to parent chain rules if no sub-chain rules were found. + if len(rules) == 0 { + rules = parentRules + } + return rules, nil } diff --git a/pkg/portutil/iptable/iptables_test.go b/pkg/portutil/iptable/iptables_test.go index 92a55662386..6b999fdc933 100644 --- a/pkg/portutil/iptable/iptables_test.go +++ b/pkg/portutil/iptable/iptables_test.go @@ -24,19 +24,19 @@ func TestParseIPTableRules(t *testing.T) { testCases := []struct { name string rules []string - want []uint64 + want []PortRule }{ { name: "Empty input", rules: []string{}, - want: []uint64{}, + want: []PortRule{}, }, { name: "Single rule with single port", rules: []string{ "-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080 -j CNI-DN-some-hash", }, - want: []uint64{8080}, + want: []PortRule{{IP: "", Port: 8080}}, }, { name: "Multiple rules with multiple ports", @@ -44,7 +44,45 @@ func TestParseIPTableRules(t *testing.T) { "-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080 -j CNI-DN-some-hash", "-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 9090 -j CNI-DN-some-hash", }, - want: []uint64{8080, 9090}, + want: []PortRule{ + {IP: "", Port: 8080}, + {IP: "", Port: 9090}, + }, + }, + { + name: "Single rule with comma-separated ports", + rules: []string{ + "-A CNI-HOSTPORT-DNAT -p tcp -m comment --comment \"dnat name: \"bridge\" id: \"some-id\"\" -m multiport --dports 8080,9090 -j CNI-DN-some-hash", + }, + want: []PortRule{ + {IP: "", Port: 8080}, + {IP: "", Port: 9090}, + }, + }, + { + name: "Sub-chain DNAT rule with destination IP", + rules: []string{ + "-A CNI-DN-some-hash -d 192.168.1.141/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.2:80", + }, + want: []PortRule{{IP: "192.168.1.141", Port: 80}}, + }, + { + name: "Multiple sub-chain rules with different IPs same port", + rules: []string{ + "-A CNI-DN-hash1 -d 192.168.1.141/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.2:80", + "-A CNI-DN-hash2 -d 192.168.1.142/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.4.0.3:80", + }, + want: []PortRule{ + {IP: "192.168.1.141", Port: 80}, + {IP: "192.168.1.142", Port: 80}, + }, + }, + { + name: "Sub-chain rule without CIDR suffix", + rules: []string{ + "-A CNI-DN-hash1 -d 10.0.0.1 -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.4.0.2:443", + }, + want: []PortRule{{IP: "10.0.0.1", Port: 443}}, }, } @@ -58,12 +96,12 @@ func TestParseIPTableRules(t *testing.T) { } } -func equal(a, b []uint64) bool { +func equal(a, b []PortRule) bool { if len(a) != len(b) { return false } - for i, v := range a { - if v != b[i] { + for i := range a { + if a[i] != b[i] { return false } } diff --git a/pkg/portutil/port_allocate_linux.go b/pkg/portutil/port_allocate_linux.go index 5e2a5956b90..ddfee7bf9b5 100644 --- a/pkg/portutil/port_allocate_linux.go +++ b/pkg/portutil/port_allocate_linux.go @@ -18,6 +18,7 @@ package portutil import ( "fmt" + "net" "github.com/containerd/nerdctl/v2/pkg/portutil/iptable" "github.com/containerd/nerdctl/v2/pkg/portutil/procnet" @@ -123,10 +124,16 @@ func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) { if err != nil { return nil, err } - destinationPorts := iptable.ParseIPTableRules(ipTableItems) - - for _, port := range destinationPorts { - usedPort[port] = true + portRules := iptable.ParseIPTableRules(ipTableItems) + + requestedIP := net.ParseIP(ip) + requestedIsWildcard := ip == "" || requestedIP.IsUnspecified() + for _, rule := range portRules { + ruleIP := net.ParseIP(rule.IP) + ruleIsWildcard := rule.IP == "" || ruleIP.IsUnspecified() + if requestedIsWildcard || ruleIsWildcard || (requestedIP != nil && ruleIP != nil && requestedIP.Equal(ruleIP)) { + usedPort[rule.Port] = true + } } return usedPort, nil From 46ca11d7badde52b849280af5267827de0815eac Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 23 Mar 2026 01:22:32 +0900 Subject: [PATCH 452/868] test: remove a new line from containerID for correct test execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running `TestContainerRmIptables` with verbose output, the following` error can be observed: ``` $ sudo go test -run TestContainerRmIptables -v ... === CONT TestContainerRmIptables/Test_iptables_rules_are_cleared_after_container_deletion container_remove_linux_test.go:135: ... container_remove_linux_test.go:49: +------------------------------------------------------------------------------------------------------------+ | ➡️ | ⚙️ /usr/local/bin/nerdctl rm -f fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504 | | | | +------------------------------------------------------------------------------------------------------------+ | | 🟠 time="2026-03-23T00:00:53+09:00" level=error msg="1 errors:\nfilters: parse error: [labels.\" | | | nerdctl/name\"==fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504 >|\n|< ]: unexp | | | ected input: \n: invalid argument: invalid argument" | ... container_remove_linux_test.go:135: <<<<<<<<<<<<<<<<<<<< 🖊️ Inspecting output (does not contain) 👀 testing: `-P PREROUTING ACCEPT -P INPUT ACCEPT ... -A POSTROUTING -s 10.4.0.148/32 -m comment --comment "name: \"bridge\" id: \"nerdctl-test-fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504\"" -j CNI-d4bd64738075587aa8d84afc ... -A CNI-HOSTPORT-DNAT -p tcp -m comment --comment "dnat name: \"bridge\" id: \"nerdctl-test-fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504\"" -m multiport --dports 5000 -j CNI-DN-d4bd64738075587aa8d84 ... -A CNI-d4bd64738075587aa8d84afc -d 10.4.0.0/24 -m comment --comment "name: \"bridge\" id: \"nerdctl-test-fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504\"" -j ACCEPT -A CNI-d4bd64738075587aa8d84afc ! -d 224.0.0.0/4 -m comment --comment "name: \"bridge\" id: \"nerdctl-test-fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504\"" -j MASQUERADE ... -P POSTROUTING ACCEPT ` ✅️ does verify: ! ~= `fd4fc99e04476d7f673133a06338933aadc547d4eef883f469464d7d31467504 ` >>>>>>>>>>>>>>>>>>>> ... === NAME TestContainerRmIptables container_remove_linux_test.go:135: +============================================================================================================+ | 🧽 | "TestContainerRmIptables": post-cleanup | +============================================================================================================+ --- PASS: TestContainerRmIptables (0.00s) --- PASS: TestContainerRmIptables/Test_iptables_rules_are_cleared_after_container_deletion (2.93s) PASS ok github.com/containerd/nerdctl/v2/cmd/nerdctl/container 2.936s ``` The error `unexpected input: \n: invalid argument: invalid argument` indicates that the container is not being removed correctly. Additionally, since the container ID remains in the iptables output, it is clear that `expect.DoesNotContain(containerID)` passes for the wrong reason, and the test is not properly validating the iptables cleanup behavior after container removal. This commit removes the new line from the container ID using `strings.TrimSpace`. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/container/container_remove_linux_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go index 53bf4928242..cda9aa1b8b9 100644 --- a/cmd/nerdctl/container/container_remove_linux_test.go +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -19,6 +19,7 @@ package container import ( "fmt" "strconv" + "strings" "testing" "time" @@ -93,7 +94,7 @@ func TestContainerRmIptables(t *testing.T) { // Create a container with port mapping to ensure iptables rules are created containerID := helpers.Capture("run", "-d", "--name", data.Identifier(), "-p", fmt.Sprintf("%d:80", port), testutil.NginxAlpineImage) - data.Labels().Set("containerID", containerID) + data.Labels().Set("containerID", strings.TrimSpace(containerID)) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { From 464bd57d6c818d454369c53c2f067f08c317870d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 26 Mar 2026 20:04:38 +0100 Subject: [PATCH 453/868] update BuildKit (0.28.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 diff --git a/Dockerfile b/Dockerfile index 1ba5604cbc9..05bde6568c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.9.0@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.26.3@BINARY +ARG BUILDKIT_VERSION=v0.28.1@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 deleted file mode 100644 index 79bad2db0fe..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.26.3 +++ /dev/null @@ -1,2 +0,0 @@ -249ae16ba4be59fadb51a49ff4d632bbf37200e2b6e187fa8574f0f1bce8166b buildkit-v0.26.3.linux-amd64.tar.gz -a98829f1b1b9ec596eb424dd03f03b9c7b596edac83e6700adf83ba0cb0d5f80 buildkit-v0.26.3.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 new file mode 100644 index 00000000000..92ee556055e --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 @@ -0,0 +1,2 @@ +2e84057b941488c54575aee57015681f08c71042bd20c739b23879dfad3f2449 buildkit-v0.28.1.linux-amd64.tar.gz +40caf32b10cb0766f7440a53fea9cfe15c8116d64f9365c53ff417958a136332 buildkit-v0.28.1.linux-arm64.tar.gz From 4979272c61e5bc47a9c77f3d9b800b8f5cae65d7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 26 Mar 2026 20:14:33 +0100 Subject: [PATCH 454/868] update CNI plugins (1.9.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 | 2 -- Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.1 | 2 ++ hack/provisioning/kube/kind.sh | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.1 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 9ed3d6b4e9b..31512b954b4 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -150,5 +150,5 @@ jobs: # Note: these as for amd64 containerd-sha: f5d8e90ecb6c1c7e33ecddf8cc268a93b9e5b54e0e850320d765511d76624f41 containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 - linux-cni-version: v1.9.0 - linux-cni-sha: 58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 + linux-cni-version: v1.9.1 + linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index 1ba5604cbc9..26e461d77af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 -ARG CNI_PLUGINS_VERSION=v1.9.0@BINARY +ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.26.3@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 deleted file mode 100644 index b23c10549fd..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.0 +++ /dev/null @@ -1,2 +0,0 @@ -58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 cni-plugins-linux-amd64-v1.9.0.tgz -2596ef56329dd1269026f46b8df262f09ba43c92dbfb940e1e69fbccccd30a29 cni-plugins-linux-arm64-v1.9.0.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.1 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.1 new file mode 100644 index 00000000000..da9b539a4e0 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.9.1 @@ -0,0 +1,2 @@ +b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 cni-plugins-linux-amd64-v1.9.1.tgz +56171987d3947707c3563db2f4001bccaf50fd63468611b9f3cbecb1375ee7ec cni-plugins-linux-arm64-v1.9.1.tgz diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index 3fd1aed52e3..ef1ad674c64 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -22,11 +22,11 @@ readonly root GO_VERSION=1.25 KIND_VERSION=v0.31.0 -CNI_PLUGINS_VERSION=v1.9.0 +CNI_PLUGINS_VERSION=v1.9.1 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_AMD64=58c03705426e929658f45a851df15a86d06ef680cacbf3f2dc127731ca265c28 +CNI_PLUGINS_SHA_AMD64=b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 # shellcheck disable=SC2034 -CNI_PLUGINS_SHA_ARM64=2596ef56329dd1269026f46b8df262f09ba43c92dbfb940e1e69fbccccd30a29 +CNI_PLUGINS_SHA_ARM64=56171987d3947707c3563db2f4001bccaf50fd63468611b9f3cbecb1375ee7ec [ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64 From 1a1493b5e913bec1c9fc7d26b3358a26a3efd18d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:22:42 +0000 Subject: [PATCH 455/868] build(deps): bump actions/cache from 5.0.3 to 5.0.4 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.3 to 5.0.4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/cdf6c1fa76f9f475f3d7449005a359c84ca0f306...668228422ae6a00e4ad889ee87cd7109ec5666a7) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index e94a0715856..63b41fdd514 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 198f475722e..a2b9dfc0ef2 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: /root/.vagrant.d key: vagrant From b34858e55d587baed5c0dcfd26974e61bd442938 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 29 Mar 2026 15:46:01 +0900 Subject: [PATCH 456/868] fix: clean up port-reserver.pid and the parent dir when the container is removed In the current implementation, when a container is started with the -p option, a `sleep infinity` process is launched to reserve host ports. The PID of this process is stored in `/run/nerdctl///port-reserver.pid`. When the container is removed, the following file and directory are expected to be cleaned up. - /run/nerdctl///port-reserver.pid - /run/nerdctl/// However, currently they are not removed as shown below: ``` $ sudo nerdctl run -d --name nginx -p 81:80 nginx ca1552d394cd8efcc5adcff9434deb4f3ead569d168f6e31bea1ae965db8b919 $ ls /run/nerdctl/default/ca1552d394cd8efcc5adcff9434deb4f3ead569d168f6e31bea1ae965db8b919 port-reserver.pid $ sudo nerdctl rm -f nginx nginx $ ls /run/nerdctl/default/ca1552d394cd8efcc5adcff9434deb4f3ead569d168f6e31bea1ae965db8b919 port-reserver.pid ``` Therefore, this commit ensures that they are removed when the container is removed. Signed-off-by: Hayato Kiwata --- pkg/ocihook/ocihook.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index e860f1b1a68..2aa14101c82 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -748,6 +748,9 @@ func onPostStop(opts *handlerOpts) error { if err = killProcessByPidFile(portReserverPidFile); err != nil { log.L.WithError(err).Errorf("failed to kill the port-reserver process") } + if err := os.RemoveAll(filepath.Dir(portReserverPidFile)); err != nil { + log.L.WithError(err).Errorf("failed to remove the port-reserver directory %s", filepath.Dir(portReserverPidFile)) + } return nil } From b1f7d47e6281f465d915255e4f927aeb3154b664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:54:16 +0000 Subject: [PATCH 457/868] build(deps): bump actions/setup-go from 6.3.0 to 6.4.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.3.0 to 6.4.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4b73464bb391d4059bd26b0524d20df3927bd417...4a3601121dd01d1626a1e23e37211e3254c1c06c) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 37993d49576..fcaee2c7273 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -52,7 +52,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 083c06458d3..6b26b2830d7 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -55,7 +55,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index d17da7e5518..0cef414e883 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -36,7 +36,7 @@ jobs: path: src/github.com/containerd/nerdctl - name: "Init: install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 60055343450..5319dd35474 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -96,7 +96,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index f2f9fbae799..5eb30e75a2f 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -63,7 +63,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index add48b491a4..9fcefc8dee7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: "Install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: "1.25" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 39ace93747c..bb36e14bf3f 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -46,7 +46,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From a5a40f4e11bb61ea34ab212f14d8fcb505a24855 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:54:26 +0000 Subject: [PATCH 458/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.2.1+incompatible to 29.3.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.2.1...v29.3.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.3.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f70aef2b12..6312e625c00 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.2.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.3.1+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index cb1931d34f9..6d247a86639 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,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.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= -github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.3.1+incompatible h1:M04FDj2TRehDacrosh7Vlkgc7AuQoWloQkf1PA5hmoI= +github.com/docker/cli v29.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From 1663bc3242be0549a5d731561729a4c1b8ff4c75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:54:46 +0000 Subject: [PATCH 459/868] build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.4 to 2.3.0 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.4 to 2.3.0. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.2.4...v2.3.0) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f70aef2b12..cbd579b2c7f 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ 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/pelletier/go-toml/v2 v2.2.4 + github.com/pelletier/go-toml/v2 v2.3.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v2 v2.3.6 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index cb1931d34f9..1874f25fc7e 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.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= From 1842ed1e7c1a79c18ff8940720a9a52d2a18e7b9 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 26 Mar 2026 20:23:11 +0100 Subject: [PATCH 460/868] update Kubo (0.40.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cbb346f98c2..7aa3e78692a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.3.9 ARG SOCI_SNAPSHOTTER_VERSION=0.12.1 -ARG KUBO_VERSION=v0.39.0 +ARG KUBO_VERSION=v0.40.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 1ae8b504295a30acc3e9c1599a57a953aea57716 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 26 Mar 2026 20:10:39 +0100 Subject: [PATCH 461/868] CI: update Go (1.26) Signed-off-by: Akihiro Suda --- .github/workflows/release.yml | 2 +- .github/workflows/workflow-lint.yml | 8 ++++---- .github/workflows/workflow-test.yml | 4 ++-- .github/workflows/workflow-tigron.yml | 2 +- Dockerfile | 2 +- hack/provisioning/kube/kind.sh | 2 +- pkg/testutil/images.yaml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fcefc8dee7..8c123fd4912 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: - name: "Install go" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: "1.25" + go-version: "1.26" check-latest: true - name: "Compile binaries" env: diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 26f9a321f59..6fe7e2bea2d 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -35,7 +35,7 @@ jobs: canary: true with: timeout: 10 - go-version: "1.25" + go-version: "1.26" runner: ubuntu-24.04 # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job # will not run. However, if you test it, it will coerce to `false`, hence: @@ -48,7 +48,7 @@ jobs: uses: ./.github/workflows/job-lint-project.yml with: timeout: 5 - go-version: "1.25" + go-version: "1.26" runner: ubuntu-24.04 # Lint for shell and yaml files @@ -68,10 +68,10 @@ jobs: matrix: include: # Build for both old and stable go - - go-version: "1.24" - go-version: "1.25" + - go-version: "1.26" # Additionally build for canary - - go-version: "1.25" + - go-version: "1.26" canary: true with: timeout: 10 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 31512b954b4..f42591d94f2 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -30,7 +30,7 @@ jobs: canary: ${{ matrix.canary && true || false }} # Windows routinely go over 5 minutes timeout: 10 - go-version: 1.25 + go-version: 1.26 windows-cni-version: v0.3.1 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 @@ -143,7 +143,7 @@ jobs: runner: ${{ matrix.runner }} binary: ${{ matrix.binary != '' && matrix.binary || 'nerdctl' }} canary: ${{ matrix.canary && true || false }} - go-version: 1.25 + go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble containerd-version: 2.2.1 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index bb36e14bf3f..3878cb12053 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -9,7 +9,7 @@ on: paths: 'mod/tigron/**' env: - GO_VERSION: "1.25" + GO_VERSION: "1.26" GOTOOLCHAIN: local jobs: diff --git a/Dockerfile b/Dockerfile index 7aa3e78692a..0aa3f541341 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash -ARG GO_VERSION=1.25 +ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index ef1ad674c64..bbdca38f2e5 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -20,7 +20,7 @@ readonly root # shellcheck source=/dev/null . "$root/../../scripts/lib.sh" -GO_VERSION=1.25 +GO_VERSION=1.26 KIND_VERSION=v0.31.0 CNI_PLUGINS_VERSION=v1.9.1 # shellcheck disable=SC2034 diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 089273231e7..4e51e332237 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -32,7 +32,7 @@ fluentd: golang: ref: "golang" - tag: "1.25.0-trixie" + tag: "1.26.1-trixie" kubo: ref: "ghcr.io/stargz-containers/ipfs/kubo" From 5cfce87e05adbc753054bd2f87c73fac869351a7 Mon Sep 17 00:00:00 2001 From: Joonsoo Won Date: Sun, 8 Mar 2026 19:52:06 +0900 Subject: [PATCH 462/868] test: refactor container_inspect_linux_test.go to use Tigron Signed-off-by: Joonsoo Won --- .../container/container_inspect_linux_test.go | 1082 +++++++++++------ 1 file changed, 685 insertions(+), 397 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 1c82925bf46..0c3f0423703 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -19,8 +19,8 @@ package container import ( "encoding/json" "fmt" - "os" "slices" + "strconv" "strings" "testing" @@ -29,162 +29,249 @@ import ( "github.com/containerd/continuity/testutil/loopback" "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/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/labels" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestContainerInspectContainsPortConfig(t *testing.T) { - testContainer := testutil.Identifier(t) + testCase := nerdtest.Setup() - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + _, err := portlock.Acquire(8080) + if err != nil { + t.Logf("Failed to acquire port: %v", err) + t.FailNow() + } + helpers.Ensure("run", "-d", "--name", data.Identifier(), "-p", "8080:80", testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + } - base.Cmd("run", "-d", "--name", testContainer, "-p", "8080:80", testutil.NginxAlpineImage).AssertOK() - inspect := base.InspectContainer(testContainer) - inspect80TCP := (*inspect.NetworkSettings.Ports)["80/tcp"] - expected := nat.PortBinding{ - HostIP: "0.0.0.0", - HostPort: "8080", + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + portlock.Release(8080) } - assert.Equal(base.T, expected, inspect80TCP[0]) + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect80TCP := (*dc[0].NetworkSettings.Ports)["80/tcp"] + expected := nat.PortBinding{ + HostIP: "0.0.0.0", + HostPort: "8080", + } + assert.Equal(tt, expected, inspect80TCP[0]) + }) + + testCase.Run(t) } func TestContainerInspectContainsMounts(t *testing.T) { - testContainer := testutil.Identifier(t) - - base := testutil.NewBase(t) - - testVolume := testutil.Identifier(t) - - defer base.Cmd("volume", "rm", "-f", testVolume).Run() - base.Cmd("volume", "create", "--label", "tag=testVolume", testVolume).AssertOK() - inspectVolume := base.InspectVolume(testVolume) - namedVolumeSource := inspectVolume.Mountpoint - - defer base.Cmd("rm", "-f", testContainer).Run() - base.Cmd("run", "-d", "--privileged", - "--name", testContainer, - "--network", "none", - "-v", "/anony-vol", - "--tmpfs", "/app1:size=64m", - "--mount", "type=bind,src=/tmp,dst=/app2,ro", - "--mount", fmt.Sprintf("type=volume,src=%s,dst=/app3,readonly=false", testVolume), - testutil.NginxAlpineImage).AssertOK() - - inspect := base.InspectContainer(testContainer) - // convert array to map to get by key of Destination - actual := make(map[string]dockercompat.MountPoint) - for i := range inspect.Mounts { - actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] - } - t.Logf("actual in TestContainerInspectContainsMounts: %+v", actual) - const localDriver = "local" - - expected := []struct { - dest string - mountPoint dockercompat.MountPoint - }{ - // anonymous volume - { - dest: "/anony-vol", - mountPoint: dockercompat.MountPoint{ - Type: "volume", - Name: "", - Source: "", // source of anonymous volume is a generated path, so here will not check it. - Destination: "/anony-vol", - Driver: localDriver, - RW: true, - }, - }, + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainer := data.Identifier() + testVolume := data.Identifier() + + helpers.Ensure("volume", "create", "--label", "tag=testVolume", testVolume) + inspectVolume := nerdtest.InspectVolume(helpers, testVolume) + namedVolumeSource := inspectVolume.Mountpoint + + helpers.Ensure("run", "-d", "--privileged", + "--name", testContainer, + "--network", "none", + "-v", "/anony-vol", + "--tmpfs", "/app1:size=64m", + "--mount", "type=bind,src=/tmp,dst=/app2,ro", + "--mount", fmt.Sprintf("type=volume,src=%s,dst=/app3,readonly=false", testVolume), + testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, testContainer) + + data.Labels().Set("namedVolumeSource", namedVolumeSource) + data.Labels().Set("testVolume", testVolume) + } - // bind - { - dest: "/app2", - mountPoint: dockercompat.MountPoint{ - Type: "bind", - Name: "", - Source: "/tmp", - Destination: "/app2", - Driver: "", - RW: false, - }, - }, + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", data.Identifier()) + } - // named volume - { - dest: "/app3", - mountPoint: dockercompat.MountPoint{ - Type: "volume", - Name: testVolume, - Source: namedVolumeSource, - Destination: "/app3", - Driver: localDriver, - RW: true, + 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: func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + // convert array to map to get by key of Destination + actual := make(map[string]dockercompat.MountPoint) + for i := range inspect.Mounts { + actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] + } + + t.Logf("actual in TestContainerInspectContainsMounts: %+v", actual) + const localDriver = "local" + + expected := []struct { + dest string + mountPoint dockercompat.MountPoint + }{ + // anonymous volume + { + dest: "/anony-vol", + mountPoint: dockercompat.MountPoint{ + Type: "volume", + Name: "", + Source: "", // source of anonymous volume is a generated path, so here will not check it. + Destination: "/anony-vol", + Driver: localDriver, + RW: true, + }, + }, + + // bind + { + dest: "/app2", + mountPoint: dockercompat.MountPoint{ + Type: "bind", + Name: "", + Source: "/tmp", + Destination: "/app2", + Driver: "", + RW: false, + }, + }, + + // named volume + { + dest: "/app3", + mountPoint: dockercompat.MountPoint{ + Type: "volume", + Name: data.Labels().Get("testVolume"), + Source: data.Labels().Get("namedVolumeSource"), + Destination: "/app3", + Driver: localDriver, + RW: true, + }, + }, + } + + for i := range expected { + mountCase := expected[i] + t.Logf("test volume[dest=%q]", mountCase.dest) + + mountPoint, ok := actual[mountCase.dest] + assert.Assert(tt, ok) + + assert.Equal(tt, mountCase.mountPoint.Type, mountPoint.Type) + assert.Equal(tt, mountCase.mountPoint.Driver, mountPoint.Driver) + assert.Equal(tt, mountCase.mountPoint.RW, mountPoint.RW) + assert.Equal(tt, mountCase.mountPoint.Destination, mountPoint.Destination) + + if mountCase.mountPoint.Source != "" { + assert.Equal(tt, mountCase.mountPoint.Source, mountPoint.Source) + } + if mountCase.mountPoint.Name != "" { + assert.Equal(tt, mountCase.mountPoint.Name, mountPoint.Name) + } + } }, - }, + } } - for i := range expected { - testCase := expected[i] - t.Logf("test volume[dest=%q]", testCase.dest) + testCase.Run(t) +} - mountPoint, ok := actual[testCase.dest] - assert.Assert(base.T, ok) +func TestContainerInspectContainsLabel(t *testing.T) { + testCase := nerdtest.Setup() - assert.Equal(base.T, testCase.mountPoint.Type, mountPoint.Type) - assert.Equal(base.T, testCase.mountPoint.Driver, mountPoint.Driver) - assert.Equal(base.T, testCase.mountPoint.RW, mountPoint.RW) - assert.Equal(base.T, testCase.mountPoint.Destination, mountPoint.Destination) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--label", "foo=foo", "--label", "bar=bar", testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + } - if testCase.mountPoint.Source != "" { - assert.Equal(base.T, testCase.mountPoint.Source, mountPoint.Source) - } - if testCase.mountPoint.Name != "" { - assert.Equal(base.T, testCase.mountPoint.Name, mountPoint.Name) - } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } -} -func TestContainerInspectContainsLabel(t *testing.T) { - t.Parallel() - testContainer := testutil.Identifier(t) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) - base.Cmd("run", "-d", "--name", testContainer, "--label", "foo=foo", "--label", "bar=bar", testutil.NginxAlpineImage).AssertOK() - base.EnsureContainerStarted(testContainer) - inspect := base.InspectContainer(testContainer) - lbs := inspect.Config.Labels + inspect := dc[0] + lbs := inspect.Config.Labels - assert.Equal(base.T, "foo", lbs["foo"]) - assert.Equal(base.T, "bar", lbs["bar"]) + assert.Equal(tt, "foo", lbs["foo"]) + assert.Equal(tt, "bar", lbs["bar"]) + }) + + testCase.Run(t) } func TestContainerInspectContainsInternalLabel(t *testing.T) { - testutil.DockerIncompatible(t) - t.Parallel() - testContainer := testutil.Identifier(t) - - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - base.Cmd("run", "-d", "--name", testContainer, "--mount", "type=bind,src=/tmp,dst=/app,readonly=false,bind-propagation=rprivate", testutil.NginxAlpineImage).AssertOK() - base.EnsureContainerStarted(testContainer) - inspect := base.InspectContainer(testContainer) - lbs := inspect.Config.Labels - - // TODO: add more internal labels testcases - labelMount := lbs[labels.Mounts] - expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]" - assert.Equal(base.T, expectedLabelMount, labelMount) + testCase := nerdtest.Setup() + + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--mount", "type=bind,src=/tmp,dst=/app,readonly=false,bind-propagation=rprivate", testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + lbs := inspect.Config.Labels + + // TODO: add more internal labels testcases + labelMount := lbs[labels.Mounts] + expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]" + assert.Equal(tt, expectedLabelMount, labelMount) + }) + + testCase.Run(t) } func TestContainerInspectConfigImage(t *testing.T) { @@ -193,7 +280,7 @@ func TestContainerInspectConfigImage(t *testing.T) { testCase := &test.Case{ Description: "Container inspect contains Config.Image field", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", "infinity") + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", nerdtest.Infinity) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) @@ -201,15 +288,15 @@ func TestContainerInspectConfigImage(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("inspect", data.Identifier()) }, - Expected: test.Expects(0, nil, func(stdout string, t tig.T) { + Expected: test.Expects(0, nil, func(stdout string, tt tig.T) { var containers []dockercompat.Container err := json.Unmarshal([]byte(stdout), &containers) - assert.NilError(t, err, "Unable to unmarshal output\n") - assert.Equal(t, 1, len(containers), "Expected exactly one container in inspect output") + assert.NilError(tt, err, "Unable to unmarshal output\n") + assert.Equal(tt, 1, len(containers), "Expected exactly one container in inspect output") container := containers[0] - assert.Assert(t, container.Config != nil, "container Config should not be nil") - assert.Assert(t, container.Config.Image != "", "Config.Image should not be empty") + assert.Assert(tt, container.Config != nil, "container Config should not be nil") + assert.Assert(tt, container.Config.Image != "", "Config.Image should not be empty") }), } @@ -217,344 +304,545 @@ func TestContainerInspectConfigImage(t *testing.T) { } func TestContainerInspectState(t *testing.T) { - t.Parallel() - testContainer := testutil.Identifier(t) - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - type testCase struct { - name, containerName, cmd string - want dockercompat.ContainerState - } // nerdctl: run error produces a nil Task, so the Status is empty because Status comes from Task. // docker : run error gives => `Status=created` as in docker there is no a separation between container and Task. - errStatus := "" - if nerdtest.IsDocker() { - errStatus = "created" - } - testCases := []testCase{ + testCase.SubTests = []*test.Case{ { - name: "inspect State with error", - containerName: fmt.Sprintf("%s-fail", testContainer), - cmd: "aa", - want: dockercompat.ContainerState{ - Error: "executable file not found in $PATH", - Status: errStatus, + Description: "docker inspect State with error", + Setup: func(data test.Data, helpers test.Helpers) { + testContainer := fmt.Sprintf("%s-fail", data.Identifier()) + helpers.Fail("run", "--name", testContainer, testutil.AlpineImage, "aa") + data.Labels().Set("testContainer", testContainer) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("testContainer")) }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("testContainer")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + expectedErrStatus := "" + if nerdtest.IsDocker() { + expectedErrStatus = "created" + } + 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) + }), }, { - name: "inspect State without error", - containerName: fmt.Sprintf("%s-success", testContainer), - cmd: "ls", - want: dockercompat.ContainerState{ - Error: "", - Status: "exited", + Description: "docker inspect State without error", + Setup: func(data test.Data, helpers test.Helpers) { + testContainer := fmt.Sprintf("%s-success", data.Identifier()) + helpers.Ensure("run", "--name", testContainer, testutil.AlpineImage, "ls") + data.Labels().Set("testContainer", testContainer) }, - }, - } + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("testContainer")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("testContainer")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - defer base.Cmd("rm", "-f", tc.containerName).Run() - if tc.want.Error != "" { - base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertFail() - } else { - base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertOK() - } - inspect := base.InspectContainer(tc.containerName) - assert.Assert(t, strings.Contains(inspect.State.Error, tc.want.Error), fmt.Sprintf("expected: %s, actual: %s", tc.want.Error, inspect.State.Error)) - assert.Equal(base.T, inspect.State.Status, tc.want.Status) - }) + 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) + }), + }, } + testCase.Run(t) } func TestContainerInspectHostConfig(t *testing.T) { - testContainer := testutil.Identifier(t) - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - // Run a container with various HostConfig options - base.Cmd("run", "-d", "--name", testContainer, - "--cpuset-cpus", "0-1", - "--cpuset-mems", "0", - "--cpu-shares", "1024", - "--cpu-quota", "100000", - "--group-add", "1000", - "--group-add", "2000", - "--add-host", "host1:10.0.0.1", - "--add-host", "host2:10.0.0.2", - "--ipc", "host", - "--memory", "512m", - "--read-only", - "--shm-size", "256m", - "--uts", "host", - "--runtime", "io.containerd.runc.v2", - testutil.AlpineImage, "sleep", "infinity").AssertOK() - - inspect := base.InspectContainer(testContainer) - - assert.Equal(t, "0-1", inspect.HostConfig.CPUSetCPUs) - assert.Equal(t, "0", inspect.HostConfig.CPUSetMems) - assert.Equal(t, uint64(1024), inspect.HostConfig.CPUShares) - assert.Equal(t, int64(100000), inspect.HostConfig.CPUQuota) - assert.Assert(t, slices.Contains(inspect.HostConfig.GroupAdd, "1000"), "Expected '1000' to be in GroupAdd") - assert.Assert(t, slices.Contains(inspect.HostConfig.GroupAdd, "2000"), "Expected '2000' to be in GroupAdd") - expectedExtraHosts := []string{"host1:10.0.0.1", "host2:10.0.0.2"} - assert.DeepEqual(t, expectedExtraHosts, inspect.HostConfig.ExtraHosts) - assert.Equal(t, "host", inspect.HostConfig.IpcMode) - assert.Equal(t, int64(536870912), inspect.HostConfig.Memory) - assert.Equal(t, int64(1073741824), inspect.HostConfig.MemorySwap) - assert.Equal(t, true, inspect.HostConfig.ReadonlyRootfs) - assert.Equal(t, "host", inspect.HostConfig.UTSMode) - assert.Equal(t, int64(268435456), inspect.HostConfig.ShmSize) + testCase := nerdtest.Setup() + + testCase.Require = require.Not( + // skip only if it's rootless AND cgroup v1 + require.All( + nerdtest.Rootless, + require.Not(nerdtest.CGroupV2), + ), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--cpuset-cpus", "0-1", + "--cpuset-mems", "0", + "--cpu-shares", "1024", + "--cpu-quota", "100000", + "--group-add", "1000", + "--group-add", "2000", + "--add-host", "host1:10.0.0.1", + "--add-host", "host2:10.0.0.2", + "--ipc", "host", + "--memory", "512m", + "--read-only", + "--shm-size", "256m", + "--uts", "host", + "--runtime", "io.containerd.runc.v2", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + + assert.Equal(tt, "0-1", inspect.HostConfig.CPUSetCPUs) + assert.Equal(tt, "0", inspect.HostConfig.CPUSetMems) + assert.Equal(tt, uint64(1024), inspect.HostConfig.CPUShares) + assert.Equal(tt, int64(100000), inspect.HostConfig.CPUQuota) + assert.Assert(tt, slices.Contains(inspect.HostConfig.GroupAdd, "1000"), "Expected '1000' to be in GroupAdd") + assert.Assert(tt, slices.Contains(inspect.HostConfig.GroupAdd, "2000"), "Expected '2000' to be in GroupAdd") + expectedExtraHosts := []string{"host1:10.0.0.1", "host2:10.0.0.2"} + assert.DeepEqual(tt, expectedExtraHosts, inspect.HostConfig.ExtraHosts) + assert.Equal(tt, "host", inspect.HostConfig.IpcMode) + assert.Equal(tt, int64(536870912), inspect.HostConfig.Memory) + assert.Equal(tt, int64(1073741824), inspect.HostConfig.MemorySwap) + assert.Equal(tt, true, inspect.HostConfig.ReadonlyRootfs) + assert.Equal(tt, "host", inspect.HostConfig.UTSMode) + assert.Equal(tt, int64(268435456), inspect.HostConfig.ShmSize) + }) + + testCase.Run(t) } func TestContainerInspectHostConfigDefaults(t *testing.T) { - testContainer := testutil.Identifier(t) - - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - var hc hostConfigValues - - // Hostconfig default values differ with Docker. - // This is because we directly retrieve the configured values instead of using preset defaults. - if nerdtest.IsDocker() { - hc.Driver = "" - hc.GroupAddSize = 0 - hc.ShmSize = int64(67108864) // Docker default 64M - hc.Runtime = "runc" - } else { - hc.GroupAddSize = 10 - hc.Driver = "json-file" - hc.ShmSize = int64(0) - hc.Runtime = "io.containerd.runc.v2" - } - - // Run a container without specifying HostConfig options - base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() - - inspect := base.InspectContainer(testContainer) - t.Logf("HostConfig in TestContainerInspectHostConfigDefaults: %+v", inspect.HostConfig) - assert.Equal(t, "", inspect.HostConfig.CPUSetCPUs) - assert.Equal(t, "", inspect.HostConfig.CPUSetMems) - assert.Equal(t, uint16(0), inspect.HostConfig.BlkioWeight) - assert.Equal(t, 0, len(inspect.HostConfig.BlkioWeightDevice)) - assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadBps)) - assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadIOps)) - assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteBps)) - assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteIOps)) - assert.Equal(t, uint64(0), inspect.HostConfig.CPUShares) - assert.Equal(t, int64(0), inspect.HostConfig.CPUQuota) - assert.Equal(t, hc.GroupAddSize, len(inspect.HostConfig.GroupAdd)) - assert.Equal(t, 0, len(inspect.HostConfig.ExtraHosts)) - assert.Equal(t, "private", inspect.HostConfig.IpcMode) - assert.Equal(t, hc.Driver, inspect.HostConfig.LogConfig.Driver) - assert.Equal(t, int64(0), inspect.HostConfig.Memory) - assert.Equal(t, int64(0), inspect.HostConfig.MemorySwap) - assert.Equal(t, bool(false), inspect.HostConfig.OomKillDisable) - assert.Equal(t, bool(false), inspect.HostConfig.ReadonlyRootfs) - assert.Equal(t, "", inspect.HostConfig.UTSMode) - assert.Equal(t, hc.ShmSize, inspect.HostConfig.ShmSize) - assert.Equal(t, hc.Runtime, inspect.HostConfig.Runtime) - assert.Equal(t, 0, len(inspect.HostConfig.Devices)) - // Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment. - got := len(inspect.HostConfig.Sysctls) - if got != 0 && got != 1 { - t.Fatalf("unexpected number of Sysctls entries: %d (want 0 or 1)", got) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var hc hostConfigValues + + // Hostconfig default values differ with Docker. + // This is because we directly retrieve the configured values instead of using preset defaults. + if nerdtest.IsDocker() { + hc.Driver = "" + hc.GroupAddSize = 0 + hc.ShmSize = int64(67108864) // Docker default 64M + hc.Runtime = "runc" + } else { + hc.GroupAddSize = 10 + hc.Driver = "json-file" + hc.ShmSize = int64(0) + hc.Runtime = "io.containerd.runc.v2" + } + + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + jsonHC, err := json.Marshal(hc) + assert.NilError(t, err) + data.Labels().Set("jsonHC", string(jsonHC)) } + + 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: func(stdout string, tt tig.T) { + var hc hostConfigValues + err := json.Unmarshal([]byte(data.Labels().Get("jsonHC")), &hc) + assert.NilError(tt, err) + + var dc []dockercompat.Container + + err = json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + t.Logf("HostConfig in TestContainerInspectHostConfigDefaults: %+v", inspect.HostConfig) + assert.Equal(tt, "", inspect.HostConfig.CPUSetCPUs) + assert.Equal(tt, "", inspect.HostConfig.CPUSetMems) + assert.Equal(tt, uint16(0), inspect.HostConfig.BlkioWeight) + assert.Equal(tt, 0, len(inspect.HostConfig.BlkioWeightDevice)) + assert.Equal(tt, 0, len(inspect.HostConfig.BlkioDeviceReadBps)) + assert.Equal(tt, 0, len(inspect.HostConfig.BlkioDeviceReadIOps)) + assert.Equal(tt, 0, len(inspect.HostConfig.BlkioDeviceWriteBps)) + assert.Equal(tt, 0, len(inspect.HostConfig.BlkioDeviceWriteIOps)) + assert.Equal(tt, uint64(0), inspect.HostConfig.CPUShares) + assert.Equal(tt, int64(0), inspect.HostConfig.CPUQuota) + assert.Equal(tt, hc.GroupAddSize, len(inspect.HostConfig.GroupAdd)) + assert.Equal(tt, 0, len(inspect.HostConfig.ExtraHosts)) + assert.Equal(tt, "private", inspect.HostConfig.IpcMode) + assert.Equal(tt, hc.Driver, inspect.HostConfig.LogConfig.Driver) + assert.Equal(tt, int64(0), inspect.HostConfig.Memory) + assert.Equal(tt, int64(0), inspect.HostConfig.MemorySwap) + assert.Equal(tt, bool(false), inspect.HostConfig.OomKillDisable) + assert.Equal(tt, bool(false), inspect.HostConfig.ReadonlyRootfs) + assert.Equal(tt, "", inspect.HostConfig.UTSMode) + assert.Equal(tt, hc.ShmSize, inspect.HostConfig.ShmSize) + assert.Equal(tt, hc.Runtime, inspect.HostConfig.Runtime) + assert.Equal(tt, 0, len(inspect.HostConfig.Devices)) + + // Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment. + got := len(inspect.HostConfig.Sysctls) + if got != 0 && got != 1 { + t.Fatalf("unexpected number of Sysctls entries: %d (want 0 or 1)", got) + } + }, + } + } + + testCase.Run(t) } func TestContainerInspectHostConfigDNS(t *testing.T) { - testContainer := testutil.Identifier(t) - - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() - - // Run a container with DNS options - base.Cmd("run", "-d", "--name", testContainer, - "--dns", "8.8.8.8", - "--dns", "1.1.1.1", - "--dns-search", "example.com", - "--dns-search", "test.local", - "--dns-option", "ndots:5", - "--dns-option", "timeout:3", - testutil.AlpineImage, "sleep", "infinity").AssertOK() - - inspect := base.InspectContainer(testContainer) - - // Check DNS servers - expectedDNSServers := []string{"8.8.8.8", "1.1.1.1"} - assert.DeepEqual(t, expectedDNSServers, inspect.HostConfig.DNS) - - // Check DNS search domains - expectedDNSSearch := []string{"example.com", "test.local"} - assert.DeepEqual(t, expectedDNSSearch, inspect.HostConfig.DNSSearch) - - // Check DNS options - expectedDNSOptions := []string{"ndots:5", "timeout:3"} - assert.DeepEqual(t, expectedDNSOptions, inspect.HostConfig.DNSOptions) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--dns", "8.8.8.8", + "--dns", "1.1.1.1", + "--dns-search", "example.com", + "--dns-search", "test.local", + "--dns-option", "ndots:5", + "--dns-option", "timeout:3", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + // Check DNS servers + expectedDNSServers := []string{"8.8.8.8", "1.1.1.1"} + assert.DeepEqual(tt, expectedDNSServers, inspect.HostConfig.DNS) + + // Check DNS search domains + expectedDNSSearch := []string{"example.com", "test.local"} + assert.DeepEqual(tt, expectedDNSSearch, inspect.HostConfig.DNSSearch) + + // Check DNS options + expectedDNSOptions := []string{"ndots:5", "timeout:3"} + assert.DeepEqual(tt, expectedDNSOptions, inspect.HostConfig.DNSOptions) + }) + + testCase.Run(t) } func TestContainerInspectHostConfigDNSDefaults(t *testing.T) { - testContainer := testutil.Identifier(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) - // Run a container without specifying DNS options - base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() + inspect := dc[0] - inspect := base.InspectContainer(testContainer) + // Check that DNS settings are empty by default + assert.Equal(tt, 0, len(inspect.HostConfig.DNS)) + assert.Equal(tt, 0, len(inspect.HostConfig.DNSSearch)) + assert.Equal(tt, 0, len(inspect.HostConfig.DNSOptions)) + }) - // Check that DNS settings are empty by default - assert.Equal(t, 0, len(inspect.HostConfig.DNS)) - assert.Equal(t, 0, len(inspect.HostConfig.DNSSearch)) - assert.Equal(t, 0, len(inspect.HostConfig.DNSOptions)) + testCase.Run(t) } func TestContainerInspectHostConfigPID(t *testing.T) { - testContainer1 := testutil.Identifier(t) + "-container1" - testContainer2 := testutil.Identifier(t) + "-container2" + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testContainer1 := data.Identifier() + "-container1" + testContainer2 := data.Identifier() + "-container2" - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer1, testContainer2).Run() + // Run the first container + helpers.Ensure("run", "-d", "--name", testContainer1, testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testContainer1) - // Run the first container - base.Cmd("run", "-d", "--name", testContainer1, testutil.AlpineImage, "sleep", "infinity").AssertOK() + containerID1 := strings.TrimSpace(helpers.Capture("inspect", "-f", "{{.Id}}", testContainer1)) - containerID1 := strings.TrimSpace(base.Cmd("inspect", "-f", "{{.Id}}", testContainer1).Out()) + var pidMode string + if nerdtest.IsDocker() { + pidMode = "container:" + containerID1 + } else { + pidMode = containerID1 + } - var hc hostConfigValues + helpers.Ensure("run", "-d", "--name", testContainer2, "--pid", fmt.Sprintf("container:%s", testContainer1), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, testContainer2) - if nerdtest.IsDocker() { - hc.PidMode = "container:" + containerID1 - } else { - hc.PidMode = containerID1 + data.Labels().Set("pidMode", pidMode) } - base.Cmd("run", "-d", "--name", testContainer2, - "--pid", fmt.Sprintf("container:%s", testContainer1), - testutil.AlpineImage, "sleep", "infinity").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()+"-container1") + helpers.Anyhow("rm", "-f", data.Identifier()+"-container2") + } - inspect := base.InspectContainer(testContainer2) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()+"-container2") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + assert.Equal(tt, data.Labels().Get("pidMode"), inspect.HostConfig.PidMode) + }, + } + } - assert.Equal(t, hc.PidMode, inspect.HostConfig.PidMode) + testCase.Run(t) } func TestContainerInspectHostConfigPIDDefaults(t *testing.T) { - testContainer := testutil.Identifier(t) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container - base.Cmd("run", "-d", "--name", testContainer, testutil.AlpineImage, "sleep", "infinity").AssertOK() + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) - inspect := base.InspectContainer(testContainer) + inspect := dc[0] - assert.Equal(t, "", inspect.HostConfig.PidMode) + assert.Equal(tt, "", inspect.HostConfig.PidMode) + }) + + testCase.Run(t) } func TestContainerInspectDevices(t *testing.T) { - testContainer := testutil.Identifier(t) + testCase := nerdtest.Setup() - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).Run() + testCase.Require = nerdtest.CgroupsAccessible - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Create a temporary directory + dir := data.Temp().Dir("device-dir") - // Create a temporary directory - dir, err := os.MkdirTemp(t.TempDir(), "device-dir") - if err != nil { - t.Fatal(err) - } + if nerdtest.IsDocker() { + dir = "/dev/zero" + } - if nerdtest.IsDocker() { - dir = "/dev/zero" + helpers.Ensure("run", "-d", "--name", data.Identifier(), "--device", dir+":/dev/xvda", testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + data.Labels().Set("dir", dir) } - // Run the container with the directory mapped as a device - base.Cmd("run", "-d", "--name", testContainer, - "--device", dir+":/dev/xvda", - testutil.AlpineImage, "sleep", "infinity").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - inspect := base.InspectContainer(testContainer) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + } - expectedDevices := []dockercompat.DeviceMapping{ - { - PathOnHost: dir, - PathInContainer: "/dev/xvda", - CgroupPermissions: "rwm", - }, + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + expectedDevices := []dockercompat.DeviceMapping{ + { + PathOnHost: data.Labels().Get("dir"), + PathInContainer: "/dev/xvda", + CgroupPermissions: "rwm", + }, + } + assert.DeepEqual(tt, expectedDevices, inspect.HostConfig.Devices) + }, + } } - assert.DeepEqual(t, expectedDevices, inspect.HostConfig.Devices) + + testCase.Run(t) } func TestContainerInspectBlkioSettings(t *testing.T) { - testutil.DockerIncompatible(t) - testContainer := testutil.Identifier(t) + var lo *loopback.Loopback + + testCase := nerdtest.Setup() + // Some of the blkio settings are not supported in cgroup v1. // So skip this test if running on cgroup v1 - if infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers or if running with cgroup v1") - } + testCase.Require = require.All( + require.Not(nerdtest.Docker), + require.Not(nerdtest.Rootless), + nerdtest.CGroupV2, + ) - if rootlessutil.IsRootless() { - t.Skip("test requires root privilege to create a dummy device") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // See https://github.com/containerd/nerdctl/issues/4185 + // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. + // For now, disable the test unless on a recent kernel. + testutil.RequireKernelVersion(t, ">= 6.0.0-0") + + var err error + lo, err = loopback.New(4096) + if err != nil { + err = fmt.Errorf("cannot find a loop device: %w", err) + t.Fatal(err) + } + + const ( + weight = 500 + readBps = 1048576 + readIops = 1000 + writeBps = 2097152 + writeIops = 2000 + ) + + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--blkio-weight", fmt.Sprintf("%d", weight), + "--blkio-weight-device", fmt.Sprintf("%s:%d", lo.Device, weight), + "--device-read-bps", fmt.Sprintf("%s:%d", lo.Device, readBps), + "--device-read-iops", fmt.Sprintf("%s:%d", lo.Device, readIops), + "--device-write-bps", fmt.Sprintf("%s:%d", lo.Device, writeBps), + "--device-write-iops", fmt.Sprintf("%s:%d", lo.Device, writeIops), + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + data.Labels().Set("weight", strconv.Itoa(weight)) + data.Labels().Set("readBps", strconv.Itoa(readBps)) + data.Labels().Set("readIops", strconv.Itoa(readIops)) + data.Labels().Set("writeBps", strconv.Itoa(writeBps)) + data.Labels().Set("writeIops", strconv.Itoa(writeIops)) } - // See https://github.com/containerd/nerdctl/issues/4185 - // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. - // For now, disable the test unless on a recent kernel. - testutil.RequireKernelVersion(t, ">= 6.0.0-0") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + if lo != nil { + lo.Close() + } + } - lo, err := loopback.New(4096) - if err != nil { - err = fmt.Errorf("cannot find a loop device: %w", err) - t.Fatal(err) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) } - defer lo.Close() - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainer).AssertOK() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + weight, err := strconv.Atoi(data.Labels().Get("weight")) + assert.NilError(tt, err) + readBps, err := strconv.Atoi(data.Labels().Get("readBps")) + assert.NilError(tt, err) + writeBps, err := strconv.Atoi(data.Labels().Get("writeBps")) + assert.NilError(tt, err) + readIops, err := strconv.Atoi(data.Labels().Get("readIops")) + assert.NilError(tt, err) + writeIops, err := strconv.Atoi(data.Labels().Get("writeIops")) + assert.NilError(tt, err) + + var dc []dockercompat.Container + + err = json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + + assert.Equal(tt, uint16(weight), inspect.HostConfig.BlkioWeight) + assert.Equal(tt, 1, len(inspect.HostConfig.BlkioWeightDevice)) + assert.Equal(tt, lo.Device, inspect.HostConfig.BlkioWeightDevice[0].Path) + assert.Equal(tt, uint16(weight), inspect.HostConfig.BlkioWeightDevice[0].Weight) + assert.Equal(tt, 1, len(inspect.HostConfig.BlkioDeviceReadBps)) + assert.Equal(tt, uint64(readBps), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) + assert.Equal(tt, 1, len(inspect.HostConfig.BlkioDeviceWriteBps)) + assert.Equal(tt, uint64(writeBps), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) + assert.Equal(tt, 1, len(inspect.HostConfig.BlkioDeviceReadIOps)) + assert.Equal(tt, uint64(readIops), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) + assert.Equal(tt, 1, len(inspect.HostConfig.BlkioDeviceWriteIOps)) + assert.Equal(tt, uint64(writeIops), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) + }, + } + } - const ( - weight = 500 - readBps = 1048576 - readIops = 1000 - writeBps = 2097152 - writeIops = 2000 - ) - base.Cmd("run", "-d", "--name", testContainer, - "--blkio-weight", fmt.Sprintf("%d", weight), - "--blkio-weight-device", fmt.Sprintf("%s:%d", lo.Device, weight), - "--device-read-bps", fmt.Sprintf("%s:%d", lo.Device, readBps), - "--device-read-iops", fmt.Sprintf("%s:%d", lo.Device, readIops), - "--device-write-bps", fmt.Sprintf("%s:%d", lo.Device, writeBps), - "--device-write-iops", fmt.Sprintf("%s:%d", lo.Device, writeIops), - testutil.AlpineImage, "sleep", "infinity").AssertOK() - - inspect := base.InspectContainer(testContainer) - assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeight) - assert.Equal(t, 1, len(inspect.HostConfig.BlkioWeightDevice)) - assert.Equal(t, lo.Device, inspect.HostConfig.BlkioWeightDevice[0].Path) - assert.Equal(t, uint16(weight), inspect.HostConfig.BlkioWeightDevice[0].Weight) - assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadBps)) - assert.Equal(t, uint64(readBps), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) - assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteBps)) - assert.Equal(t, uint64(writeBps), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) - assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadIOps)) - assert.Equal(t, uint64(readIops), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) - assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteIOps)) - assert.Equal(t, uint64(writeIops), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) + testCase.Run(t) } func TestContainerInspectUser(t *testing.T) { From 66d7753c62287dec28b7a7d6d09ee166c9ad49d5 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 1 Apr 2026 00:25:20 +0900 Subject: [PATCH 463/868] TestRunWithSystemdTrueEnabled: mark as "needs fixing" See issue 4746 Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_run_systemd_linux_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go index 759b2962892..b6b99f0ee4d 100644 --- a/cmd/nerdctl/container/container_run_systemd_linux_test.go +++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go @@ -70,6 +70,7 @@ func TestRunWithSystemdTrueEnabled(t *testing.T) { testCase.Require = require.All( require.Amd64, require.Not(nerdtest.Docker), + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4746"), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { From 5aca2af2020b7246c6e581a301a0d9f1342ac456 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 1 Apr 2026 00:27:37 +0900 Subject: [PATCH 464/868] TestLogs: mark as flaky See issue 4782 Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_logs_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index a1c483290d6..983a4405d8b 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -44,6 +44,7 @@ bar testCase := nerdtest.Setup() + testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4782") if runtime.GOOS == "windows" { testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") } From afbfb77828cc3ad72c2a0e5a75abc8b12dff6cd3 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 1 Apr 2026 00:29:19 +0900 Subject: [PATCH 465/868] TestLoadStdinFromPipe: mark as flaky See issue 4789 Signed-off-by: Akihiro Suda --- cmd/nerdctl/image/image_load_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 90e8c970d1a..07888261732 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -38,7 +38,7 @@ func TestLoadStdinFromPipe(t *testing.T) { testCase := &test.Case{ Description: "TestLoadStdinFromPipe", - Require: require.Linux, + Require: require.All(require.Linux, nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4789")), Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("pull", "--quiet", testutil.CommonImage) From 5764a0ddc8587c5fc9e26b03e5ee64755daba269 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 06:36:45 +0000 Subject: [PATCH 466/868] build(deps): bump the golang-x group across 1 directory with 6 updates Bumps the golang-x group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.48.0 to 0.49.0 - [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.49.0) Updates `golang.org/x/net` from 0.50.0 to 0.51.0 - [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0) Updates `golang.org/x/sync` from 0.19.0 to 0.20.0 - [Commits](https://github.com/golang/sync/compare/v0.19.0...v0.20.0) Updates `golang.org/x/sys` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/sys/compare/v0.41.0...v0.42.0) Updates `golang.org/x/term` from 0.40.0 to 0.41.0 - [Commits](https://github.com/golang/term/compare/v0.40.0...v0.41.0) Updates `golang.org/x/text` from 0.34.0 to 0.35.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.34.0...v0.35.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index cb914e79d21..3b796e7323b 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.24.3 +go 1.25.0 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.48.0 - golang.org/x/net v0.50.0 - golang.org/x/sync v0.19.0 //gomodjail:unconfined - golang.org/x/sys v0.41.0 //gomodjail:unconfined - golang.org/x/term v0.40.0 //gomodjail:unconfined - golang.org/x/text v0.34.0 + golang.org/x/crypto v0.49.0 + golang.org/x/net v0.51.0 + golang.org/x/sync v0.20.0 //gomodjail:unconfined + golang.org/x/sys v0.42.0 //gomodjail:unconfined + golang.org/x/term v0.41.0 //gomodjail:unconfined + golang.org/x/text v0.35.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) @@ -135,7 +135,7 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.32.0 // indirect + golang.org/x/mod v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect //gomodjail:unconfined google.golang.org/grpc v1.79.3 // indirect diff --git a/go.sum b/go.sum index 1a1c65dc2dd..600d2c0d069 100644 --- a/go.sum +++ b/go.sum @@ -363,8 +363,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= 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= @@ -378,8 +378,8 @@ 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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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= @@ -396,8 +396,8 @@ 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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= 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= @@ -410,8 +410,8 @@ 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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/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= @@ -434,8 +434,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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.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= @@ -445,8 +445,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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= 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= @@ -456,8 +456,8 @@ 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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= 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= @@ -470,8 +470,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= 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= From 600a13eb16068c77392705062027fa08a26b5947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:07:49 +0000 Subject: [PATCH 467/868] build(deps): bump github.com/klauspost/compress from 1.18.4 to 1.18.5 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.4 to 1.18.5. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.4...v1.18.5) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cb914e79d21..c234501925a 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.0 - github.com/klauspost/compress v1.18.4 + github.com/klauspost/compress v1.18.5 github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 diff --git a/go.sum b/go.sum index 1a1c65dc2dd..14b1cb6dc67 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 3ae7b3c4c1480ff9c9cbd78bba9aae6932715979 Mon Sep 17 00:00:00 2001 From: Siddhesh Suryawanshi Date: Thu, 12 Mar 2026 07:42:55 -0700 Subject: [PATCH 468/868] test: refactor compose_ps_linux_test.go to use tigron Signed-off-by: Siddhesh Suryawanshi --- cmd/nerdctl/compose/compose_ps_linux_test.go | 281 ++++++++++++++----- 1 file changed, 217 insertions(+), 64 deletions(-) diff --git a/cmd/nerdctl/compose/compose_ps_linux_test.go b/cmd/nerdctl/compose/compose_ps_linux_test.go index c6ebb1de8aa..892dbafe41b 100644 --- a/cmd/nerdctl/compose/compose_ps_linux_test.go +++ b/cmd/nerdctl/compose/compose_ps_linux_test.go @@ -19,18 +19,27 @@ package compose import ( "encoding/json" "fmt" + "path/filepath" "strings" "testing" - "time" "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/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposePs(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.Private + var dockerComposeYAML = fmt.Sprintf(` services: wordpress: @@ -61,56 +70,121 @@ volumes: wordpress: db: `, testutil.WordpressImage, testutil.MariaDBImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYAML", composePath) + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + + nerdtest.EnsureContainerStarted(helpers, "wordpress_container") + nerdtest.EnsureContainerStarted(helpers, "db_container") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if path := data.Labels().Get("composeYAML"); path != "" { + helpers.Anyhow("compose", "-f", path, "down", "-v") + } + } + + assertHandler := func(expectedName, expectedImage string) test.Comparator { + return func(stdout string, t tig.T) { - assertHandler := func(expectedName, expectedImage string) func(stdout string) error { - return func(stdout string) error { lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } + assert.Assert(t, len(lines) >= 2) tab := tabutil.NewReader("NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } + assert.NilError(t, tab.ParseHeader(lines[0])) container, _ := tab.ReadRow(lines[1], "NAME") assert.Equal(t, container, expectedName) image, _ := tab.ReadRow(lines[1], "IMAGE") assert.Equal(t, image, expectedImage) - - return nil } + } + testCase.SubTests = []*test.Case{ + { + Description: "compose ps wordpress", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "wordpress") + }, + Expected: test.Expects(0, nil, + assertHandler("wordpress_container", testutil.WordpressImage), + ), + }, + { + Description: "compose ps db", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "db") + }, + Expected: test.Expects(0, nil, + assertHandler("db_container", testutil.MariaDBImage), + ), + }, + { + Description: "compose ps should not show alpine unless running", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps") + }, + Expected: test.Expects(0, nil, + expect.DoesNotContain(testutil.CommonImage), + ), + }, + { + Description: "compose ps alpine -a", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "alpine", "-a") + }, + Expected: test.Expects(0, nil, + assertHandler("alpine_container", testutil.CommonImage), + ), + }, + { + Description: "compose ps filter exited", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "-a", "--filter", "status=exited") + }, + Expected: test.Expects(0, nil, + assertHandler("alpine_container", testutil.CommonImage), + ), + }, + { + Description: "compose ps services", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "--services", "-a") + }, + Expected: test.Expects(0, nil, + expect.All( + expect.Contains("wordpress\n"), + expect.Contains("db\n"), + expect.Contains("alpine\n"), + ), + ), + }, } - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutWithFunc(assertHandler("wordpress_container", testutil.WordpressImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutWithFunc(assertHandler("db_container", testutil.MariaDBImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.CommonImage) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.CommonImage)) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--services", "-a").AssertOutContainsAll("wordpress\n", "db\n", "alpine\n") + testCase.Run(t) } func TestComposePsJSON(t *testing.T) { + testCase := nerdtest.Setup() + // docker parses unknown 'format' as a Go template and won't output an error - testutil.DockerIncompatible(t) + testCase.Require = require.All( + nerdtest.Private, + require.Not(nerdtest.Docker), + ) - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: image: %s + container_name: wordpress_container ports: - 8080:80 environment: @@ -122,6 +196,7 @@ services: - wordpress:/var/www/html db: image: %s + container_name: db_container environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser @@ -135,50 +210,128 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase.Setup = func(data test.Data, helpers test.Helpers) { - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYAML", composePath) + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + + nerdtest.EnsureContainerStarted(helpers, "wordpress_container") + nerdtest.EnsureContainerStarted(helpers, "db_container") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if path := data.Labels().Get("composeYAML"); path != "" { + helpers.Anyhow("compose", "-f", path, "down", "-v") + } + } + + assertHandler := func(svc string, count int, fields ...string) test.Comparator { + return func(stdout string, t tig.T) { - assertHandler := func(svc string, count int, fields ...string) func(stdout string) error { - return func(stdout string) error { - // 1. check json output can be unmarshalled back to printables. var printables []composeContainerPrintable - if err := json.Unmarshal([]byte(stdout), &printables); err != nil { - return fmt.Errorf("[service: %s]failed to unmarshal json output from `compose ps`: %s", svc, stdout) - } + // 1. check json output can be unmarshalled back to printables. + assert.NilError(t, json.Unmarshal([]byte(stdout), &printables)) // 2. check #printables matches expected count. - if len(printables) != count { - return fmt.Errorf("[service: %s]unmarshal generates %d printables, expected %d: %s", svc, len(printables), count, stdout) - } + assert.Equal(t, len(printables), count) // 3. check marshalled json string has all expected substrings. for _, field := range fields { - if !strings.Contains(stdout, field) { - return fmt.Errorf("[service: %s]marshalled json output doesn't have expected string (%s): %s", svc, field, stdout) - } + assert.Assert(t, strings.Contains(stdout, field), + fmt.Sprintf("[service: %s] expected %s in %s", svc, field, stdout)) } - return nil } } - // check other formats are not supported - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "yaml").AssertFail() - // check all services are up (can be marshalled and unmarshalled) and check Image field exists - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json"). - AssertOutWithFunc(assertHandler("all", 2, `"Service":"wordpress"`, `"Service":"db"`, - fmt.Sprintf(`"Image":"%s"`, testutil.WordpressImage), fmt.Sprintf(`"Image":"%s"`, testutil.MariaDBImage))) - // check wordpress is running - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress"). - AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"running"`, `"TargetPort":80`, `"PublishedPort":8080`)) - // check wordpress is stopped - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress", "-a"). - AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"exited"`)) - // check wordpress is removed - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress"). - AssertOutWithFunc(assertHandler("wordpress", 0)) + testCase.SubTests = []*test.Case{ + { // check other formats are not supported + Description: "unsupported format should fail", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", data.Labels().Get("composeYAML"), + "ps", + "--format", "yaml", + ) + }, + Expected: test.Expects(1, nil, nil), + }, + { // check all services are up (can be marshalled and unmarshalled) and check Image field exists + Description: "ps json all services", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "--format", "json") + }, + Expected: test.Expects(0, nil, + assertHandler("all", 2, + `"Service":"wordpress"`, + `"Service":"db"`, + fmt.Sprintf(`"Image":"%s"`, testutil.WordpressImage), + fmt.Sprintf(`"Image":"%s"`, testutil.MariaDBImage), + ), + ), + }, + { // check wordpress is running + Description: "wordpress running", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), + "ps", "--format", "json", "wordpress") + }, + Expected: test.Expects(0, nil, + assertHandler("wordpress", 1, + `"Service":"wordpress"`, + `"State":"running"`, + `"TargetPort":80`, + `"PublishedPort":8080`, + )), + }, + { + Description: "stop wordpress", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), + "stop", "wordpress") + }, + Expected: test.Expects(0, nil, nil), + }, + { // check wordpress is stopped + Description: "wordpress exited", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), + "ps", "--format", "json", "wordpress", "-a") + }, + Expected: test.Expects(0, nil, + assertHandler("wordpress", 1, + `"Service":"wordpress"`, + `"State":"exited"`, + )), + }, + { + Description: "remove wordpress", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), + "rm", "-f", "wordpress") + }, + Expected: test.Expects(0, nil, nil), + }, + { // check wordpress is removed + Description: "wordpress removed", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), + "ps", "--format", "json", "wordpress") + }, + Expected: test.Expects(0, nil, + assertHandler("wordpress", 0), + ), + }, + } + + testCase.Run(t) } From 6d74b97e46732443949b20ca30c1ed17d8b325fc Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Thu, 2 Apr 2026 22:49:23 +0900 Subject: [PATCH 469/868] fix: clean up the directory for port-reserver pid on error in applyNetworkSettings follow-up: https://github.com/containerd/nerdctl/pull/4811 Signed-off-by: Hayato Kiwata --- pkg/ocihook/ocihook.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 2aa14101c82..910ff3aebd4 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -503,6 +503,7 @@ func applyNetworkSettings(opts *handlerOpts) (err error) { if err != nil { log.L.Debugf("killing the port reserver process (pid=%d)", reserverCmdPid) _ = reserverCmd.Process.Kill() + _ = os.RemoveAll(filepath.Dir(portReserverPidFilePath(opts))) } }() if err := writePidFile(portReserverPidFilePath(opts), reserverCmdPid); err != nil { From c04945493bcb4913afc5175c1c3064fe1502c4d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:43:14 +0000 Subject: [PATCH 470/868] build(deps): bump github.com/fatih/color from 1.18.0 to 1.19.0 Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.18.0 to 1.19.0. - [Release notes](https://github.com/fatih/color/releases) - [Commits](https://github.com/fatih/color/compare/v1.18.0...v1.19.0) --- updated-dependencies: - dependency-name: github.com/fatih/color dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 3b796e7323b..2801dd8676b 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined - github.com/fatih/color v1.18.0 //gomodjail:unconfined + github.com/fatih/color v1.19.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 @@ -95,7 +95,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect diff --git a/go.sum b/go.sum index 600d2c0d069..981317c4a6b 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ= @@ -185,9 +185,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= @@ -423,7 +422,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 95e429ed69d2f055c20cf875ac173be830f0da9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:32:19 +0000 Subject: [PATCH 471/868] build(deps): bump docker/login-action from 4.0.0 to 4.1.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 265bff8078b..1cc82c817d8 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -44,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 4928c795c0d936e458ddfb57a8da307a20133f46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:32:26 +0000 Subject: [PATCH 472/868] build(deps): bump the stargz group with 3 updates Bumps the stargz group with 3 updates: [github.com/containerd/stargz-snapshotter](https://github.com/containerd/stargz-snapshotter), [github.com/containerd/stargz-snapshotter/estargz](https://github.com/containerd/stargz-snapshotter) and [github.com/containerd/stargz-snapshotter/ipfs](https://github.com/containerd/stargz-snapshotter). Updates `github.com/containerd/stargz-snapshotter` from 0.18.1 to 0.18.2 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.1...v0.18.2) Updates `github.com/containerd/stargz-snapshotter/estargz` from 0.18.1 to 0.18.2 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.1...v0.18.2) Updates `github.com/containerd/stargz-snapshotter/ipfs` from 0.18.1 to 0.18.2 - [Release notes](https://github.com/containerd/stargz-snapshotter/releases) - [Commits](https://github.com/containerd/stargz-snapshotter/compare/v0.18.1...v0.18.2) --- updated-dependencies: - dependency-name: github.com/containerd/stargz-snapshotter dependency-version: 0.18.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/estargz dependency-version: 0.18.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz - dependency-name: github.com/containerd/stargz-snapshotter/ipfs dependency-version: 0.18.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: stargz ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 22 ++++++++++------------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 3b796e7323b..2589c05d533 100644 --- a/go.mod +++ b/go.mod @@ -22,9 +22,9 @@ require ( github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.11 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter v0.18.1 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/estargz v0.18.1 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/ipfs v0.18.1 //gomodjail:unconfined + 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/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.9.1 //gomodjail:unconfined @@ -120,7 +120,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect @@ -133,7 +133,7 @@ require ( go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect diff --git a/go.sum b/go.sum index 600d2c0d069..79e88c2d76c 100644 --- a/go.sum +++ b/go.sum @@ -57,12 +57,12 @@ github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6a github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter v0.18.1 h1:eIkwsafohSWas5YmhxoumrI7elmb2EZJcW8eu7goyOY= -github.com/containerd/stargz-snapshotter v0.18.1/go.mod h1:HPC+XHGIxkjWfAONMvXepQyOs8iGApP2e5A3fOv2TCU= -github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= -github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= -github.com/containerd/stargz-snapshotter/ipfs v0.18.1 h1:v0kozDNJCW1iVy/1MgD5uZImW87CvkHLzb9L9JwfOco= -github.com/containerd/stargz-snapshotter/ipfs v0.18.1/go.mod h1:qgy0jrKhqtLxn6J5rb9BXZ1Xj1Xeimd0vOi25Zyhpds= +github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= +github.com/containerd/stargz-snapshotter v0.18.2/go.mod h1:iS0a4lgCFjGbdBJNrm1jwvaMFGGnQ6PZ5Sd09i060h8= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+XesH/6BBuJcdtV6ymGlGg= +github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -283,8 +283,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+x github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -300,7 +300,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -349,8 +348,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= @@ -421,7 +420,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 2f671915f21e19cb581387796c27fd6fa13392e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:34:28 +0000 Subject: [PATCH 473/868] build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v4 dependency-version: 4.1.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3b796e7323b..6f60666b213 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/djherbis/times v1.6.0 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/go.sum b/go.sum index 600d2c0d069..ca2fe60ff99 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOe github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From f01a38d89fca8b85958e0e6807a91a7f277ec419 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:32:34 +0000 Subject: [PATCH 474/868] build(deps): bump golang.org/x/net in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.51.0 to 0.52.0 - [Commits](https://github.com/golang/net/compare/v0.51.0...v0.52.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.52.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2589c05d533..5c711b41d9c 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.49.0 - golang.org/x/net v0.51.0 + golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 //gomodjail:unconfined golang.org/x/sys v0.42.0 //gomodjail:unconfined golang.org/x/term v0.41.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 79e88c2d76c..10cdbdc4729 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,8 @@ 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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 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= From 1a341a63defa4406d5097a37445dfc6a5728fc3a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 6 Apr 2026 07:31:15 +0900 Subject: [PATCH 475/868] Dockerfile: fix building runc >= 1.5.0-rc.2 Fix issue 4825 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0aa3f541341..6bbec3d9a36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,7 +98,7 @@ RUN git-checkout-tag-with-hash.sh ${RUNC_VERSION} && \ mkdir -p /out ENV CGO_ENABLED=1 # FIXME: avoid omitting libpathrs -RUN set -x ; GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make BUILDTAGS="$(grep -oP "^BUILDTAGS := \K.*" Makefile | sed -e s/libpathrs//)" static && \ +RUN set -x ; GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make RUNC_BUILDTAGS="-libpathrs" static && \ xx-verify --static runc && cp -v -a runc /out/runc.${TARGETARCH} FROM build-base AS build-bypass4netns From 8f2a13e938342d98f06b84570d2df8ce8cedb2a7 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 1 Apr 2026 22:23:01 +0900 Subject: [PATCH 476/868] test: resolve a flaky test of TestLoadStdinFromPipe Signed-off-by: Hayato Kiwata --- pkg/imgutil/load/load.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/imgutil/load/load.go b/pkg/imgutil/load/load.go index 5e80096d5db..c8b4c629a56 100644 --- a/pkg/imgutil/load/load.go +++ b/pkg/imgutil/load/load.go @@ -86,7 +86,6 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I var loadedImages []images.Image pf, done := transferutil.ProgressHandler(ctx, options.Stdout) - defer done() err = client.Transfer(ctx, tarchive.NewImageImportStream(options.Stdin, ""), @@ -96,9 +95,6 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I if img, err := imageService.Get(ctx, p.Name); err == nil { if !beforeSet[img.Name] { loadedImages = append(loadedImages, img) - if !options.Quiet { - fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img.Name) - } } } } @@ -106,6 +102,14 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I }), ) + done() + + if !options.Quiet { + for _, img := range loadedImages { + fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img.Name) + } + } + return loadedImages, err } From b5755a033f115556db36991fe6f3c891c241d052 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 6 Apr 2026 21:18:51 +0900 Subject: [PATCH 477/868] Revert "TestLoadStdinFromPipe: mark as flaky" This reverts commit afbfb77828cc3ad72c2a0e5a75abc8b12dff6cd3. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/image/image_load_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 07888261732..90e8c970d1a 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -38,7 +38,7 @@ func TestLoadStdinFromPipe(t *testing.T) { testCase := &test.Case{ Description: "TestLoadStdinFromPipe", - Require: require.All(require.Linux, nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4789")), + Require: require.Linux, Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("pull", "--quiet", testutil.CommonImage) From e79492b2b1a869ec4bf96bf12c3872f088b9fa7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:32:49 +0000 Subject: [PATCH 478/868] build(deps): bump github.com/containerd/platforms Bumps [github.com/containerd/platforms](https://github.com/containerd/platforms) from 1.0.0-rc.2 to 1.0.0-rc.4. - [Release notes](https://github.com/containerd/platforms/releases) - [Commits](https://github.com/containerd/platforms/compare/v1.0.0-rc.2...v1.0.0-rc.4) --- updated-dependencies: - dependency-name: github.com/containerd/platforms dependency-version: 1.0.0-rc.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a79ba7c2234..32c1feb3ac4 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.11 //gomodjail:unconfined - github.com/containerd/platforms v1.0.0-rc.2 //gomodjail:unconfined + github.com/containerd/platforms v1.0.0-rc.4 //gomodjail:unconfined 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 diff --git a/go.sum b/go.sum index 4b52ea9b5d6..ad9c5e7cb2f 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.11 h1:YTdF4rsjFRsfyaIhnWVUSLz8FqJwOyRZ5FhvFjHh7Uc= github.com/containerd/nydus-snapshotter v0.15.11/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= -github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= -github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= From 2aa00100d491cd30ab7ad1dfbb2b2fd2093b9faa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:33:04 +0000 Subject: [PATCH 479/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.10.1 to 2.10.2. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.10.1...v2.10.2) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.10.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a79ba7c2234..705177f3005 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hcsshim v0.14.0-rc.1 - github.com/compose-spec/compose-go/v2 v2.10.1 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.10.2 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.1 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined @@ -153,7 +153,7 @@ require ( github.com/moby/moby/api v1.52.0 // indirect github.com/moby/moby/client v0.1.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect ) replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index 4b52ea9b5d6..669e2cadf07 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU= -github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg= +github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= +github.com/compose-spec/compose-go/v2 v2.10.2/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.1 h1:jeZYAaq5pMCeyRZ0I916OjJsEb2TGjAQmfAZyQLi3ec= github.com/containerd/accelerated-container-image v1.4.1/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= @@ -351,8 +351,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= -go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +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= From db0d1cfb1fae1fd537a421b45d9a7acbab34e974 Mon Sep 17 00:00:00 2001 From: Istvan Csaba Varga Date: Tue, 7 Apr 2026 14:37:24 +0200 Subject: [PATCH 480/868] docs: update list of unimplemented Docker features - Move --health-start-interval to the unimplemented docker run flags list (it was incorrectly marked as :whale: in the health check flags section, but the flag does not exist in the codebase) - Fix typo in compose push section: "docker-compose pull" -> "docker-compose push" - Expand unimplemented compose commands list with missing Docker Compose V2 subcommands: attach, ls, stats, wait, watch (events and scale were already listed) Partial fix for #3867 Signed-off-by: Istvan Csaba Varga --- docs/command-reference.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index a4a29958fd3..3d40d9398b1 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -343,7 +343,6 @@ Health check flags: - :whale: `--health-timeout`: Time to wait before considering the check failed (e.g., 5s) - :whale: `--health-retries`: Number of failures before container is considered unhealthy - :whale: `--health-start-period`: Start period for the container to initialize before starting health-retries countdown -- :whale: `--health-start-interval`: Interval between checks during the start period - :whale: `--no-healthcheck`: Disable any health checks defined by image or CLI Logging flags: @@ -454,7 +453,8 @@ IPFS flags: Unimplemented `docker run` flags: `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, - `--link*`, `--publish-all`, `--storage-opt`, `--volume-driver` + `--health-start-interval`, `--link*`, `--publish-all`, `--storage-opt`, + `--volume-driver` ### :whale: nerdctl exec @@ -1809,7 +1809,7 @@ Push service images Usage: `nerdctl compose push [OPTIONS] [SERVICE...]` -Unimplemented `docker-compose pull` (V1) flags: `--ignore-push-failures` +Unimplemented `docker-compose push` (V1) flags: `--ignore-push-failures` ### :whale: nerdctl compose pause @@ -1994,7 +1994,13 @@ Network management: Compose: -- `docker-compose events|scale` +- `docker compose attach` +- `docker compose events` +- `docker compose ls` +- `docker compose scale` +- `docker compose stats` +- `docker compose wait` +- `docker compose watch` Builder: From af8db663a89ba8cd0397d167d00c44b41abdd37a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:33:03 +0000 Subject: [PATCH 481/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.11 to 0.15.14. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.11...v0.15.14) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 32c1feb3ac4..fd9eb61e09d 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.11 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.14 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.4 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index ad9c5e7cb2f..4c3a8a921a7 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.11 h1:YTdF4rsjFRsfyaIhnWVUSLz8FqJwOyRZ5FhvFjHh7Uc= -github.com/containerd/nydus-snapshotter v0.15.11/go.mod h1:EWRd/QJ0b6UKHAqYgiV5gHlqLC2qq5cQiSlXEdVovrA= +github.com/containerd/nydus-snapshotter v0.15.14 h1:rjtDCwtBOLdaGnhjkPI274e9zGy+FBY8A8AaJNLxioU= +github.com/containerd/nydus-snapshotter v0.15.14/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From 4898211d5470089740a864649b007c82fe6f8bc5 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 8 Apr 2026 13:18:43 +0200 Subject: [PATCH 482/868] inital draft for the compose_linux_restart test refactor Signed-off-by: sathiraumesh --- .../compose/compose_restart_linux_test.go | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/cmd/nerdctl/compose/compose_restart_linux_test.go b/cmd/nerdctl/compose/compose_restart_linux_test.go index 1e2ca2c5c61..e93c24c6adc 100644 --- a/cmd/nerdctl/compose/compose_restart_linux_test.go +++ b/cmd/nerdctl/compose/compose_restart_linux_test.go @@ -18,13 +18,18 @@ package compose import ( "fmt" + "regexp" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeRestart(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` services: wordpress: @@ -51,24 +56,65 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Labels().Get("yamlPath"), "down", "-v") + } - // stop and restart a single service. - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited") - base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "db").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") + testCase.SubTests = []*test.Case{ + { + Description: "restart single service", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "db") + ps := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db", "-a") + expect.Match(regexp.MustCompile("Exit|exited"))(ps, helpers.T()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "restart", "db") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + ps := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + expect.Match(regexp.MustCompile("Up|running"))(ps, t) + }, + } + }, + }, + { + Description: "stop one service and restart all with timeout", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "db") + ps := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db", "-a") + expect.Match(regexp.MustCompile("Exit|exited"))(ps, helpers.T()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "restart", "--timeout", "5") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + comp := expect.Match(regexp.MustCompile("Up|running")) + comp(db, t) + comp(wp, t) + }, + } + }, + }, + } - // stop one service and restart all (also check `--timeout` arg). - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited") - base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "--timeout", "5").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") + testCase.Run(t) } From 2ddcec2a7a5611783407e5b4fe20670e2c60276c Mon Sep 17 00:00:00 2001 From: Swapnanil Gupta Date: Wed, 8 Apr 2026 18:58:48 +0000 Subject: [PATCH 483/868] update cosign to v3.0.5 in Dockerfile Signed-off-by: Swapnanil Gupta --- Dockerfile | 2 +- cmd/nerdctl/image/image_pull_linux_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6bbec3d9a36..15cc8e390e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -332,7 +332,7 @@ WORKDIR /go/src/github.com/containerd/nerdctl VOLUME /tmp ENV CGO_ENABLED=0 # copy cosign binary for integration test -COPY --from=ghcr.io/sigstore/cosign/cosign:v2.2.3@sha256:8fc9cad121611e8479f65f79f2e5bea58949e8a87ffac2a42cb99cf0ff079ba7 /ko-app/cosign /usr/local/bin/cosign +COPY --from=ghcr.io/sigstore/cosign/cosign:v3.0.5@sha256:be924970ba7438c22e18067dec5637946d6566eac711f5bedd1584e7137008fb /ko-app/cosign /usr/local/bin/cosign # installing soci for integration test ARG SOCI_SNAPSHOTTER_VERSION RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index 5637680e139..744ab85dc51 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -102,7 +102,7 @@ CMD ["echo", "nerdctl-build-test-string"] _, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "2") return helpers.Command("pull", "--quiet", "--verify=cosign", "--cosign-key="+pub, data.Labels().Get("image_ref")+":two") }, - Expected: test.Expects(12, nil, nil), + Expected: test.Expects(1, nil, nil), }, }, } From fe8549eca34762d9554f241ef748d3afc2e0b38f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:32:34 +0000 Subject: [PATCH 484/868] build(deps): bump golang.org/x/sys in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/sys](https://github.com/golang/sys). Updates `golang.org/x/sys` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 32c1feb3ac4..b56e8c00c29 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( golang.org/x/crypto v0.49.0 golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 //gomodjail:unconfined - golang.org/x/sys v0.42.0 //gomodjail:unconfined + golang.org/x/sys v0.43.0 //gomodjail:unconfined golang.org/x/term v0.41.0 //gomodjail:unconfined golang.org/x/text v0.35.0 gotest.tools/v3 v3.5.2 diff --git a/go.sum b/go.sum index ad9c5e7cb2f..c04886b7069 100644 --- a/go.sum +++ b/go.sum @@ -430,8 +430,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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.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= From b6292c5068d7e96e12671d51dc22bd1e3ac0daf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:32:37 +0000 Subject: [PATCH 485/868] build(deps): bump github.com/docker/cli in the docker group Bumps the docker group with 1 update: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.3.1+incompatible to 29.4.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.3.1...v29.4.0) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.4.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 32c1feb3ac4..ef46a1e817e 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.3.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.4.0+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index ad9c5e7cb2f..9c78020e5b5 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,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.3.1+incompatible h1:M04FDj2TRehDacrosh7Vlkgc7AuQoWloQkf1PA5hmoI= -github.com/docker/cli v29.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= +github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= From ae4a33952993aae463dc066dc5900f5132ba94a2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 6 Apr 2026 08:20:47 +0900 Subject: [PATCH 486/868] Update RootlessKit (3.0.0) - slirp4netns is no longer needed as gvisor-tap-vsock is now embedded in RootlessKit. slirp4netns is still used when installed. - The `builtin` port driver can now correctly propagate the source IP. Signed-off-by: Akihiro Suda --- Dockerfile | 16 +++++----------- Dockerfile.d/SHA256SUMS.d/SHA256SUMS | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 | 6 ++++++ Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 | 7 ------- README.md | 5 ++--- docs/faq.md | 4 ++-- docs/rootless.md | 6 +++--- .../rootless/containerd-rootless-setuptool.sh | 1 - extras/rootless/containerd-rootless.sh | 17 +++++++++-------- go.mod | 2 +- go.sum | 4 ++-- hack/build-integration-canary.sh | 2 -- pkg/bypass4netnsutil/bypass.go | 2 +- pkg/ocihook/ocihook.go | 2 +- pkg/ocihook/rootless_linux.go | 2 +- pkg/ocihook/rootless_other.go | 2 +- pkg/rootlessutil/port_linux.go | 4 ++-- pkg/rootlessutil/rootlessutil_linux.go | 2 +- pkg/rootlessutil/rootlessutil_other.go | 2 +- 20 files changed, 38 insertions(+), 60 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/SHA256SUMS delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 delete mode 100644 Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 diff --git a/Dockerfile b/Dockerfile index 15cc8e390e8..0c820e156c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,8 +28,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v2.3.6@BINARY -ARG SLIRP4NETNS_VERSION=v1.3.3@BINARY +ARG ROOTLESSKIT_VERSION=v3.0.0@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS @@ -194,14 +193,6 @@ RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%%@*}" https://githu git-checkout-tag-with-hash.sh "${IMGCRYPT_VERSION}" && \ CGO_ENABLED=0 make && DESTDIR=/out make install && \ echo "- imgcrypt: ${IMGCRYPT_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md -ARG SLIRP4NETNS_VERSION -RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION%%@*}; \ - fname="slirp4netns-$(cat /target_uname_m)" && \ - curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \ - grep "${fname}" "/SHA256SUMS.d/slirp4netns-${SLIRP4NETNS_VERSION}" | sha256sum -c && \ - mv "${fname}" /out/bin/slirp4netns && \ - chmod +x /out/bin/slirp4netns && \ - echo "- slirp4netns: ${SLIRP4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BYPASS4NETNS_VERSION COPY --from=build-bypass4netns /out/${TARGETARCH:-amd64}/* /out/bin/ RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md @@ -256,7 +247,6 @@ RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \ echo "## License" >> /out/share/doc/nerdctl-full/README.md && \ - echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/${SLIRP4NETNS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/fuse-overlayfs: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/containers/fuse-overlayfs/blob/${FUSE_OVERLAYFS_VERSION%%@*}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/{runc,bypass4netns,bypass4netnsd}: Apache License 2.0, statically linked with libseccomp ([LGPL 2.1](https://github.com/seccomp/libseccomp/blob/main/LICENSE), source code available at https://github.com/seccomp/libseccomp/)" >> /out/share/doc/nerdctl-full/README.md && \ echo "- bin/tini: [MIT License](https://github.com/krallin/tini/blob/${TINI_VERSION%%@*}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \ @@ -371,6 +361,8 @@ RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ uidmap \ openssh-server \ openssh-client +# Install slirp4netns only if rootlesskit is prior to v3.0 +RUN if ! rootlesskit --help | grep -q gvisor-tap-vsock; then apt-get install -qq --no-install-recommends slirp4netns; fi # TODO: update containerized-systemd to enable sshd by default, or allow `systemctl wants ssh` here RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N '' && \ useradd -m -s /bin/bash rootless && \ @@ -388,6 +380,8 @@ CMD ["/test-integration-rootless.sh", "./hack/test-integration.sh"] # test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns +RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ + slirp4netns COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf RUN chown -R rootless:rootless /home/rootless/.config diff --git a/Dockerfile.d/SHA256SUMS.d/SHA256SUMS b/Dockerfile.d/SHA256SUMS.d/SHA256SUMS deleted file mode 100644 index f9bb64f0557..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/SHA256SUMS +++ /dev/null @@ -1,6 +0,0 @@ -3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz -6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz -730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz -05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz -199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz -afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 deleted file mode 100644 index f9bb64f0557..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.6 +++ /dev/null @@ -1,6 +0,0 @@ -3edc52986c442576da856a66b59a61d16cf765359712c5ecf2d147c69f0df6e9 rootlesskit-aarch64.tar.gz -6ce9eed50f9e12f18f3e5197cf93d226bc9290185880a626ab186244593d2eed rootlesskit-armv7l.tar.gz -730ef884439e2fe15551218b05d5c4f96d96d6945db8ad7e89b1d12946408a8d rootlesskit-ppc64le.tar.gz -05da5803d0f023ec51112bbdf8967a3e12ae19544f8c101a7f08f3bb9c6548fd rootlesskit-riscv64.tar.gz -199f6bfcd0495d0b944d95f70e6fa1177ace16d801e2693fdd86fdaafa69b01a rootlesskit-s390x.tar.gz -afc52e9fa2f7a2d4bb692f675cf3d2f70f3a184f02593e8b18cfbbbc34cbfd41 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 new file mode 100644 index 00000000000..c3a4d72a3db --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 @@ -0,0 +1,6 @@ +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/slirp4netns-v1.3.3 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 deleted file mode 100644 index a40e6aee074..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.3 +++ /dev/null @@ -1,7 +0,0 @@ -d0e6a13342efbedb8b7454629a0e9ce9b7a937c261034c85f46ed81af76307d8 SOURCE_DATE_EPOCH -1ca9d2f5f1fb4beb91f354653e5dad35b95c049afb264268d99a96ff2a10d903 slirp4netns-aarch64 -3e209d1c56fccbe627a038d311b233c15e8d914b30f9b981b5ed78b98e836859 slirp4netns-armv7l -4d1003a98103ee170c0fcd4aad8a5e0ba7aa2e70fbca883cbb6a39f40447c8da slirp4netns-ppc64le -06a13b398d88120097b20dace966d7dd5e2fbfd284b95a086347808df392200e slirp4netns-riscv64 -23d4a206edd6d3fc9c86f8b05c0881ff77a607b8d471f20964ad9f9c3f3176b1 slirp4netns-s390x -5618887b671a30a2f7548f2bdf7fba98a53981abc80cfd3183cd28b4dc8b2b97 slirp4netns-x86_64 diff --git a/README.md b/README.md index d01e10fc3b8..3b00cf2ec30 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,8 @@ In addition to containerd, the following components should be installed: - v1.1.0 or later is highly recommended. - [BuildKit](https://github.com/moby/buildkit) (OPTIONAL): for using `nerdctl build`. BuildKit daemon (`buildkitd`) needs to be running. See also [the document about setting up BuildKit](./docs/build.md). - v0.11.0 or later is highly recommended. Some features, such as pruning caches with `nerdctl system prune`, do not work with older versions. -- [RootlessKit](https://github.com/rootless-containers/rootlesskit) and [slirp4netns](https://github.com/rootless-containers/slirp4netns) (OPTIONAL): for [Rootless mode](./docs/rootless.md) - - RootlessKit needs to be v0.10.0 or later. v2.0.0 or later is recommended. - - slirp4netns needs to be v0.4.0 or later. v1.1.7 or later is recommended. +- [RootlessKit](https://github.com/rootless-containers/rootlesskit) (OPTIONAL): for [Rootless mode](./docs/rootless.md) + - RootlessKit needs to be v0.10.0 or later. v3.0.0 or later is recommended. These dependencies are included in `nerdctl-full---.tar.gz`, but not included in `nerdctl---.tar.gz`. diff --git a/docs/faq.md b/docs/faq.md index eef678ca9d2..69595442e51 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -311,9 +311,9 @@ See also: - https://rootlesscontaine.rs/getting-started/containerd/ ### `nerdctl run -p ` does not propagate source IP -Expected behavior with the default `rootlesskit` port driver. +Make sure that nerdctl is running with RootlessKit v3.0 or later. -The solution is to change the port driver to `slirp4netns` (sacrifices performance). +For older version of RootlessKit, change the port driver to `slirp4netns` (sacrifices performance). See https://rootlesscontaine.rs/getting-started/containerd/#changing-the-port-forwarder . diff --git a/docs/rootless.md b/docs/rootless.md index 4b3f593f760..011fa3ff7b2 100644 --- a/docs/rootless.md +++ b/docs/rootless.md @@ -153,9 +153,9 @@ More detail is available at [https://github.com/rootless-containers/bypass4netns Rootless containerd recognizes the following environment variables to configure the behavior of [RootlessKit](https://github.com/rootless-containers/rootlesskit): * `CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR`: the rootlesskit state dir. Defaults to `$XDG_RUNTIME_DIR/containerd-rootless`. -* `CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|lxc-user-nic)`: the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "vpnkit". -* `CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM`: the MTU value for the rootlesskit network driver. Defaults to 65520 for slirp4netns, 1500 for other drivers. -* `CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns)`: the rootlesskit port driver. Defaults to "builtin" (this driver does not propagate the container's source IP address and always uses 127.0.0.1. Please check [Port Drivers](https://github.com/rootless-containers/rootlesskit/blob/master/docs/port.md#port-drivers) for more details). +* `CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|pasta|gvisor-tap-vsock|lxc-user-nic)`: the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "gvisor-tap-vsock". +* `CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM`: the MTU value for the rootlesskit network driver. Defaults to 65520 or 1500, depending on the network driver. +* `CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|implicit|gvisor-tap-vsock)`: the rootlesskit port driver. Defaults to "builtin". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false)`: whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false)`: whether to protect slirp4netns with seccomp. Defaults to "auto". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false)`: whether to launch rootlesskit with the "detach-netns" mode. diff --git a/extras/rootless/containerd-rootless-setuptool.sh b/extras/rootless/containerd-rootless-setuptool.sh index c0f754e37b8..d4caaa941eb 100755 --- a/extras/rootless/containerd-rootless-setuptool.sh +++ b/extras/rootless/containerd-rootless-setuptool.sh @@ -104,7 +104,6 @@ cmd_entrypoint_check() { init INFO "Checking RootlessKit functionality" if ! rootlesskit \ - --net=slirp4netns \ --disable-host-loopback \ --copy-up=/etc --copy-up=/run --copy-up=/var/lib \ true; then diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh index f569484a574..b992b5744be 100755 --- a/extras/rootless/containerd-rootless.sh +++ b/extras/rootless/containerd-rootless.sh @@ -28,14 +28,13 @@ # External dependencies: # * newuidmap and newgidmap needs to be installed. # * /etc/subuid and /etc/subgid needs to be configured for the current user. -# * RootlessKit (>= v0.10.0) needs to be installed. RootlessKit >= v2.0.0 is recommended. -# * Either one of slirp4netns (>= v0.4.0), VPNKit, lxc-user-nic needs to be installed. slirp4netns >= v1.1.7 is recommended. +# * RootlessKit (>= v0.10.0) needs to be installed. RootlessKit >= v3.0.0 is recommended. # # Recognized environment variables: # * CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR: the rootlesskit state dir. Defaults to "$XDG_RUNTIME_DIR/containerd-rootless". -# * CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|lxc-user-nic): the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "vpnkit". -# * CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM: the MTU value for the rootlesskit network driver. Defaults to 65520 for slirp4netns, 1500 for other drivers. -# * CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns): the rootlesskit port driver. Defaults to "builtin". +# * CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|pasta|gvisor-tap-vsock|lxc-user-nic): the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "gvisor-tap-vsock". +# * CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM: the MTU value for the rootlesskit network driver. Defaults to 65520 or 1500, depending on the network driver. +# * CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|implicit|gvisor-tap-vsock): the rootlesskit port driver. Defaults to "builtin". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false): whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false): whether to protect slirp4netns with seccomp. Defaults to "auto". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false): whether to launch rootlesskit with the "detach-netns" mode. @@ -90,15 +89,17 @@ if [ -z "$_CONTAINERD_ROOTLESS_CHILD" ]; then mtu=65520 fi else - echo "slirp4netns found but seems older than v0.4.0. Falling back to VPNKit." + echo "slirp4netns found but seems older than v0.4.0. Falling back to other drivers." fi fi if [ -z "$net" ]; then if command -v vpnkit >/dev/null 2>&1; then net=vpnkit else - echo "Either slirp4netns (>= v0.4.0) or vpnkit needs to be installed" - exit 1 + net=gvisor-tap-vsock + if [ -z "$mtu" ]; then + mtu=65520 + fi fi fi fi diff --git a/go.mod b/go.mod index 4d078b30f23..703287185cd 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/runtime-spec v1.3.0 github.com/pelletier/go-toml/v2 v2.3.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v2 v2.3.6 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.0.0 //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 diff --git a/go.sum b/go.sum index 5d426e5a495..01a0d5afd57 100644 --- a/go.sum +++ b/go.sum @@ -275,8 +275,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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/v2 v2.3.6 h1:m/26nAx0DbHZYaM46+uoQjfpu9G77QLzWj2jz25chO8= -github.com/rootless-containers/rootlesskit/v2 v2.3.6/go.mod h1:pv+RESmjRmeUIOsEWOT1f8560CrdaQrDW0YsF4K5kAY= +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/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= diff --git a/hack/build-integration-canary.sh b/hack/build-integration-canary.sh index d94fdc91ea2..7f55cc6e868 100755 --- a/hack/build-integration-canary.sh +++ b/hack/build-integration-canary.sh @@ -46,7 +46,6 @@ dependencies=( containernetworking/plugins rootless-containers/rootlesskit opencontainers/runc - rootless-containers/slirp4netns awslabs/soci-snapshotter containerd/stargz-snapshotter krallin/tini @@ -65,7 +64,6 @@ FUSE_OVERLAYFS_CHECKSUM=linux # Avoids the full build BUILDG_CHECKSUM=buildg-v ROOTLESSKIT_CHECKSUM=linux -SLIRP4NETNS_CHECKSUM=linux STARGZ_SNAPSHOTTER_CHECKSUM=linux # We specifically want the static ones TINI_CHECKSUM=static diff --git a/pkg/bypass4netnsutil/bypass.go b/pkg/bypass4netnsutil/bypass.go index bc9eed11f9d..e3b51158b2f 100644 --- a/pkg/bypass4netnsutil/bypass.go +++ b/pkg/bypass4netnsutil/bypass.go @@ -25,7 +25,7 @@ import ( b4nnapi "github.com/rootless-containers/bypass4netns/pkg/api" "github.com/rootless-containers/bypass4netns/pkg/api/daemon/client" - rlkclient "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + rlkclient "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" "github.com/containerd/errdefs" "github.com/containerd/go-cni" diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 910ff3aebd4..0522949cfea 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -33,7 +33,7 @@ import ( types100 "github.com/containernetworking/cni/pkg/types/100" "github.com/opencontainers/runtime-spec/specs-go" b4nndclient "github.com/rootless-containers/bypass4netns/pkg/api/daemon/client" - rlkclient "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + rlkclient "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" "github.com/containerd/go-cni" "github.com/containerd/log" diff --git a/pkg/ocihook/rootless_linux.go b/pkg/ocihook/rootless_linux.go index 5f908e62d15..47bb1a0ce0d 100644 --- a/pkg/ocihook/rootless_linux.go +++ b/pkg/ocihook/rootless_linux.go @@ -19,7 +19,7 @@ package ocihook import ( "context" - rlkclient "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + rlkclient "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" "github.com/containerd/go-cni" diff --git a/pkg/ocihook/rootless_other.go b/pkg/ocihook/rootless_other.go index ed1485a958a..96cce295df5 100644 --- a/pkg/ocihook/rootless_other.go +++ b/pkg/ocihook/rootless_other.go @@ -22,7 +22,7 @@ import ( "context" "fmt" - rlkclient "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + rlkclient "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" "github.com/containerd/go-cni" ) diff --git a/pkg/rootlessutil/port_linux.go b/pkg/rootlessutil/port_linux.go index dddf37a6f98..7d29fe55411 100644 --- a/pkg/rootlessutil/port_linux.go +++ b/pkg/rootlessutil/port_linux.go @@ -20,8 +20,8 @@ import ( "context" "net" - "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" - "github.com/rootless-containers/rootlesskit/v2/pkg/port" + "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" + "github.com/rootless-containers/rootlesskit/v3/pkg/port" "github.com/containerd/errdefs" "github.com/containerd/go-cni" diff --git a/pkg/rootlessutil/rootlessutil_linux.go b/pkg/rootlessutil/rootlessutil_linux.go index bb5349f255f..492160acdfa 100644 --- a/pkg/rootlessutil/rootlessutil_linux.go +++ b/pkg/rootlessutil/rootlessutil_linux.go @@ -24,7 +24,7 @@ import ( "strconv" "github.com/containernetworking/plugins/pkg/ns" - "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" ) func IsRootless() bool { diff --git a/pkg/rootlessutil/rootlessutil_other.go b/pkg/rootlessutil/rootlessutil_other.go index 4ebd5c1d832..d9723e624b4 100644 --- a/pkg/rootlessutil/rootlessutil_other.go +++ b/pkg/rootlessutil/rootlessutil_other.go @@ -25,7 +25,7 @@ package rootlessutil import ( "fmt" - "github.com/rootless-containers/rootlesskit/v2/pkg/api/client" + "github.com/rootless-containers/rootlesskit/v3/pkg/api/client" ) // Always returns false on non-Linux platforms. From d0b91cb5d223c39d1666c4f7a70caee2412703e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:32:50 +0000 Subject: [PATCH 487/868] build(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.21. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-version: 0.0.21 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index f96ae5866d3..e19c095e5ee 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.0 github.com/klauspost/compress v1.18.5 - github.com/mattn/go-isatty v0.0.20 //gomodjail:unconfined + github.com/mattn/go-isatty v0.0.21 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 784e57b4f4d..0951feda93c 100644 --- a/go.sum +++ b/go.sum @@ -187,8 +187,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -423,7 +423,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From b879c1e1c3c53aff4edc8771e7456d6df6a7c591 Mon Sep 17 00:00:00 2001 From: Federico Bramucci <163430291+fedebram@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:16:13 +0200 Subject: [PATCH 488/868] Fix nil pointer panic in commonLock defer The defer function in commonLock calls file.Close() when an error occurs, but file is always nil at that point. If os.Open or os.OpenFile fails, file is nil. If platformSpecificLock fails, file is closed inline and then set to nil by the return statement. In both cases, calling file.Close() in the defer panics on a nil pointer. Add nil check before file.Close() in the defer. Signed-off-by: Federico Bramucci <163430291+fedebram@users.noreply.github.com> --- pkg/internal/filesystem/lock.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/internal/filesystem/lock.go b/pkg/internal/filesystem/lock.go index d993e380b41..f1169b71db6 100644 --- a/pkg/internal/filesystem/lock.go +++ b/pkg/internal/filesystem/lock.go @@ -51,7 +51,10 @@ func ReadOnlyLock(path string) (file *os.File, err error) { func commonlock(path string, mode lockType) (file *os.File, err error) { defer func() { if err != nil { - err = errors.Join(ErrLockFail, err, file.Close()) + err = errors.Join(ErrLockFail, err) + if file != nil { + err = errors.Join(err, file.Close()) + } } }() From bfff2ee5cf74787286cdc74df794cfba5dd964b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:32:24 +0000 Subject: [PATCH 489/868] build(deps): bump docker/build-push-action from 7.0.0 to 7.1.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 1cc82c817d8..a592fa41951 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -61,7 +61,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64 From f5cc058e31b002781ad10b2d44642c3a8923f500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:32:46 +0000 Subject: [PATCH 490/868] build(deps): bump the golang-x group with 4 updates Bumps the golang-x group with 4 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net), [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/crypto` from 0.49.0 to 0.50.0 - [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0) Updates `golang.org/x/net` from 0.52.0 to 0.53.0 - [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0) Updates `golang.org/x/term` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/term/compare/v0.41.0...v0.42.0) Updates `golang.org/x/text` from 0.35.0 to 0.36.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.35.0...v0.36.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index f96ae5866d3..be5913b3a60 100644 --- a/go.mod +++ b/go.mod @@ -63,12 +63,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.49.0 - golang.org/x/net v0.52.0 + golang.org/x/crypto v0.50.0 + golang.org/x/net v0.53.0 golang.org/x/sync v0.20.0 //gomodjail:unconfined golang.org/x/sys v0.43.0 //gomodjail:unconfined - golang.org/x/term v0.41.0 //gomodjail:unconfined - golang.org/x/text v0.35.0 + golang.org/x/term v0.42.0 //gomodjail:unconfined + golang.org/x/text v0.36.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) @@ -135,7 +135,7 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/mod v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect //gomodjail:unconfined google.golang.org/grpc v1.79.3 // indirect diff --git a/go.sum b/go.sum index 784e57b4f4d..1ca61b6dce6 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= 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= @@ -376,8 +376,8 @@ 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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= 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= @@ -394,8 +394,8 @@ 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= 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= @@ -441,8 +441,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.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= 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= @@ -452,8 +452,8 @@ 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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= 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= @@ -466,8 +466,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= 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= From ebf70c5b878a77e158aacec2257c963f211bd496 Mon Sep 17 00:00:00 2001 From: Shouhei Date: Mon, 6 Apr 2026 08:39:46 +0900 Subject: [PATCH 491/868] fix: ignore missing /proc/net/tcp6 and /proc/net/udp6 on IPv6-disabled systems Signed-off-by: Shouhei --- pkg/portutil/port_allocate_linux.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/portutil/port_allocate_linux.go b/pkg/portutil/port_allocate_linux.go index ddfee7bf9b5..8c0a2b181b0 100644 --- a/pkg/portutil/port_allocate_linux.go +++ b/pkg/portutil/port_allocate_linux.go @@ -17,8 +17,10 @@ package portutil import ( + "errors" "fmt" "net" + "os" "github.com/containerd/nerdctl/v2/pkg/portutil/iptable" "github.com/containerd/nerdctl/v2/pkg/portutil/procnet" @@ -87,14 +89,14 @@ func getUsedPorts(ip string, protocol string) (map[uint64]bool, error) { // So we need some trick to process this situation. if protocol == "tcp" { tempTCPV6Data, err := procnet.ReadStatsFileData("tcp6") - if err != nil { + if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } netprocItems = append(netprocItems, procnet.Parse(tempTCPV6Data)...) } if protocol == "udp" { tempUDPV6Data, err := procnet.ReadStatsFileData("udp6") - if err != nil { + if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } netprocItems = append(netprocItems, procnet.Parse(tempUDPV6Data)...) From 9627cc38c8809994a49f646da1b35a90747f9cb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:15:16 +0000 Subject: [PATCH 492/868] build(deps): bump github.com/ipfs/go-cid from 0.6.0 to 0.6.1 Bumps [github.com/ipfs/go-cid](https://github.com/ipfs/go-cid) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/ipfs/go-cid/releases) - [Commits](https://github.com/ipfs/go-cid/compare/v0.6.0...v0.6.1) --- updated-dependencies: - dependency-name: github.com/ipfs/go-cid dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 86332c1d358..090bfbabb81 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/ipfs/go-cid v0.6.0 + github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.5 github.com/mattn/go-isatty v0.0.21 //gomodjail:unconfined github.com/moby/sys/mount v0.3.4 @@ -105,11 +105,11 @@ require ( github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/symlink v0.3.0 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multiaddr v0.16.1 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect diff --git a/go.sum b/go.sum index 3e1e37a5a4a..07d3e12ec6a 100644 --- a/go.sum +++ b/go.sum @@ -169,8 +169,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= -github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= +github.com/ipfs/go-cid v0.6.1 h1:T5TnNb08+ueovG76Z5gx1L4Y7QOaGTXHg1F6raWFxIc= +github.com/ipfs/go-cid v0.6.1/go.mod h1:zrY0SwOhjrrIdfPQ/kf+k1sXyJ0QE7cMxfCployLBs0= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= @@ -227,8 +227,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -237,8 +237,8 @@ github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= From 1980eb35d7dd4eb1abeb8edd034283915a02a579 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:52:08 +0000 Subject: [PATCH 493/868] build(deps): bump actions/cache from 5.0.4 to 5.0.5 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 63b41fdd514..22874665162 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -39,7 +39,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index a2b9dfc0ef2..53062f27dc3 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: "Init: setup cache" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: /root/.vagrant.d key: vagrant From 1c0d92470e3956187d58bcb5f30c01edb4eae73e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 14 Apr 2026 18:35:14 +0900 Subject: [PATCH 494/868] CI: drop Go 1.25 containerd v2.3 will not support Go 1.25. See containerd/containerd PR 13090. Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 6fe7e2bea2d..36d97bd573a 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -67,8 +67,6 @@ jobs: fail-fast: false matrix: include: - # Build for both old and stable go - - go-version: "1.25" - go-version: "1.26" # Additionally build for canary - go-version: "1.26" From e6b4e4a39f7e3b8b54fcb4357db1143939ac025f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 14 Apr 2026 18:34:29 +0900 Subject: [PATCH 495/868] go.mod: github.com/containerd/containerd/v2 v2.3.0-beta.1 Signed-off-by: Akihiro Suda --- go.mod | 28 ++++---- go.sum | 66 ++++++++++--------- .../container_network_manager.go | 2 +- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index 8a9f066e749..f656e9d7c94 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.25.0 +go 1.26.2 require ( github.com/Masterminds/semver/v3 v3.4.0 - github.com/Microsoft/go-winio v0.6.2 - github.com/Microsoft/hcsshim v0.14.0-rc.1 + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/hcsshim v0.15.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.2 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.1 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.10.0 - github.com/containerd/containerd/v2 v2.2.2 //gomodjail:unconfined + github.com/containerd/containerd/api v1.11.0-beta.1 + github.com/containerd/containerd/v2 v2.3.0-beta.1 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -79,7 +79,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/plugin v1.0.0 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/ttrpc v1.2.8 // indirect github.com/containers/ocicrypt v1.2.1 // indirect github.com/creack/pty v1.1.24 // indirect github.com/djherbis/times v1.6.0 // indirect @@ -113,7 +113,6 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect - github.com/opencontainers/selinux v1.13.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -129,18 +128,18 @@ 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.60.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.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.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect //gomodjail:unconfined - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/grpc v1.80.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect @@ -148,7 +147,6 @@ require ( ) require ( - cyphar.com/go-pathrs v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/moby/moby/api v1.52.0 // indirect github.com/moby/moby/client v0.1.0 // indirect diff --git a/go.sum b/go.sum index 61e5973c72b..be8cbd3516a 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= -github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= +github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -31,10 +31,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= -github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= -github.com/containerd/containerd/v2 v2.2.2 h1:mjVQdtfryzT7lOqs5EYUFZm8ioPVjOpkSoG1GJPxEMY= -github.com/containerd/containerd/v2 v2.2.2/go.mod h1:5Jhevmv6/2J+Iu/A2xXAdUIdI5Ah/hfyO7okJ4AFIdY= +github.com/containerd/containerd/api v1.11.0-beta.1 h1:3d96IaaVarwsaR5QuYHD+cW8UKgS8V7NLvBx5boILEw= +github.com/containerd/containerd/api v1.11.0-beta.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= +github.com/containerd/containerd/v2 v2.3.0-beta.1 h1:n1tb00CL1g75i8Yss5/xqdjSyxTRvyuurw4aMCWhZKI= +github.com/containerd/containerd/v2 v2.3.0-beta.1/go.mod h1:2WhixvQvQ2HbiV8urFbkG09e7OqlqO/9as3/mF43QE0= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -63,8 +63,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+XesH/6BBuJcdtV6ymGlGg= github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/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/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= @@ -106,6 +106,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erofs/go-erofs v0.2.0 h1:LoqBN0t85zH74ozeSS6eyw6sRGRjYdR5T0Z2LweoXvo= +github.com/erofs/go-erofs v0.2.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -269,8 +271,8 @@ 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.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 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= @@ -329,18 +331,18 @@ 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.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +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.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +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.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= @@ -471,22 +473,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +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= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/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.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +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/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= @@ -496,8 +498,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index 3638b450917..3c1924e2bd9 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -170,7 +170,7 @@ func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOp // put the container in the specified network namespace instead of the root. manager = &hostNetworkManager{globalOptions, netOpts, client} default: - return nil, fmt.Errorf("unexpected container networking type: %q", netType) + return nil, fmt.Errorf("unexpected container networking type: %v", netType) } return manager, nil From f35a1ef841d44d0ae99ef41efdb2900773189d4f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 14 Apr 2026 18:37:22 +0900 Subject: [PATCH 496/868] update containerd (2.3.0-beta.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index f42591d94f2..55dbaf2ada3 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,9 +146,9 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.2.1 + containerd-version: 2.3.0-beta.1 # Note: these as for amd64 - containerd-sha: f5d8e90ecb6c1c7e33ecddf8cc268a93b9e5b54e0e850320d765511d76624f41 + containerd-sha: sha256:34fb28c7d9e80cee64d995059043df33460346cf89bbe85bd5b0692e09dfcb4a containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index 0c820e156c2..ffa235ac47b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.2.1@dea7da592f5d1d2b7755e3a161be07f43fad8f75 +ARG CONTAINERD_VERSION=v2.3.0-beta.1@212b10b249ea8e541344f2d9f343d34ca3c29e4a ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From 2c686a4df75a8152c5b8051c02802adb453cf059 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:32:42 +0000 Subject: [PATCH 497/868] build(deps): bump github.com/docker/go-connections from 0.6.0 to 0.7.0 Bumps [github.com/docker/go-connections](https://github.com/docker/go-connections) from 0.6.0 to 0.7.0. - [Commits](https://github.com/docker/go-connections/compare/v0.6.0...v0.7.0) --- updated-dependencies: - dependency-name: github.com/docker/go-connections dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8a9f066e749..61b696bc9f9 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/docker/cli v29.4.0+incompatible //gomodjail:unconfined github.com/docker/docker v28.5.2+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.6.0 + 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 github.com/fatih/color v1.19.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 61e5973c72b..68ea7408d02 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaft github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= From debaf77c21dd69a5c3946bc848b34f5a23aac97e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:35:54 +0000 Subject: [PATCH 498/868] build(deps): bump github.com/containerd/accelerated-container-image Bumps [github.com/containerd/accelerated-container-image](https://github.com/containerd/accelerated-container-image) from 1.4.1 to 1.4.3. - [Release notes](https://github.com/containerd/accelerated-container-image/releases) - [Commits](https://github.com/containerd/accelerated-container-image/compare/v1.4.1...v1.4.3) --- updated-dependencies: - dependency-name: github.com/containerd/accelerated-container-image dependency-version: 1.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f656e9d7c94..5a616999bc0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.2 //gomodjail:unconfined - github.com/containerd/accelerated-container-image v1.4.1 + github.com/containerd/accelerated-container-image v1.4.3 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.0-beta.1 diff --git a/go.sum b/go.sum index be8cbd3516a..42bdb2ebc2e 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= github.com/compose-spec/compose-go/v2 v2.10.2/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= -github.com/containerd/accelerated-container-image v1.4.1 h1:jeZYAaq5pMCeyRZ0I916OjJsEb2TGjAQmfAZyQLi3ec= -github.com/containerd/accelerated-container-image v1.4.1/go.mod h1:rhqPgQ63sgkYHY56pAVl0NBN+lDJYgzgZW9m781nnWg= +github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= +github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= From 86e8e50158a1168efbc28edd3fc533aa2c448455 Mon Sep 17 00:00:00 2001 From: ayush-panta Date: Wed, 15 Apr 2026 13:58:59 -0700 Subject: [PATCH 499/868] feat: add missing HostConfig fields to dockercompat inspect response Signed-off-by: ayush-panta --- .../container/container_inspect_linux_test.go | 97 +++++++ pkg/cmd/container/create.go | 7 + pkg/inspecttypes/dockercompat/dockercompat.go | 177 ++++++++++-- .../dockercompat/dockercompat_test.go | 256 +++++++++++++++++- pkg/labels/labels.go | 3 + 5 files changed, 511 insertions(+), 29 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 0c3f0423703..8f7d956a61c 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -391,10 +391,16 @@ func TestContainerInspectHostConfig(t *testing.T) { "--add-host", "host2:10.0.0.2", "--ipc", "host", "--memory", "512m", + "--memory-reservation", "200m", + "--memory-swappiness", "60", + "--pids-limit", "100", + "--ulimit", "nofile=1024:65536", "--read-only", "--shm-size", "256m", "--uts", "host", + "--restart", "on-failure:3", "--runtime", "io.containerd.runc.v2", + "--annotation", "com.example.key=test-val", testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, data.Identifier()) } @@ -430,6 +436,21 @@ func TestContainerInspectHostConfig(t *testing.T) { assert.Equal(tt, true, inspect.HostConfig.ReadonlyRootfs) assert.Equal(tt, "host", inspect.HostConfig.UTSMode) assert.Equal(tt, int64(268435456), inspect.HostConfig.ShmSize) + assert.Equal(tt, int64(209715200), inspect.HostConfig.MemoryReservation) + assert.Equal(tt, int64(100), inspect.HostConfig.PidsLimit) + assert.Equal(tt, 1, len(inspect.HostConfig.Ulimits)) + assert.Equal(tt, "nofile", inspect.HostConfig.Ulimits[0].Name) + assert.Equal(tt, int64(65536), inspect.HostConfig.Ulimits[0].Hard) + assert.Equal(tt, int64(1024), inspect.HostConfig.Ulimits[0].Soft) + assert.Equal(tt, "on-failure", inspect.HostConfig.RestartPolicy.Name) + assert.Equal(tt, 3, inspect.HostConfig.RestartPolicy.MaximumRetryCount) + if !nerdtest.IsDocker() { + // The docker CI runner warns "Your kernel does not support memory + // swappiness capabilities or the cgroup is not mounted" and returns null. + assert.Assert(tt, inspect.HostConfig.MemorySwappiness != nil) + assert.Equal(tt, int64(60), *inspect.HostConfig.MemorySwappiness) + } + assert.Equal(tt, "test-val", inspect.HostConfig.Annotations["com.example.key"]) }) testCase.Run(t) @@ -509,6 +530,14 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) { assert.Equal(tt, hc.ShmSize, inspect.HostConfig.ShmSize) assert.Equal(tt, hc.Runtime, inspect.HostConfig.Runtime) assert.Equal(tt, 0, len(inspect.HostConfig.Devices)) + assert.Equal(tt, false, inspect.HostConfig.Privileged) + assert.Equal(tt, false, inspect.HostConfig.AutoRemove) + assert.Equal(tt, int64(0), inspect.HostConfig.MemoryReservation) + assert.Equal(tt, int64(0), inspect.HostConfig.PidsLimit) + assert.Equal(tt, 0, len(inspect.HostConfig.CapAdd)) + assert.Equal(tt, 0, len(inspect.HostConfig.CapDrop)) + assert.Equal(tt, 0, len(inspect.HostConfig.Ulimits)) + assert.Assert(tt, inspect.HostConfig.MemorySwappiness == nil) // Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment. got := len(inspect.HostConfig.Sysctls) @@ -881,3 +910,71 @@ type hostConfigValues struct { GroupAddSize int Runtime string } + +func TestContainerInspectHostConfigPrivileged(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--privileged", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + assert.Equal(tt, true, inspect.HostConfig.Privileged) + }) + + testCase.Run(t) +} + +func TestContainerInspectHostConfigCapabilities(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--cap-add", "NET_ADMIN", + "--cap-drop", "CHOWN", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, 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 { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(tt, err) + assert.Equal(tt, 1, len(dc)) + + inspect := dc[0] + assert.Assert(tt, slices.Contains(inspect.HostConfig.CapAdd, "CAP_NET_ADMIN"), + "Expected CAP_NET_ADMIN in CapAdd") + assert.Assert(tt, slices.Contains(inspect.HostConfig.CapDrop, "CAP_CHOWN"), + "Expected CAP_CHOWN in CapDrop") + }) + + testCase.Run(t) +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 7ebc3b702d6..bf44e02c22c 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -395,6 +395,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.extraHosts = extraHosts internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm) + internalLabels.privileged = options.Privileged // TODO: abolish internal labels and only use annotations ilOpt, err := withInternalLabels(internalLabels) @@ -765,6 +766,8 @@ type internalLabels struct { user string healthcheck string + + privileged bool } // WithInternalLabels sets the internal labels for a container. @@ -845,6 +848,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.ContainerAutoRemove] = internalLabels.rm } + if internalLabels.privileged { + m[labels.Privileged] = "true" + } + if internalLabels.cidFile != "" { hostConfigLabel.CidFile = internalLabels.cidFile } diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index d684c1feced..7bb1f4ea9b7 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -142,26 +142,27 @@ type HostConfig struct { // Binds []string // List of volume bindings for this container ContainerIDFile string // File (path) where the containerId is written LogConfig loggerLogConfig // Configuration of the logs for this container - // NetworkMode NetworkMode // Network mode to use for the container - PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host - // RestartPolicy RestartPolicy // Restart policy to be used for the container - // AutoRemove bool // Automatically remove container when it exits + NetworkMode string // Network mode to use for the container + PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host + RestartPolicy RestartPolicy // Restart policy to be used for the container + AutoRemove bool // Automatically remove container when it exits // VolumeDriver string // Name of the volume driver used to mount volumes // VolumesFrom []string // List of volumes to take from other container - // CapAdd strslice.StrSlice // List of kernel capabilities to add to the container - // CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container - - CgroupnsMode string // Cgroup namespace mode to use for the container - DNS []string `json:"Dns"` // List of DNS server to lookup - DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for - DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for - ExtraHosts []string // List of extra hosts - GroupAdd []string // GroupAdd specifies additional groups to join - IpcMode string `json:"IpcMode"` // IPC namespace to use for the container + CapAdd []string // List of kernel capabilities to add to the container + CapDrop []string // List of kernel capabilities to remove from the container + + CgroupnsMode string // Cgroup namespace mode to use for the container + DNS []string `json:"Dns"` // List of DNS server to lookup + DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for + DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for + ExtraHosts []string // List of extra hosts + GroupAdd []string // GroupAdd specifies additional groups to join + IpcMode string `json:"IpcMode"` // IPC namespace to use for the container + Annotations map[string]string `json:",omitempty"` // Arbitrary non-identifying metadata attached to container and provided to the runtime // Cgroup CgroupSpec // Cgroup to use for the container OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) PidMode string // PID namespace to use for the container - // Privileged bool // Is the container in privileged mode + Privileged bool // Is the container in privileged mode // PublishAllPorts bool // Should docker publish all exposed port for the container ReadonlyRootfs bool // Is the container root filesystem in read-only // SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. @@ -180,6 +181,10 @@ type HostConfig struct { CPURealtimeRuntime int64 `json:"CpuRealtimeRuntime"` // Limits the CPU real-time runtime in microseconds Memory int64 // Memory limit (in bytes) MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap + MemoryReservation int64 // Memory soft limit (in bytes) + MemorySwappiness *int64 // Tuning container memory swappiness (0 to 100); nil means not set + PidsLimit int64 // Setting PIDs limit for a container; 0 or -1 for unlimited + Ulimits []*units.Ulimit // List of ulimits to be set in the container OomKillDisable bool // specifies whether to disable OOM Killer Devices []DeviceMapping // List of devices to map inside the container BlkioSettings @@ -268,6 +273,12 @@ type DeviceMapping struct { CgroupPermissions string } +// RestartPolicy represents the restart policies of the container. +type RestartPolicy struct { + Name string + MaximumRetryCount int +} + type CPUSettings struct { CPUSetCpus string CPUSetMems string @@ -309,6 +320,26 @@ type NetworkEndpointSettings struct { // TODO DriverOpts map[string]string } +// defaultCaps mirrors containerd's defaultUnixCaps() — the 14 capabilities +// granted to non-privileged containers by default. Used as the baseline for +// reconstructing CapAdd/CapDrop from the OCI spec's bounding set. +var defaultCaps = map[string]struct{}{ + "CAP_CHOWN": {}, + "CAP_DAC_OVERRIDE": {}, + "CAP_FSETID": {}, + "CAP_FOWNER": {}, + "CAP_MKNOD": {}, + "CAP_NET_RAW": {}, + "CAP_SETGID": {}, + "CAP_SETUID": {}, + "CAP_SETFCAP": {}, + "CAP_SETPCAP": {}, + "CAP_NET_BIND_SERVICE": {}, + "CAP_SYS_CHROOT": {}, + "CAP_KILL": {}, + "CAP_AUDIT_WRITE": {}, +} + // ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container. func ContainerFromNative(n *native.Container) (*Container, error) { var hostname string @@ -500,6 +531,11 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.HostConfig.OomKillDisable = memorySettings.DisableOOMKiller c.HostConfig.Memory = memorySettings.Limit c.HostConfig.MemorySwap = memorySettings.Swap + c.HostConfig.MemoryReservation = memorySettings.Reservation + if memorySettings.Swappiness != nil { + swappiness := int64(*memorySettings.Swappiness) + c.HostConfig.MemorySwappiness = &swappiness + } dnsSettings, err := getDNSFromNative(n.Labels) if err != nil { @@ -573,6 +609,63 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.Config.User = n.Labels[labels.User] } + capAdd, capDrop, err := getCapabilitiesFromNative(n.Spec.(*specs.Spec)) + if err != nil { + return nil, fmt.Errorf("failed to get capabilities: %w", err) + } + c.HostConfig.CapAdd = capAdd + c.HostConfig.CapDrop = capDrop + + ulimits, err := getUlimitsFromNative(n.Spec.(*specs.Spec)) + if err != nil { + return nil, fmt.Errorf("failed to get ulimits: %w", err) + } + c.HostConfig.Ulimits = ulimits + + if policyStr := n.Labels[restart.PolicyLabel]; policyStr != "" { + rp, err := restart.NewPolicy(policyStr) + if err != nil { + return nil, fmt.Errorf("failed to parse restart policy: %w", err) + } + c.HostConfig.RestartPolicy = RestartPolicy{ + Name: rp.Name(), + MaximumRetryCount: rp.MaximumRetryCount(), + } + } + + if len(containerAnnotations) > 0 { + userAnnotations := make(map[string]string) + for k, v := range containerAnnotations { + if !strings.HasPrefix(k, labels.Prefix) { + userAnnotations[k] = v + } + } + if len(userAnnotations) > 0 { + c.HostConfig.Annotations = userAnnotations + } + } + + if sp, ok := n.Spec.(*specs.Spec); ok { + if sp.Linux != nil && sp.Linux.Resources != nil && + sp.Linux.Resources.Pids != nil && sp.Linux.Resources.Pids.Limit != nil { + c.HostConfig.PidsLimit = *sp.Linux.Resources.Pids.Limit + } + } + + if networksJSON := n.Labels[labels.Networks]; networksJSON != "" { + var networks []string + if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil { + return nil, fmt.Errorf("failed to parse networks label: %v", err) + } + if len(networks) > 0 { + c.HostConfig.NetworkMode = networks[0] + } + } + + c.HostConfig.Privileged = n.Labels[labels.Privileged] == "true" + + c.HostConfig.AutoRemove = n.Labels[labels.ContainerAutoRemove] == "true" + // Add health check config if present in labels if hConfig, ok := n.Labels[labels.HealthCheck]; ok && hConfig != "" { healthCheckConfig, err := healthcheck.HealthCheckFromJSON(hConfig) @@ -850,6 +943,15 @@ func getMemorySettingsFromNative(sp *specs.Spec) (*MemorySetting, error) { if sp.Linux.Resources.Memory.Swap != nil { res.Swap = *sp.Linux.Resources.Memory.Swap } + + if sp.Linux.Resources.Memory.Reservation != nil { + res.Reservation = *sp.Linux.Resources.Memory.Reservation + } + + if sp.Linux.Resources.Memory.Swappiness != nil { + v := *sp.Linux.Resources.Memory.Swappiness + res.Swappiness = &v + } } return res, nil } @@ -904,6 +1006,43 @@ func getSysctlFromNative(sp *specs.Spec) (map[string]string, error) { return res, nil } +func getCapabilitiesFromNative(sp *specs.Spec) (capAdd, capDrop []string, err error) { + if sp.Process == nil || sp.Process.Capabilities == nil { + return nil, nil, nil + } + capAdd = []string{} + capDrop = []string{} + boundingSet := make(map[string]struct{}, len(sp.Process.Capabilities.Bounding)) + for _, cap := range sp.Process.Capabilities.Bounding { + boundingSet[cap] = struct{}{} + if _, isDefault := defaultCaps[cap]; !isDefault { + capAdd = append(capAdd, cap) + } + } + for cap := range defaultCaps { + if _, present := boundingSet[cap]; !present { + capDrop = append(capDrop, cap) + } + } + return capAdd, capDrop, nil +} + +func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) { + if sp.Process == nil || len(sp.Process.Rlimits) == 0 { + return nil, nil + } + ulimits := make([]*units.Ulimit, 0, len(sp.Process.Rlimits)) + for _, rl := range sp.Process.Rlimits { + name := strings.ToLower(strings.TrimPrefix(rl.Type, "RLIMIT_")) + ulimits = append(ulimits, &units.Ulimit{ + Name: name, + Hard: int64(rl.Hard), + Soft: int64(rl.Soft), + }) + } + return ulimits, nil +} + type IPAMConfig struct { Subnet string `json:"Subnet,omitempty"` Gateway string `json:"Gateway,omitempty"` @@ -944,9 +1083,11 @@ type structuredCNI struct { } type MemorySetting struct { - Limit int64 `json:"limit"` - Swap int64 `json:"swap"` - DisableOOMKiller bool `json:"disableOOMKiller"` + Limit int64 `json:"limit"` + Swap int64 `json:"swap"` + Reservation int64 `json:"reservation"` + Swappiness *uint64 `json:"swappiness"` + DisableOOMKiller bool `json:"disableOOMKiller"` } // parseNetworkSubnets extracts and parses subnet configurations from IPAM config diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index a4dbec2d4f3..945228d7323 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/docker/go-connections/nat" + "github.com/docker/go-units" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" @@ -70,15 +71,46 @@ 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/state-dir": tempStateDir, - "nerdctl/hostname": "host1", - "nerdctl/user": "test-user", + "nerdctl/mounts": "[{\"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", + "nerdctl/networks": `["my-net"]`, + "nerdctl/privileged": "true", + "nerdctl/auto-remove": "true", + "containerd.io/restart.policy": "on-failure:3", }, }, Spec: &specs.Spec{ Process: &specs.Process{ Env: []string{"/some/path"}, + Capabilities: &specs.LinuxCapabilities{ + Bounding: []string{ + "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER", + "CAP_MKNOD", "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", + "CAP_SETFCAP", "CAP_SETPCAP", "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", "CAP_KILL", "CAP_AUDIT_WRITE", + "CAP_NET_ADMIN", + }, + }, + Rlimits: []specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Hard: 65536, Soft: 1024}, + }, + }, + Linux: &specs.Linux{ + Resources: &specs.LinuxResources{ + Memory: &specs.LinuxMemory{ + Reservation: func() *int64 { v := int64(209715200); return &v }(), + Swappiness: func() *uint64 { v := uint64(60); return &v }(), + }, + Pids: &specs.LinuxPids{ + Limit: func() *int64 { v := int64(100); return &v }(), + }, + }, + }, + Annotations: map[string]string{ + "nerdctl/state-dir": tempStateDir, + "com.example.key": "user-val", }, }, Process: &native.Process{ @@ -105,9 +137,20 @@ func TestContainerFromNative(t *testing.T) { Driver: "json-file", Opts: map[string]string{}, }, - UTSMode: "host", - Tmpfs: map[string]string{}, - BlkioSettings: getDefaultBlkioSettings(), + UTSMode: "host", + Tmpfs: map[string]string{}, + BlkioSettings: getDefaultBlkioSettings(), + NetworkMode: "my-net", + Privileged: true, + AutoRemove: true, + RestartPolicy: RestartPolicy{Name: "on-failure", MaximumRetryCount: 3}, + CapAdd: []string{"CAP_NET_ADMIN"}, + CapDrop: []string{}, + Ulimits: []*units.Ulimit{{Name: "nofile", Hard: 65536, Soft: 1024}}, + MemoryReservation: 209715200, + MemorySwappiness: func() *int64 { v := int64(60); return &v }(), + PidsLimit: 100, + Annotations: map[string]string{"com.example.key": "user-val"}, }, Mounts: []MountPoint{ { @@ -121,10 +164,14 @@ 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/state-dir": tempStateDir, - "nerdctl/hostname": "host1", - "nerdctl/user": "test-user", + "nerdctl/mounts": "[{\"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", + "nerdctl/networks": `["my-net"]`, + "nerdctl/privileged": "true", + "nerdctl/auto-remove": "true", + "containerd.io/restart.policy": "on-failure:3", }, Hostname: "host1", Env: []string{"/some/path"}, @@ -371,6 +418,193 @@ func TestContainerFromNative(t *testing.T) { } } +func TestGetCapabilitiesFromNative(t *testing.T) { + // Build the full default bounding set for test fixtures. + allDefaults := []string{ + "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER", + "CAP_MKNOD", "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", + "CAP_SETFCAP", "CAP_SETPCAP", "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", "CAP_KILL", "CAP_AUDIT_WRITE", + } + + testcases := []struct { + name string + spec *specs.Spec + expectedCapAdd []string + expectedCapDrop []string + }{ + { + name: "default container", + spec: &specs.Spec{ + Process: &specs.Process{ + Capabilities: &specs.LinuxCapabilities{ + Bounding: allDefaults, + }, + }, + }, + expectedCapAdd: []string{}, + expectedCapDrop: []string{}, + }, + { + name: "cap added", + spec: &specs.Spec{ + Process: &specs.Process{ + Capabilities: &specs.LinuxCapabilities{ + Bounding: append(allDefaults, "CAP_NET_ADMIN"), + }, + }, + }, + expectedCapAdd: []string{"CAP_NET_ADMIN"}, + expectedCapDrop: []string{}, + }, + { + name: "cap dropped", + spec: &specs.Spec{ + Process: &specs.Process{ + Capabilities: &specs.LinuxCapabilities{ + Bounding: func() []string { + var caps []string + for _, c := range allDefaults { + if c != "CAP_CHOWN" { + caps = append(caps, c) + } + } + return caps + }(), + }, + }, + }, + expectedCapAdd: []string{}, + expectedCapDrop: []string{"CAP_CHOWN"}, + }, + { + name: "cap added and dropped", + spec: &specs.Spec{ + Process: &specs.Process{ + Capabilities: &specs.LinuxCapabilities{ + Bounding: func() []string { + var caps []string + for _, c := range allDefaults { + if c != "CAP_CHOWN" { + caps = append(caps, c) + } + } + return append(caps, "CAP_NET_ADMIN") + }(), + }, + }, + }, + expectedCapAdd: []string{"CAP_NET_ADMIN"}, + expectedCapDrop: []string{"CAP_CHOWN"}, + }, + { + name: "empty bounding set", + spec: &specs.Spec{ + Process: &specs.Process{ + Capabilities: &specs.LinuxCapabilities{ + Bounding: []string{}, + }, + }, + }, + expectedCapAdd: []string{}, + expectedCapDrop: allDefaults, + }, + { + name: "nil process", + spec: &specs.Spec{}, + expectedCapAdd: nil, + expectedCapDrop: nil, + }, + { + name: "nil capabilities", + spec: &specs.Spec{ + Process: &specs.Process{}, + }, + expectedCapAdd: nil, + expectedCapDrop: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + capAdd, capDrop, err := getCapabilitiesFromNative(tc.spec) + assert.NilError(tt, err) + assert.DeepEqual(tt, capAdd, tc.expectedCapAdd) + // CapDrop order is non-deterministic (map iteration), so check length and contents + if tc.expectedCapDrop == nil { + assert.Assert(tt, capDrop == nil) + } else { + assert.Equal(tt, len(capDrop), len(tc.expectedCapDrop)) + dropSet := make(map[string]struct{}, len(capDrop)) + for _, c := range capDrop { + dropSet[c] = struct{}{} + } + for _, c := range tc.expectedCapDrop { + _, ok := dropSet[c] + assert.Assert(tt, ok, "expected %s in CapDrop", c) + } + } + }) + } +} + +func TestGetUlimitsFromNative(t *testing.T) { + testcases := []struct { + name string + spec *specs.Spec + expected []*units.Ulimit + }{ + { + name: "single rlimit", + spec: &specs.Spec{ + Process: &specs.Process{ + Rlimits: []specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Hard: 65536, Soft: 1024}, + }, + }, + }, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 65536, Soft: 1024}, + }, + }, + { + name: "multiple rlimits", + spec: &specs.Spec{ + Process: &specs.Process{ + Rlimits: []specs.POSIXRlimit{ + {Type: "RLIMIT_NOFILE", Hard: 65536, Soft: 1024}, + {Type: "RLIMIT_NPROC", Hard: 4096, Soft: 2048}, + }, + }, + }, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 65536, Soft: 1024}, + {Name: "nproc", Hard: 4096, Soft: 2048}, + }, + }, + { + name: "no rlimits", + spec: &specs.Spec{ + Process: &specs.Process{}, + }, + expected: nil, + }, + { + name: "nil process", + spec: &specs.Spec{}, + expected: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + result, err := getUlimitsFromNative(tc.spec) + assert.NilError(tt, err) + assert.DeepEqual(tt, result, tc.expected) + }) + } +} + func TestNetworkSettingsFromNative(t *testing.T) { tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index e0838d6ea9c..46ca0e9c4a4 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -128,4 +128,7 @@ const ( // HealthState stores the current health state (status and failing streak). HealthState = Prefix + "healthstate" + + // Privileged indicates whether the container was created with --privileged. + Privileged = Prefix + "privileged" ) From da3d2263d15393e349cb54c3fb3f39bf627943ea Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 18 Apr 2026 03:50:57 +0900 Subject: [PATCH 500/868] go.mod: github.com/containerd/containerd/v2 v2.3.0-beta.2 Signed-off-by: Akihiro Suda --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index f656e9d7c94..75a9bd3ea50 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/containerd/accelerated-container-image v1.4.1 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.0-beta.1 - github.com/containerd/containerd/v2 v2.3.0-beta.1 //gomodjail:unconfined + github.com/containerd/containerd/api v1.11.0-beta.2 + github.com/containerd/containerd/v2 v2.3.0-beta.2 //gomodjail:unconfined github.com/containerd/continuity v0.4.5 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -134,7 +134,7 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.34.0 // indirect + golang.org/x/mod v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect //gomodjail:unconfined google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index be8cbd3516a..a9f821b72cb 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.11.0-beta.1 h1:3d96IaaVarwsaR5QuYHD+cW8UKgS8V7NLvBx5boILEw= -github.com/containerd/containerd/api v1.11.0-beta.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.0-beta.1 h1:n1tb00CL1g75i8Yss5/xqdjSyxTRvyuurw4aMCWhZKI= -github.com/containerd/containerd/v2 v2.3.0-beta.1/go.mod h1:2WhixvQvQ2HbiV8urFbkG09e7OqlqO/9as3/mF43QE0= +github.com/containerd/containerd/api v1.11.0-beta.2 h1:bMyDRSESgCyGQ3xYboaU7fQlUPc6g0HufZ7LDSQnsJ0= +github.com/containerd/containerd/api v1.11.0-beta.2/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= +github.com/containerd/containerd/v2 v2.3.0-beta.2 h1:ra5zjIRSukNZSmNEBd8Rc4U/k1UySHlAYfWroz8vmcA= +github.com/containerd/containerd/v2 v2.3.0-beta.2/go.mod h1:XzMeqc+joRAaJ4ceelXPQhOJ7ApekaR1zRs85cceutE= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -106,8 +106,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erofs/go-erofs v0.2.0 h1:LoqBN0t85zH74ozeSS6eyw6sRGRjYdR5T0Z2LweoXvo= -github.com/erofs/go-erofs v0.2.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= +github.com/erofs/go-erofs v0.2.1 h1:6tFEewfzPTAVrLmNR16hdzQJGH62an9m75gJTbnGEPw= +github.com/erofs/go-erofs v0.2.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -378,8 +378,8 @@ 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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= 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= From e6c71003b96c59072de5e672b9ded1e8c43e270b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:33:11 +0000 Subject: [PATCH 501/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.14 to 0.15.15. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.14...v0.15.15) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.15 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f656e9d7c94..c08db18a370 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 - github.com/containerd/nydus-snapshotter v0.15.14 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.4 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index be8cbd3516a..72c98581302 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtq github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.15.14 h1:rjtDCwtBOLdaGnhjkPI274e9zGy+FBY8A8AaJNLxioU= -github.com/containerd/nydus-snapshotter v0.15.14/go.mod h1:t95dwCb4I0RE4n1iOk0sJCWosNoACA8daOXmU5A2VHI= +github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk= +github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= From ee945806ed5a3a4731406a07ff880ace3ae7a68f Mon Sep 17 00:00:00 2001 From: Shouhei Date: Sun, 5 Apr 2026 15:05:54 +0900 Subject: [PATCH 502/868] fix: support human-readable sizes for fluentd-buffer-limit log option Signed-off-by: Shouhei --- docs/command-reference.md | 2 +- pkg/logging/fluentd_logger.go | 7 ++++++- pkg/logging/fluentd_logger_test.go | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 3d40d9398b1..b5b9cd6cabe 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -365,7 +365,7 @@ Logging flags: - The `fluentd` logging driver supports the following logging options: - :whale: `--log-opt=fluentd-address=
`: The address of the `fluentd` daemon, tcp(default) and unix sockets are supported.. - :whale: `--log-opt=fluentd-async=`: Enable async mode for fluentd. The default value is false. - - :whale: `--log-opt=fluentd-buffer-limit=`: The buffer limit for fluentd. If the buffer is full, the call to record logs will fail. The default is 8192. () + - :whale: `--log-opt=fluentd-buffer-limit=`: The buffer limit for fluentd. If the buffer is full, the call to record logs will fail. The default is 1MiB. Accepts human-readable sizes (e.g., `1KiB`, `1MiB`, `1GiB`) or raw byte values. () - :whale: `--log-opt=fluentd-retry-wait=<1s|1ms>`: The time to wait before retrying to send logs to fluentd. The default value is 1s. - :whale: `--log-opt=fluentd-max-retries=<1>`: The maximum number of retries to send logs to fluentd. The default value is MaxInt32. - :whale: `--log-opt=fluentd-sub-second-precision=`: Enable sub-second precision for fluentd. The default value is false. diff --git a/pkg/logging/fluentd_logger.go b/pkg/logging/fluentd_logger.go index 7dc4c308c11..e281b6dbf2a 100644 --- a/pkg/logging/fluentd_logger.go +++ b/pkg/logging/fluentd_logger.go @@ -27,6 +27,7 @@ import ( "sync" "time" + units "github.com/docker/go-units" "github.com/fluent/fluent-logger-golang/fluent" "github.com/containerd/containerd/v2/core/runtime/v2/logging" @@ -223,10 +224,14 @@ func parseFluentdConfig(config map[string]string) (fluent.Config, error) { } bufferLimit := defaultBufferLimit if config[fluentdBufferLimit] != "" { - bufferLimit, err = strconv.Atoi(config[fluentdBufferLimit]) + parsedBufferLimit, err := units.RAMInBytes(config[fluentdBufferLimit]) if err != nil { return result, fmt.Errorf("error occurs %w,invalid buffer limit (%s)", err, config[fluentdBufferLimit]) } + if parsedBufferLimit > int64(math.MaxInt) { + return result, fmt.Errorf("invalid buffer limit: value %d overflows int", parsedBufferLimit) + } + bufferLimit = int(parsedBufferLimit) } retryWait := int(defaultRetryWait) if config[fluentdRetryWait] != "" { diff --git a/pkg/logging/fluentd_logger_test.go b/pkg/logging/fluentd_logger_test.go index 81babbd9a51..8da21192d22 100644 --- a/pkg/logging/fluentd_logger_test.go +++ b/pkg/logging/fluentd_logger_test.go @@ -229,6 +229,27 @@ func TestParseFluentdConfig(t *testing.T) { AsyncReconnectInterval: 0, SubSecondPrecision: false, RequestAck: true}, false}, + {"HumanReadableBufferLimit", args{ + config: map[string]string{ + fluentdBufferLimit: "1M", + }}, + fluent.Config{ + FluentPort: defaultPort, + FluentHost: defaultHost, + FluentNetwork: defaultProtocol, + FluentSocketPath: "", + BufferLimit: defaultBufferLimit, + RetryWait: int(defaultRetryWait), + MaxRetry: defaultMaxRetries, + Async: false, + AsyncReconnectInterval: 0, + SubSecondPrecision: false, + RequestAck: false}, false}, + {"InvalidHumanReadableBufferLimit", args{ + config: map[string]string{ + fluentdBufferLimit: "not-a-size", + }}, + fluent.Config{}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 1872f6989db86127ec8db424a942ee5686f7d260 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 18 Apr 2026 15:35:15 +0900 Subject: [PATCH 503/868] fix: wait for logger to finish before reading logs of stopped containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TestLogsFollowNoExtraneousLineFeed` sometimes fails on CI. - https://github.com/containerd/nerdctl/actions/runs/24197307638/job/71240723501?pr=4835 - https://github.com/containerd/nerdctl/actions/runs/24197307638/job/71215370984?pr=4835 ```bash +------------------------------------------------------------------------------------------------------------+ | ➡️ | ⚙️ /usr/local/bin/nerdctl.gomodjail run --name testlogsfollownoextraneouslinefeed-f5d64e9b ghcr. | | | io/stargz-containers/alpine:3.13-org sh -c printf 'Hello without newline' | +------------------------------------------------------------------------------------------------------------+ | | 🟢 Hello without newline ... container_logs_test.go:562: 🔗 <<<<<<<<<<<<<<<<<<<< 🖊️ Inspecting output (equals) 👀 testing: `` ❌ FAILED! = `Hello without newline` >>>>>>>>>>>>>>>>>>>> ``` My Investigation revealed that the flakiness of `TestLogsFollowNoExtraneousLineFeed` is caused by `nerdctl logs -f` not waiting for the logger to finish writing when the container has already stopped. Therefore, this commit fixes `nerdctl logs -f` to wait for the logger to finish writing when the container has already stopped. This commit should fix the flakiness of TestLogsFollowNoExtraneousLineFeed. After applying this fix, running the test 1000 times showed no flakiness. ```bash $ sudo go test -count=1000 -run '^TestLogsFollowNoExtraneousLineFeed$' test target: "nerdctl" PASS ok github.com/containerd/nerdctl/v2/cmd/nerdctl/container 419.653s ``` Signed-off-by: Hayato Kiwata --- pkg/cmd/container/logs.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/cmd/container/logs.go b/pkg/cmd/container/logs.go index c2ede0f0b35..cd9f62cfdd9 100644 --- a/pkg/cmd/container/logs.go +++ b/pkg/cmd/container/logs.go @@ -105,6 +105,14 @@ func Logs(ctx context.Context, client *containerd.Client, container string, opti } } + // When follow was requested but the task has already stopped, + // wait for the logger to finish writing before reading the log file. + if !follow && options.Follow { + if err := logging.WaitForLogger(dataStore, l[labels.Namespace], found.Container.ID()); err != nil { + log.G(ctx).WithError(err).Warn("failed to wait for logger shutdown") + } + } + var detailPrefix string if options.Details { if logConfigJSON, ok := l["nerdctl/log-config"]; ok { From 4f71951a73e7d5bd0a9748e9bfcb8ab47ae720b7 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 8 Apr 2026 23:19:57 +0900 Subject: [PATCH 504/868] fix: clean up unused iptables chains not being deleted on container removal When publishing a container's port(s) to the host and removeing the container, there are some iptables chains that are not deleted, as shown below: ```bash $ sudo nerdctl run -d --name nginx -p 8080:80 nginx 81cc6b08527975ef8bf151b1460ead6a3d767310a7513d306a4bbe19f61fe6a8 $ ID=$(echo -n "bridgedefault-$(sudo nerdctl ps -q --no-trunc --filter=name=nginx)" | sha512sum | awk '{print substr($1, 1, 24)}') $ sudo iptables -t nat -S | grep $ID -N CNI-5e9207ffbe238a4b386cd5bd -A POSTROUTING -s 10.4.0.156/32 -m comment --comment "name: \"bridge\" id: \"default-81cc6b08527975ef8bf151b1460ead6a3d767310a7513d306a4bbe19f61fe6a8\"" -j CNI-5e9207ffbe238a4b386cd5bd -A CNI-5e9207ffbe238a4b386cd5bd -d 10.4.0.0/24 -m comment --comment "name: \"bridge\" id: \"default-81cc6b08527975ef8bf151b1460ead6a3d767310a7513d306a4bbe19f61fe6a8\"" -j ACCEPT -A CNI-5e9207ffbe238a4b386cd5bd ! -d 224.0.0.0/4 -m comment --comment "name: \"bridge\" id: \"default-81cc6b08527975ef8bf151b1460ead6a3d767310a7513d306a4bbe19f61fe6a8\"" -j MASQUERADE $ sudo nerdctl rm -f nginx nginx $ sudo iptables -t nat -S | grep $ID -N CNI-5e9207ffbe238a4b386cd5bd $ sudo iptables -L -nv -t nat | grep $ID -3 Chain CNI-5cd4851e431cb9d7ef1a143b (0 references) pkts bytes target prot opt in out source destination Chain CNI-5e9207ffbe238a4b386cd5bd (0 references) pkts bytes target prot opt in out source destination Chain CNI-5fa88ae608b5a4cfbe76c33d (0 references) ``` Unused iptables chains should be deleted. Therefore, this PR makes a change so that the relevant iptables chains are deleted when a container is removed. Signed-off-by: Hayato Kiwata --- .../container/container_remove_linux_test.go | 11 +++- go.mod | 1 + go.sum | 4 ++ pkg/ocihook/ocihook.go | 39 +------------- pkg/ocihook/ocihook_linux.go | 54 +++++++++++++++++++ pkg/ocihook/ocihook_nolinux.go | 5 ++ 6 files changed, 74 insertions(+), 40 deletions(-) diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go index cda9aa1b8b9..57ea219fb44 100644 --- a/cmd/nerdctl/container/container_remove_linux_test.go +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -23,6 +23,8 @@ import ( "testing" "time" + cniutils "github.com/containernetworking/plugins/pkg/utils" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -111,10 +113,15 @@ func TestContainerRmIptables(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { // Get the container ID from the label containerID := data.Labels().Get("containerID") + id := fmt.Sprintf("%s-%s", testutil.Namespace, containerID) + chain := cniutils.FormatChainName("bridge", id) return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - // Verify that the iptables output does not contain the container ID - Output: expect.DoesNotContain(containerID), + // Verify that the iptables output does not contain the container ID and a chain name generated from the cni name, namespace, and container ID + Output: expect.All( + expect.DoesNotContain(containerID), + expect.DoesNotContain(chain), + ), } }, }, diff --git a/go.mod b/go.mod index f656e9d7c94..d7208299a56 100644 --- a/go.mod +++ b/go.mod @@ -152,6 +152,7 @@ require ( github.com/moby/moby/client v0.1.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 ) replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron diff --git a/go.sum b/go.sum index be8cbd3516a..17f46d99a36 100644 --- a/go.sum +++ b/go.sum @@ -187,6 +187,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= @@ -516,6 +518,8 @@ lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= +sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 0522949cfea..e8b4579da80 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -722,7 +722,7 @@ func onPostStop(opts *handlerOpts) error { // opts.cni.Remove has trouble removing network configurations when netns is empty. // Therefore, we force the deletion of iptables rules here to prevent netns exhaustion. // This is a workaround until https://github.com/containernetworking/plugins/pull/1078 is merged. - if err := cleanupIptablesRules(opts.fullID); err != nil { + if err := cleanupIptablesRules(opts.fullID, opts.cniNames); err != nil { log.L.WithError(err).Warnf("failed to clean up iptables rules for container %s", opts.fullID) // Don't return error here, continue with the rest of the cleanup } @@ -755,43 +755,6 @@ func onPostStop(opts *handlerOpts) error { return nil } -// cleanupIptablesRules cleans up iptables rules related to the container -func cleanupIptablesRules(containerID string) error { - // Check if iptables command exists - if _, err := exec.LookPath("iptables"); err != nil { - return fmt.Errorf("iptables command not found: %w", err) - } - - // Tables to check for rules - tables := []string{"nat", "filter", "mangle"} - - for _, table := range tables { - // Get all iptables rules for this table - cmd := exec.Command("iptables", "-t", table, "-S") - output, err := cmd.CombinedOutput() - if err != nil { - log.L.WithError(err).Warnf("failed to list iptables rules for table %s", table) - continue - } - - // Find and delete rules related to the container - rules := strings.Split(string(output), "\n") - for _, rule := range rules { - if strings.Contains(rule, containerID) { - // Execute delete command - deleteCmd := exec.Command("sh", "-c", "--", fmt.Sprintf(`iptables -t %s -D %s`, table, rule[3:])) - if deleteOutput, err := deleteCmd.CombinedOutput(); err != nil { - log.L.WithError(err).Warnf("failed to delete iptables rule: %s, output: %s", rule, string(deleteOutput)) - } else { - log.L.Debugf("deleted iptables rule: %s", rule) - } - } - } - } - - return nil -} - // writePidFile writes the pid atomically to a file. // From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/commands.go#L265-L282 func writePidFile(path string, pid int) error { diff --git a/pkg/ocihook/ocihook_linux.go b/pkg/ocihook/ocihook_linux.go index da961d75435..bb3bd29cb3c 100644 --- a/pkg/ocihook/ocihook_linux.go +++ b/pkg/ocihook/ocihook_linux.go @@ -17,6 +17,12 @@ package ocihook import ( + "fmt" + "os/exec" + "strings" + + cniutils "github.com/containernetworking/plugins/pkg/utils" + "github.com/containerd/containerd/v2/contrib/apparmor" "github.com/containerd/log" @@ -37,3 +43,51 @@ func loadAppArmor() { // but the profile was not actually loaded, runc will fail. } } + +// cleanupIptablesRules cleans up iptables rules related to the container +func cleanupIptablesRules(containerID string, cniNames []string) error { + // Check if iptables command exists + if _, err := exec.LookPath("iptables"); err != nil { + return fmt.Errorf("iptables command not found: %w", err) + } + + // Tables to check for rules + tables := []string{"nat", "filter", "mangle"} + + for _, table := range tables { + // Get all iptables rules for this table + cmd := exec.Command("iptables", "-t", table, "-S") + output, err := cmd.CombinedOutput() + if err != nil { + log.L.WithError(err).Warnf("failed to list iptables rules for table %s", table) + continue + } + + // Find and delete rules related to the container + rules := strings.Split(string(output), "\n") + for _, rule := range rules { + if strings.Contains(rule, containerID) { + // Execute delete command + deleteCmd := exec.Command("sh", "-c", "--", fmt.Sprintf(`iptables -t %s -D %s`, table, rule[3:])) + if deleteOutput, err := deleteCmd.CombinedOutput(); err != nil { + log.L.WithError(err).Warnf("failed to delete iptables rule: %s, output: %s", rule, string(deleteOutput)) + } else { + log.L.Debugf("deleted iptables rule: %s", rule) + } + } + } + } + + // Delete CNI chains related to the container + for _, cniName := range cniNames { + chain := cniutils.FormatChainName(cniName, containerID) + deleteCmd := exec.Command("iptables", "-t", "nat", "-X", chain) + if deleteOutput, err := deleteCmd.CombinedOutput(); err != nil { + log.L.WithError(err).Warnf("failed to delete iptables chain: %s, output: %s", chain, string(deleteOutput)) + } else { + log.L.Debugf("deleted iptables chain: %s", chain) + } + } + + return nil +} diff --git a/pkg/ocihook/ocihook_nolinux.go b/pkg/ocihook/ocihook_nolinux.go index 85f465f392f..0d106383951 100644 --- a/pkg/ocihook/ocihook_nolinux.go +++ b/pkg/ocihook/ocihook_nolinux.go @@ -21,3 +21,8 @@ package ocihook func loadAppArmor() { //noop } + +func cleanupIptablesRules(containerID string, cniNames []string) error { + //noop + return nil +} From fc771580a7c9ba5e9c852e65caea826d3090171f Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Wed, 8 Apr 2026 20:21:33 -0600 Subject: [PATCH 505/868] rootlessutil: remove dead -r/ from nsenter args The -r/ flag was placed at args[0], which becomes argv[0] (the program name) when passed to syscall.Exec. nsenter never parsed it as a flag, so it has been a no-op since it was added. If -r/ were moved to a proper argv position, it would break rootless container creation. nsenter opens the root fd before setns, so chroot anchors path resolution to the host root. In rootless mode, the host /var/lib/containerd is owned by real root (unmapped in the user namespace), causing overlay lowerdir resolution to fail with EACCES during WithAdditionalGIDs. Remove -r/ entirely rather than fixing its position. Signed-off-by: Aaron --- pkg/rootlessutil/parent_linux.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/rootlessutil/parent_linux.go b/pkg/rootlessutil/parent_linux.go index 7ae9b36c66b..93c31159834 100644 --- a/pkg/rootlessutil/parent_linux.go +++ b/pkg/rootlessutil/parent_linux.go @@ -91,10 +91,13 @@ func ParentMain(hostGatewayIP string) error { if err != nil { return err } - // args are compatible with both util-linux nsenter and busybox nsenter - args := []string{ - "-r/", // root dir (busybox nsenter wants this to be explicitly specified), - } + // -r/ (root dir) is intentionally omitted. nsenter would open the host + // root fd before setns, then chroot to it after entering the mount + // namespace, anchoring the process to host paths. In rootless mode, + // host dirs owned by real uid 0 (e.g. /var/lib/containerd) are + // inaccessible inside the user namespace and overlay mounts would + // fail with EACCES. + args := []string{arg0} // Only append wd if we do have a working dir // - https://github.com/rootless-containers/usernetes/pull/327 From e6af03c25159c5cd79fd5a33dc26ebf547d567a0 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 18 Apr 2026 03:51:53 +0900 Subject: [PATCH 506/868] update containerd (2.3.0-beta.2) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 6 ++++-- Dockerfile | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 55dbaf2ada3..bd52eb8f74d 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,9 +146,11 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.3.0-beta.1 + containerd-version: 2.3.0-beta.2 + # FIXME: containerd-sha is not verified (only affects tests) + # https://github.com/containerd/nerdctl/issues/4666 # Note: these as for amd64 - containerd-sha: sha256:34fb28c7d9e80cee64d995059043df33460346cf89bbe85bd5b0692e09dfcb4a + containerd-sha: sha256:feabdaf784298c5389972a93bf670b80abf7e674cd20a9d629fe133552046d0e containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index ffa235ac47b..c20bc71ba3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.0-beta.1@212b10b249ea8e541344f2d9f343d34ca3c29e4a +ARG CONTAINERD_VERSION=v2.3.0-beta.2@8a5337317f3216cd920283334f69b2f9003f75b2 ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From 632b6305012381cb215e48ce572fa11872f4a46c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 16 Apr 2026 13:59:07 +0900 Subject: [PATCH 507/868] go.mod: github.com/moby/moby/v2 v2.0.0-beta.9 The former github.com/docker/docker package is no longer updated. Signed-off-by: Akihiro Suda --- .github/dependabot.yml | 3 +- go.mod | 10 +- go.sum | 21 ++-- pkg/cmd/volume/create.go | 2 +- pkg/composer/copy.go | 26 ++++- pkg/containerutil/containerutil.go | 6 +- pkg/infoutil/infoutil.go | 2 +- pkg/infoutil/infoutil_darwin.go | 2 +- pkg/infoutil/infoutil_freebsd.go | 2 +- pkg/infoutil/infoutil_linux.go | 4 +- pkg/infoutil/infoutil_windows.go | 4 +- pkg/logging/journald_logger.go | 6 +- pkg/logging/jsonfile/jsonfile.go | 8 +- pkg/timestamp/timestamp.go | 154 +++++++++++++++++++++++++++++ pkg/timestamp/timestamp_test.go | 118 ++++++++++++++++++++++ 15 files changed, 331 insertions(+), 37 deletions(-) create mode 100644 pkg/timestamp/timestamp.go create mode 100644 pkg/timestamp/timestamp_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0c21fa19f87..d5b9656295d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,8 +23,9 @@ updates: - "github.com/moby/sys/*" docker: patterns: - - "github.com/docker/docker" - "github.com/docker/cli" + - "github.com/moby/moby/client" + - "github.com/moby/moby/v2" containerd: patterns: - "github.com/containerd/containerd" diff --git a/go.mod b/go.mod index 4b1aadc3f09..0b100882223 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,6 @@ require ( github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.4.0+incompatible //gomodjail:unconfined - github.com/docker/docker v28.5.2+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 @@ -44,6 +43,8 @@ require ( github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.5 github.com/mattn/go-isatty v0.0.21 //gomodjail:unconfined + github.com/moby/moby/client v0.4.0 + github.com/moby/moby/v2 v2.0.0-beta.9 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined @@ -75,7 +76,7 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/cilium/ebpf v0.16.0 // indirect + github.com/cilium/ebpf v0.17.3 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/plugin v1.0.0 // indirect @@ -88,7 +89,7 @@ require ( github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // 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 @@ -148,8 +149,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/moby/moby/api v1.52.0 // indirect - github.com/moby/moby/client v0.1.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect github.com/moby/sys/capability v0.4.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect ) diff --git a/go.sum b/go.sum index 7093d6b29d1..c5d4a24ab48 100644 --- a/go.sum +++ b/go.sum @@ -14,13 +14,14 @@ github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vL github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= -github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= +github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= +github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= @@ -94,8 +95,6 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -131,8 +130,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/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= @@ -207,10 +206,12 @@ 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.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= -github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8= -github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/moby/v2 v2.0.0-beta.9 h1:UnFpzAeGOS4Wh8zgeFzwUx62P+VzcHGNoqJB4kx1VKg= +github.com/moby/moby/v2 v2.0.0-beta.9/go.mod h1:3AICfhxV7CcWBOG/BB/hdDOrcSd04cYDSgn1hNYEBUI= 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= diff --git a/pkg/cmd/volume/create.go b/pkg/cmd/volume/create.go index 5aac0ce0486..dec4c80b9e2 100644 --- a/pkg/cmd/volume/create.go +++ b/pkg/cmd/volume/create.go @@ -19,7 +19,7 @@ package volume import ( "fmt" - "github.com/docker/docker/pkg/stringid" + "github.com/moby/moby/client/pkg/stringid" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" diff --git a/pkg/composer/copy.go b/pkg/composer/copy.go index a75d56bac33..615c8a8a884 100644 --- a/pkg/composer/copy.go +++ b/pkg/composer/copy.go @@ -14,16 +14,23 @@ limitations under the License. */ +/* + Portions from https://github.com/moby/moby/blob/v28.5.2/pkg/system/filesys.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/v28.5.2/NOTICE +*/ + package composer import ( "context" "errors" "fmt" + "os" + "path/filepath" "strings" - "github.com/docker/docker/pkg/system" - containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/log" @@ -144,9 +151,22 @@ func (c *Composer) listContainersTargetedForCopy(ctx context.Context, index int, return containers, err } +// isAbs is a platform-agnostic wrapper for filepath.IsAbs. +// From https://github.com/moby/moby/blob/v28.5.2/pkg/system/filesys.go#L9-L19 +// +// On Windows, golang filepath.IsAbs does not consider a path \windows\system32 +// as absolute as it doesn't start with a drive-letter/colon combination. However, +// in docker we need to verify things such as WORKDIR /windows/system32 in +// a Dockerfile (which gets translated to \windows\system32 when being processed +// by the daemon). This SHOULD be treated as absolute from a docker processing +// perspective. +func isAbs(path string) bool { + return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator)) +} + // https://github.com/docker/compose/blob/v2.21.0/pkg/compose/cp.go#L307 func splitCpArg(arg string) (container, path string) { - if system.IsAbs(arg) { + if isAbs(arg) { // Explicit local absolute path, e.g., `C:\foo` or `/foo`. return "", arg } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 875f202fbda..94a8e043069 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -30,7 +30,6 @@ import ( "time" dockercliopts "github.com/docker/cli/opts" - dockeropts "github.com/docker/docker/opts" "github.com/moby/sys/signal" "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/term" @@ -644,6 +643,7 @@ func DecodeContainerRmOptLabel(rmOptLabel string) (bool, error) { // // Returns a map of host-to-IPs or errors if any mapping strings are not correctly formatted. func ParseExtraHosts(extraHosts []string, hostGatewayIP, separator string) ([]string, error) { + const hostGatewayName = "host-gateway" hosts := make([]string, 0, len(extraHosts)) for _, hostToIP := range strutil.DedupeStrSlice(extraHosts) { if _, err := dockercliopts.ValidateExtraHost(hostToIP); err != nil { @@ -659,9 +659,9 @@ func ParseExtraHosts(extraHosts []string, hostGatewayIP, separator string) ([]st // If the IP address is a string called "host-gateway", replace this value with the IP address stored // in the daemon level HostGatewayIP config variable. - if ip == dockeropts.HostGatewayName && hostGatewayIP == "" { + if ip == hostGatewayName && hostGatewayIP == "" { return nil, errors.New("unable to derive the IP value for host-gateway") - } else if ip == dockeropts.HostGatewayName { + } else if ip == hostGatewayName { ip = hostGatewayIP } diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index 75cdc7b59e6..5dd6e84780d 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/docker/docker/pkg/sysinfo" + "github.com/moby/moby/v2/pkg/sysinfo" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/introspection" diff --git a/pkg/infoutil/infoutil_darwin.go b/pkg/infoutil/infoutil_darwin.go index 3b87f89df2f..13c2ca2960c 100644 --- a/pkg/infoutil/infoutil_darwin.go +++ b/pkg/infoutil/infoutil_darwin.go @@ -17,7 +17,7 @@ package infoutil import ( - "github.com/docker/docker/pkg/sysinfo" + "github.com/moby/moby/v2/pkg/sysinfo" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" ) diff --git a/pkg/infoutil/infoutil_freebsd.go b/pkg/infoutil/infoutil_freebsd.go index 40cd76f8bf3..4ef252e5981 100644 --- a/pkg/infoutil/infoutil_freebsd.go +++ b/pkg/infoutil/infoutil_freebsd.go @@ -17,7 +17,7 @@ package infoutil import ( - "github.com/docker/docker/pkg/sysinfo" + "github.com/moby/moby/v2/pkg/sysinfo" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" ) diff --git a/pkg/infoutil/infoutil_linux.go b/pkg/infoutil/infoutil_linux.go index 61ea9ce4bca..e8c68a54b47 100644 --- a/pkg/infoutil/infoutil_linux.go +++ b/pkg/infoutil/infoutil_linux.go @@ -21,8 +21,8 @@ import ( "runtime" "strings" - "github.com/docker/docker/pkg/meminfo" - "github.com/docker/docker/pkg/sysinfo" + "github.com/moby/moby/v2/pkg/meminfo" + "github.com/moby/moby/v2/pkg/sysinfo" "github.com/containerd/cgroups/v3" diff --git a/pkg/infoutil/infoutil_windows.go b/pkg/infoutil/infoutil_windows.go index 7758b905997..4256ead5431 100644 --- a/pkg/infoutil/infoutil_windows.go +++ b/pkg/infoutil/infoutil_windows.go @@ -21,8 +21,8 @@ import ( "runtime" "strings" - "github.com/docker/docker/pkg/meminfo" - "github.com/docker/docker/pkg/sysinfo" + "github.com/moby/moby/v2/pkg/meminfo" + "github.com/moby/moby/v2/pkg/sysinfo" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" diff --git a/pkg/logging/journald_logger.go b/pkg/logging/journald_logger.go index ce2f3afe59e..3677aea4fe6 100644 --- a/pkg/logging/journald_logger.go +++ b/pkg/logging/journald_logger.go @@ -31,7 +31,6 @@ import ( "github.com/coreos/go-systemd/v22/journal" "github.com/docker/cli/templates" - timetypes "github.com/docker/docker/api/types/time" "github.com/containerd/containerd/v2/core/runtime/v2/logging" "github.com/containerd/log" @@ -39,6 +38,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/strutil" + "github.com/containerd/nerdctl/v2/pkg/timestamp" ) var JournalDriverLogOpts = []string{ @@ -206,7 +206,7 @@ func viewLogsJournald(lvopts LogViewOptions, stdout, stderr io.Writer, stopChann } if lvopts.Since != "" { // using GetTimestamp from moby to keep time format consistency - ts, err := timetypes.GetTimestamp(lvopts.Since, time.Now()) + ts, err := timestamp.GetTimestamp(lvopts.Since, time.Now()) if err != nil { return fmt.Errorf("invalid value for \"since\": %w", err) } @@ -221,7 +221,7 @@ func viewLogsJournald(lvopts LogViewOptions, stdout, stderr io.Writer, stopChann } if lvopts.Until != "" { // using GetTimestamp from moby to keep time format consistency - ts, err := timetypes.GetTimestamp(lvopts.Until, time.Now()) + ts, err := timestamp.GetTimestamp(lvopts.Until, time.Now()) if err != nil { return fmt.Errorf("invalid value for \"until\": %w", err) } diff --git a/pkg/logging/jsonfile/jsonfile.go b/pkg/logging/jsonfile/jsonfile.go index 6e6f7984eda..22afc6f3442 100644 --- a/pkg/logging/jsonfile/jsonfile.go +++ b/pkg/logging/jsonfile/jsonfile.go @@ -26,9 +26,9 @@ import ( "sync" "time" - timetypes "github.com/docker/docker/api/types/time" - "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/timestamp" ) // Entry is compatible with Docker "json-file" logs @@ -75,7 +75,7 @@ func writeEntry(e *Entry, stdout, stderr io.Writer, refTime time.Time, timestamp output := []byte{} if since != "" { - ts, err := timetypes.GetTimestamp(since, refTime) + ts, err := timestamp.GetTimestamp(since, refTime) if err != nil { return fmt.Errorf("invalid value for \"since\": %w", err) } @@ -90,7 +90,7 @@ func writeEntry(e *Entry, stdout, stderr io.Writer, refTime time.Time, timestamp } if until != "" { - ts, err := timetypes.GetTimestamp(until, refTime) + ts, err := timestamp.GetTimestamp(until, refTime) if err != nil { return fmt.Errorf("invalid value for \"until\": %w", err) } diff --git a/pkg/timestamp/timestamp.go b/pkg/timestamp/timestamp.go new file mode 100644 index 00000000000..f349cb03092 --- /dev/null +++ b/pkg/timestamp/timestamp.go @@ -0,0 +1,154 @@ +/* + 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. +*/ + +/* + Originally from https://github.com/moby/moby/blob/v2.0.0-beta.9/client/internal/timestamp/timestamp.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/v2.0.0-beta.9/NOTICE +*/ + +package timestamp + +import ( + "fmt" + "math" + "strconv" + "strings" + "time" +) + +// These are additional predefined layouts for use in Time.Format and Time.Parse +// with --since and --until parameters for `docker logs` and `docker events` +const ( + rFC3339Local = "2006-01-02T15:04:05" // RFC3339 with local timezone + rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone + dateWithZone = "2006-01-02Z07:00" // RFC3339 with time at 00:00:00 + dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00 +) + +// GetTimestamp tries to parse given string as golang duration, +// then RFC3339 time and finally as a Unix timestamp. If +// any of these were successful, it returns a Unix timestamp +// as string otherwise returns the given value back. +// In case of duration input, the returned timestamp is computed +// as the given reference time minus the amount of the duration. +func GetTimestamp(value string, reference time.Time) (string, error) { + if d, err := time.ParseDuration(value); value != "0" && err == nil { + return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil + } + + var format string + // if the string has a Z or a + or three dashes use parse otherwise use parseinlocation + parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3 + + if strings.Contains(value, ".") { + if parseInLocation { + format = rFC3339NanoLocal + } else { + format = time.RFC3339Nano + } + } else if strings.Contains(value, "T") { + // we want the number of colons in the T portion of the timestamp + tcolons := strings.Count(value, ":") + // if parseInLocation is off and we have a +/- zone offset (not Z) then + // there will be an extra colon in the input for the tz offset subtract that + // colon from the tcolons count + if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 { + tcolons-- + } + if parseInLocation { + switch tcolons { + case 0: + format = "2006-01-02T15" + case 1: + format = "2006-01-02T15:04" + default: + format = rFC3339Local + } + } else { + switch tcolons { + case 0: + format = "2006-01-02T15Z07:00" + case 1: + format = "2006-01-02T15:04Z07:00" + default: + format = time.RFC3339 + } + } + } else if parseInLocation { + format = dateLocal + } else { + format = dateWithZone + } + + var t time.Time + var err error + + if parseInLocation { + t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone())) + } else { + t, err = time.Parse(format, value) + } + + if err != nil { + // if there is a `-` then it's an RFC3339 like timestamp + if strings.Contains(value, "-") { + return "", err // was probably an RFC3339 like timestamp but the parser failed with an error + } + if _, _, err := parseTimestamp(value); err != nil { + return "", fmt.Errorf("failed to parse value as time or duration: %q", value) + } + return value, nil // unix timestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server) + } + + return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil +} + +// ParseTimestamps returns seconds and nanoseconds from a timestamp that has +// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())). +// If the incoming nanosecond portion is longer than 9 digits it is truncated. +// The expectation is that the seconds and nanoseconds will be used to create a +// time variable. For example: +// +// seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0) +// since := time.Unix(seconds, nanoseconds) +// +// returns seconds as defaultSeconds if value == "" +func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) { + if value == "" { + return defaultSeconds, 0, nil + } + return parseTimestamp(value) +} + +func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) { + s, n, ok := strings.Cut(value, ".") + sec, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return sec, 0, err + } + if !ok { + return sec, 0, nil + } + nsec, err := strconv.ParseInt(n, 10, 64) + if err != nil { + return sec, nsec, err + } + // should already be in nanoseconds but just in case convert n to nanoseconds + nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n)))) + return sec, nsec, nil +} diff --git a/pkg/timestamp/timestamp_test.go b/pkg/timestamp/timestamp_test.go new file mode 100644 index 00000000000..176ba3c08af --- /dev/null +++ b/pkg/timestamp/timestamp_test.go @@ -0,0 +1,118 @@ +/* + 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. +*/ + +/* + Originally from https://github.com/moby/moby/blob/v2.0.0-beta.9/client/internal/timestamp/timestamp.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/v2.0.0-beta.9/NOTICE +*/ + +package timestamp + +import ( + "fmt" + "testing" + "time" +) + +func TestGetTimestamp(t *testing.T) { + now := time.Now().In(time.UTC) + cases := []struct { + in, expected string + expectedErr bool + }{ + // Partial RFC3339 strings get parsed with second precision + {"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false}, + {"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false}, + {"2006-01-02T15:04:05.999999999", "1136214245.999999999", false}, + {"2006-01-02T15:04:05Z", "1136214245.000000000", false}, + {"2006-01-02T15:04:05", "1136214245.000000000", false}, + {"2006-01-02T15:04:0Z", "", true}, + {"2006-01-02T15:04:0", "", true}, + {"2006-01-02T15:04Z", "1136214240.000000000", false}, + {"2006-01-02T15:04+00:00", "1136214240.000000000", false}, + {"2006-01-02T15:04-00:00", "1136214240.000000000", false}, + {"2006-01-02T15:04", "1136214240.000000000", false}, + {"2006-01-02T15:0Z", "", true}, + {"2006-01-02T15:0", "", true}, + {"2006-01-02T15Z", "1136214000.000000000", false}, + {"2006-01-02T15+00:00", "1136214000.000000000", false}, + {"2006-01-02T15-00:00", "1136214000.000000000", false}, + {"2006-01-02T15", "1136214000.000000000", false}, + {"2006-01-02T1Z", "1136163600.000000000", false}, + {"2006-01-02T1", "1136163600.000000000", false}, + {"2006-01-02TZ", "", true}, + {"2006-01-02T", "", true}, + {"2006-01-02+00:00", "1136160000.000000000", false}, + {"2006-01-02-00:00", "1136160000.000000000", false}, + {"2006-01-02-00:01", "1136160060.000000000", false}, + {"2006-01-02Z", "1136160000.000000000", false}, + {"2006-01-02", "1136160000.000000000", false}, + {"2015-05-13T20:39:09Z", "1431549549.000000000", false}, + + // unix timestamps returned as is + {"1136073600", "1136073600", false}, + {"1136073600.000000001", "1136073600.000000001", false}, + // Durations + {"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false}, + {"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false}, + {"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false}, + + {"invalid", "", true}, + {"", "", true}, + } + + for _, c := range cases { + o, err := GetTimestamp(c.in, now) + if o != c.expected || + (err == nil && c.expectedErr) || + (err != nil && !c.expectedErr) { + t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err) + t.Fail() + } + } +} + +func TestParseTimestamps(t *testing.T) { + cases := []struct { + in string + def, expectedS, expectedN int64 + expectedErr bool + }{ + // unix timestamps + {"1136073600", 0, 1136073600, 0, false}, + {"1136073600.000000001", 0, 1136073600, 1, false}, + {"1136073600.0000000010", 0, 1136073600, 1, false}, + {"1136073600.0000000001", 0, 1136073600, 0, false}, + {"1136073600.0000000009", 0, 1136073600, 0, false}, + {"1136073600.00000001", 0, 1136073600, 10, false}, + {"foo.bar", 0, 0, 0, true}, + {"1136073600.bar", 0, 1136073600, 0, true}, + {"", -1, -1, 0, false}, + } + + for _, c := range cases { + s, n, err := ParseTimestamps(c.in, c.def) + if s != c.expectedS || + n != c.expectedN || + (err == nil && c.expectedErr) || + (err != nil && !c.expectedErr) { + t.Errorf("wrong values for input `%s` with default `%d` expected:'%d'seconds and `%d`nanosecond got:'%d'seconds and `%d`nanoseconds with error: `%s`", c.in, c.def, c.expectedS, c.expectedN, s, n, err) + t.Fail() + } + } +} From 4f5543364e943e23c380991e6134c61ce8bf5709 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Sat, 6 Dec 2025 17:18:05 +0800 Subject: [PATCH 508/868] feature: support selinux Signed-off-by: ningmingxiao --- .../container_run_security_linux_test.go | 106 ++++++++++++++++++ cmd/nerdctl/helpers/flagutil.go | 5 + cmd/nerdctl/main.go | 1 + docs/command-reference.md | 2 + docs/config.md | 2 + go.mod | 2 + pkg/cmd/container/run_linux.go | 2 +- pkg/cmd/container/run_security_linux.go | 36 +++++- pkg/config/config.go | 2 + pkg/mountutil/mountutil_linux.go | 12 +- pkg/testutil/nerdtest/requirements.go | 14 +++ 11 files changed, 180 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index 6a4cc35bb4b..4313cad4e3f 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -26,9 +26,15 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/apparmorutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func getCapEff(base *testutil.Base, args ...string) uint64 { @@ -186,6 +192,106 @@ func TestRunApparmor(t *testing.T) { base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined") } +func TestRunSelinuxWithSecurityOpt(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.NoSelinux) + testContainer := testutil.Identifier(t) + + testCase.SubTests = []*test.Case{ + { + Description: "test run with selinux-enabled", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", testContainer) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) + pid := strings.TrimSpace(inspectOut) + fileName := fmt.Sprintf("/proc/%s/attr/current", pid) + data, err := os.ReadFile(fileName) + assert.NilError(t, err) + assert.Equal(t, strings.Contains(string(data), "container_t"), true) + }, + ), + } + }, + }, + } + testCase.Run(t) +} +func TestRunSelinux(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.NoSelinux) + testContainer := testutil.Identifier(t) + + testCase.SubTests = []*test.Case{ + { + Description: "test run with selinux-enabled", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", testContainer) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(stdout string, t tig.T) { + inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) + pid := strings.TrimSpace(inspectOut) + fileName := fmt.Sprintf("/proc/%s/attr/current", pid) + data, err := os.ReadFile(fileName) + assert.NilError(t, err) + assert.Equal(t, strings.Contains(string(data), "container_t"), true) + }, + ), + } + }, + }, + } + testCase.Run(t) +} + +func TestRunSelinuxWithVolumeLabel(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.NoSelinux) + testContainer := testutil.Identifier(t) + + testCase.SubTests = []*test.Case{ + { + Description: "test run with selinux-enabled", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("/%s:/%s:Z", testContainer, testContainer), "--name", testContainer, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", testContainer) + os.RemoveAll(fmt.Sprintf("/%s", testContainer)) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + func(stdout string, t tig.T) { + cmd := exec.Command("ls", "-Z", fmt.Sprintf("/%s", testContainer)) + lsStdout, err := cmd.CombinedOutput() + assert.NilError(t, err) + assert.Equal(t, strings.Contains(string(lsStdout), "container_t"), true) + }, + ), + } + }, + }, + } + testCase.Run(t) +} + // TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976 func TestRunSeccompCapSysPtrace(t *testing.T) { base := testutil.NewBase(t) diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 22fc1acb1bf..1ebed1f35d1 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -154,6 +154,10 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) return types.GlobalCommandOptions{}, err } + selinuxEnabled, err := cmd.Flags().GetBool("selinux-enabled") + if err != nil { + return types.GlobalCommandOptions{}, err + } // Point to dataRoot for filesystem-helpers implementing rollback / backups. err = fs.InitFS(dataRoot) if err != nil { @@ -180,6 +184,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) DNS: dns, DNSOpts: dnsOpts, DNSSearch: dnsSearch, + SelinuxEnabled: selinuxEnabled, }, nil } diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 1fb96f47cc3..17d679f84c8 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -191,6 +191,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, helpers.AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host") helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network") rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io") + rootCmd.PersistentFlags().Bool("selinux-enabled", cfg.SelinuxEnabled, "Enable selinux support") rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi") rootCmd.PersistentFlags().String("userns-remap", cfg.UsernsRemap, "Support idmapping for creating and running containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively") helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns", cfg.DNS, "Global DNS servers for containers") diff --git a/docs/command-reference.md b/docs/command-reference.md index 3d40d9398b1..a8a5474a8a8 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -255,6 +255,7 @@ Security flags: - :whale: `--security-opt seccomp=`: specify custom seccomp profile - :whale: `--security-opt apparmor=`: specify custom AppArmor profile + :whale: `--security-opt label=`: specify custom selinux label - :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities - :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container - :whale: `--security-opt writable-cgroups`: making the cgroups writeable @@ -1977,6 +1978,7 @@ Flags: - :nerd_face: `--host-gateway-ip`: IP address that the special 'host-gateway' string in --add-host resolves to. It has no effect without setting --add-host - Default: the IP address of the host - :nerd_face: `--userns-remap=:`: Support idmapping of containers. This options is only supported on rootful linux for container create and run if a user name and optionally group name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively. Note: `--userns-remap` is not supported for building containers. Nerdctl Build doesn't support userns-remap feature. (format: [:]) +- :nerd_face: `--selinux-enabled`: Enable selinux support The global flags can be also specified in `/etc/nerdctl/nerdctl.toml` (rootful) and `~/.config/nerdctl/nerdctl.toml` (rootless). See [`./config.md`](./config.md). diff --git a/docs/config.md b/docs/config.md index 4ed70965948..605a776a68e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -30,6 +30,7 @@ userns_remap = "" dns = ["8.8.8.8", "1.1.1.1"] dns_opts = ["ndots:1", "timeout:2"] dns_search = ["example.com", "example.org"] +selinux_enabled= true ``` ## Properties @@ -56,6 +57,7 @@ dns_search = ["example.com", "example.org"] | `dns` | | | Set global DNS servers for containers | Since 2.1.3 | | `dns_opts` | | | Set global DNS options for containers | Since 2.1.3 | | `dns_search` | | | Set global DNS search domains for containers | Since 2.1.3 | +| `selinux_enabled` | | |Enable selinux support for containers | Since 2.3.0 | The properties are parsed in the following precedence: 1. CLI flag diff --git a/go.mod b/go.mod index a3848f87f56..3314b569a99 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ 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.13.1 github.com/pelletier/go-toml/v2 v2.3.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined @@ -148,6 +149,7 @@ require ( ) require ( + cyphar.com/go-pathrs v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/moby/moby/api v1.54.1 // indirect github.com/moby/sys/capability v0.4.0 // indirect diff --git a/pkg/cmd/container/run_linux.go b/pkg/cmd/container/run_linux.go index f49aee4197e..dfa876a05bb 100644 --- a/pkg/cmd/container/run_linux.go +++ b/pkg/cmd/container/run_linux.go @@ -71,7 +71,7 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts } opts = append(opts, capOpts...) securityOptsMaps := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(options.SecurityOpt)) - secOpts, err := generateSecurityOpts(options.Privileged, securityOptsMaps) + secOpts, err := generateSecurityOpts(options.Privileged, options.GOptions.SelinuxEnabled, securityOptsMaps) if err != nil { return nil, err } diff --git a/pkg/cmd/container/run_security_linux.go b/pkg/cmd/container/run_security_linux.go index dbd76234c1b..46c667e720b 100644 --- a/pkg/cmd/container/run_security_linux.go +++ b/pkg/cmd/container/run_security_linux.go @@ -17,14 +17,19 @@ package container import ( + "context" "errors" "fmt" "strconv" "strings" "sync" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux/label" + "github.com/containerd/containerd/v2/contrib/apparmor" "github.com/containerd/containerd/v2/contrib/seccomp" + "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/cap" "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/log" @@ -51,10 +56,10 @@ const ( systemPathsUnconfined = "unconfined" ) -func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) { +func generateSecurityOpts(privileged bool, selinuxEnabled bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) { for k := range securityOptsMap { switch k { - case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups": + case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups", "label": default: log.L.Warnf("unknown security-opt: %q", k) } @@ -95,6 +100,18 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([ opts = append(opts, apparmor.WithProfile(defaults.AppArmorProfileName)) } } + // TODO: should set unique MCS categorie. + if !privileged && selinuxEnabled { + var labelOpts []string + if selinuxLabel, ok := securityOptsMap["label"]; ok { + labelOpts = append(labelOpts, selinuxLabel) + } + processLabel, mountLabel, err := label.InitLabels(labelOpts) + if err != nil { + return nil, err + } + opts = append(opts, WithSelinuxLabel(processLabel, mountLabel)) + } nnp, err := maputil.MapBoolValueAsOpt(securityOptsMap, "no-new-privileges") if err != nil { @@ -141,6 +158,21 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([ return opts, nil } +// WithSelinuxLabels sets the mount and process labels +func WithSelinuxLabel(process, mount string) oci.SpecOpts { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + if s.Linux == nil { + s.Linux = &specs.Linux{} + } + if s.Process == nil { + s.Process = &specs.Process{} + } + s.Linux.MountLabel = mount + s.Process.SelinuxLabel = process + return nil + } +} + func canonicalizeCapName(s string) string { if s == "" { return "" diff --git a/pkg/config/config.go b/pkg/config/config.go index 5ecf41b9256..e967c91393a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -47,6 +47,7 @@ type Config struct { DNSOpts []string `toml:"dns_opts,omitempty"` DNSSearch []string `toml:"dns_search,omitempty"` DisableHCSystemd bool `toml:"disable_hc_systemd"` + SelinuxEnabled bool `toml:"selinux_enabled"` } // New creates a default Config object statically, @@ -63,6 +64,7 @@ func New() *Config { DataRoot: ncdefaults.DataRoot(), CgroupManager: ncdefaults.CgroupManager(), InsecureRegistry: false, + SelinuxEnabled: false, HostsDir: ncdefaults.HostsDirs(), Experimental: true, HostGatewayIP: ncdefaults.HostGatewayIP(), diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index a6a79d8e963..d1fde8b1c2a 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -28,6 +28,7 @@ import ( "github.com/docker/go-units" mobymount "github.com/moby/sys/mount" "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux/label" "golang.org/x/sys/unix" "github.com/containerd/containerd/v2/core/containers" @@ -112,6 +113,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun propagationRawOpts []string bindOpts []string ) + var specOpts []oci.SpecOpts for _, opt := range strings.Split(optsRaw, ",") { switch opt { case "rw", "ro", "rro": @@ -121,6 +123,15 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun case "bind", "rbind": // bind means not recursively bind-mounted, rbind is the opposite bindOpts = append(bindOpts, opt) + case "Z", "z": + specOpts = append(specOpts, func(ctx context.Context, cli oci.Client, c *containers.Container, s *oci.Spec) error { + if s.Linux != nil && s.Linux.MountLabel != "" { + if err := label.Relabel(src, s.Linux.MountLabel, opt == "z"); err != nil { + return err + } + } + return nil + }) case "": // NOP default: @@ -129,7 +140,6 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun } var opts []string - var specOpts []oci.SpecOpts if len(bindOpts) > 0 && vType != Bind { return nil, nil, fmt.Errorf("volume bind/rbind option is only supported for bind mount: %+v", bindOpts) diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 8e2c0a0e258..5b8463903ea 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -25,6 +25,7 @@ import ( "strings" "github.com/Masterminds/semver/v3" + "github.com/opencontainers/selinux/go-selinux" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/defaults" @@ -161,6 +162,19 @@ var Rootless = &test.Requirement{ }, } +// NoSelinux marks a test as suitable only for the noselinux enable environment +var NoSelinux = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = !selinux.GetEnabled() + if ret { + mess = "selinux is disabled" + } else { + mess = "selinux is enabled" + } + return ret, mess + }, +} + // RootlessWithDetachNetNS marks a test as suitable only for rootless environment with detached netns support. var RootlessWithDetachNetNS = &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { From e623dd532058e690334d058056250eb24e487078 Mon Sep 17 00:00:00 2001 From: Ansuman Sahoo Date: Wed, 22 Apr 2026 19:28:09 +0530 Subject: [PATCH 509/868] fix: free up reserved ports in rootful mode Signed-off-by: Ansuman Sahoo --- pkg/cmd/container/stop.go | 4 ++++ pkg/ocihook/ocihook.go | 30 +++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/container/stop.go b/pkg/cmd/container/stop.go index 755686e4bd8..b0cd62225f2 100644 --- a/pkg/cmd/container/stop.go +++ b/pkg/cmd/container/stop.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/ocihook" ) // Stop stops a list of containers specified by `reqs`. @@ -50,6 +51,9 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ } return err } + if err := ocihook.CleanupPortReserverProcess(opt.GOptions.Namespace, found.Container.ID()); err != nil { + return fmt.Errorf("unable to cleanup port reserver process for container: %s: %w", found.Req, err) + } _, err := fmt.Fprintln(opt.Stdout, found.Req) return err }, diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index e8b4579da80..581a8d8847f 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -456,8 +456,24 @@ func reserveSocket(protocol, hostAddr string) (*os.File, error) { } // portReserverPidFilePath returns /run/nerdctl///port-reserver.pid -func portReserverPidFilePath(opts *handlerOpts) string { - return filepath.Join("/run/nerdctl/", opts.state.Annotations[labels.Namespace], opts.state.ID, "port-reserver.pid") +func portReserverPidFilePath(namespace, id string) string { + return filepath.Join("/run/nerdctl/", namespace, id, "port-reserver.pid") +} + +func CleanupPortReserverProcess(namespace, id string) error { + // In rootless mode, port-reserver is handled by Rootlesskit, so no cleanup is needed. + if rootlessutil.IsRootlessChild() { + return nil + } + + pidFile := portReserverPidFilePath(namespace, id) + if err := killProcessByPidFile(pidFile); err != nil { + return err + } + if err := os.RemoveAll(filepath.Dir(pidFile)); err != nil { + log.L.WithError(err).Errorf("failed to remove the port-reserver directory %s", filepath.Dir(pidFile)) + } + return nil } func applyNetworkSettings(opts *handlerOpts) (err error) { @@ -503,10 +519,10 @@ func applyNetworkSettings(opts *handlerOpts) (err error) { if err != nil { log.L.Debugf("killing the port reserver process (pid=%d)", reserverCmdPid) _ = reserverCmd.Process.Kill() - _ = os.RemoveAll(filepath.Dir(portReserverPidFilePath(opts))) + _ = os.RemoveAll(filepath.Dir(portReserverPidFilePath(opts.state.Annotations[labels.Namespace], opts.state.ID))) } }() - if err := writePidFile(portReserverPidFilePath(opts), reserverCmdPid); err != nil { + if err := writePidFile(portReserverPidFilePath(opts.state.Annotations[labels.Namespace], opts.state.ID), reserverCmdPid); err != nil { return fmt.Errorf("cannot write the pid file of the port reserver process: %w", err) } } @@ -745,13 +761,9 @@ func onPostStop(opts *handlerOpts) error { return fmt.Errorf("failed to release container name %s: %w", name, err) } // Kill port-reserver process if any - portReserverPidFile := portReserverPidFilePath(opts) - if err = killProcessByPidFile(portReserverPidFile); err != nil { + if err = CleanupPortReserverProcess(ns, opts.state.ID); err != nil { log.L.WithError(err).Errorf("failed to kill the port-reserver process") } - if err := os.RemoveAll(filepath.Dir(portReserverPidFile)); err != nil { - log.L.WithError(err).Errorf("failed to remove the port-reserver directory %s", filepath.Dir(portReserverPidFile)) - } return nil } From 8ff789427cd87cd8c46a001d2ba71283920f0a93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:33:19 +0000 Subject: [PATCH 510/868] build(deps): bump the docker group across 1 directory with 3 updates Bumps the docker group with 3 updates in the / directory: [github.com/docker/cli](https://github.com/docker/cli), [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.4.0+incompatible to 29.4.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.4.0...v29.4.1) Updates `github.com/moby/moby/client` from 0.4.0 to 0.4.1 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.4.1/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.4.0...v0.4.1) Updates `github.com/moby/moby/v2` from 2.0.0-beta.9 to 2.0.0-beta.11 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.9...v2.0.0-beta.11) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.4.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/client dependency-version: 0.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 094dd3cd5bb..ad16548d8eb 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.4.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.4.1+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,8 +43,8 @@ require ( github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.5 github.com/mattn/go-isatty v0.0.21 //gomodjail:unconfined - github.com/moby/moby/client v0.4.0 - github.com/moby/moby/v2 v2.0.0-beta.9 + github.com/moby/moby/client v0.4.1 + github.com/moby/moby/v2 v2.0.0-beta.11 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined @@ -129,14 +129,14 @@ 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.67.0 // 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.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/mod v0.35.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect //gomodjail:unconfined google.golang.org/grpc v1.80.0 // indirect //gomodjail:unconfined @@ -149,7 +149,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/api v1.54.2 // 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 002c8d53a99..168e23545e4 100644 --- a/go.sum +++ b/go.sum @@ -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.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= -github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.4.1+incompatible h1:02RT8QqqwtGRn+6SYypv8IUEbD/ltY6sfKCJIoUcGzk= +github.com/docker/cli v29.4.1+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= @@ -208,12 +208,12 @@ 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.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= -github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= -github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= -github.com/moby/moby/v2 v2.0.0-beta.9 h1:UnFpzAeGOS4Wh8zgeFzwUx62P+VzcHGNoqJB4kx1VKg= -github.com/moby/moby/v2 v2.0.0-beta.9/go.mod h1:3AICfhxV7CcWBOG/BB/hdDOrcSd04cYDSgn1hNYEBUI= +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.11 h1:0s92vMOUn8H/MDXGeYyEqI/ulYlVpQ1Dh1zSgprgQ/Y= +github.com/moby/moby/v2 v2.0.0-beta.11/go.mod h1:CJq/k6N+wkom/MTVsGVZfsB4FxrHt4sVS0yJZphif6w= 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= @@ -334,16 +334,16 @@ 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.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +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.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +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.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -483,8 +483,8 @@ 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-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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/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= From e05fa5201423e0b8b8f5e5b2974c44bc59299f97 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:22:17 +0900 Subject: [PATCH 511/868] update runc (1.4.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c20bc71ba3f..38a8a6f73af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.3.0-beta.2@8a5337317f3216cd920283334f69b2f9003f75b2 -ARG RUNC_VERSION=v1.4.0@8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4 +ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From 79869bd141dabaa4bdce1299213dd009f70fc2f9 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:24:10 +0900 Subject: [PATCH 512/868] update BuildKit (0.29.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 diff --git a/Dockerfile b/Dockerfile index 38a8a6f73af..a7be32b02ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.28.1@BINARY +ARG BUILDKIT_VERSION=v0.29.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 deleted file mode 100644 index 92ee556055e..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.28.1 +++ /dev/null @@ -1,2 +0,0 @@ -2e84057b941488c54575aee57015681f08c71042bd20c739b23879dfad3f2449 buildkit-v0.28.1.linux-amd64.tar.gz -40caf32b10cb0766f7440a53fea9cfe15c8116d64f9365c53ff417958a136332 buildkit-v0.28.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 new file mode 100644 index 00000000000..9c29c3409f0 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 @@ -0,0 +1,2 @@ +ab8d93c72253b450f34a43e1c480abc52380f4aec3a8a395aebf09489efef7a0 buildkit-v0.29.0.linux-amd64.tar.gz +99a279e30be2947294eece98d82d1461fcfdc47da59514cb85252bb5ef414801 buildkit-v0.29.0.linux-arm64.tar.gz From 0e14b74ca5710536206779e1b91e3a6e284cad01 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:25:16 +0900 Subject: [PATCH 513/868] update stargz-snapshotter (0.18.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 | 3 --- Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 diff --git a/Dockerfile b/Dockerfile index a7be32b02ec..c1f82e2aa14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.29.0@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.1@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 deleted file mode 100644 index 831e77f35a2..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.1 +++ /dev/null @@ -1,3 +0,0 @@ -f8f106a61b9fc797a6336d6c06435cdbf8b896f3f49fdc5288e08e87dff6bbdf stargz-snapshotter-v0.18.1-linux-amd64.tar.gz -643d04f5e97e83606b9ee129c2c33513df13a091dbc1dc084256d13a1034b749 stargz-snapshotter-v0.18.1-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 new file mode 100644 index 00000000000..e03654c4bac --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 @@ -0,0 +1,3 @@ +515a3c3af0012f192ace31fb79e910597977c77227e976680aeaaef6e9ae50a9 stargz-snapshotter-v0.18.2-linux-amd64.tar.gz +97719faad48fb55c92a49abb9f12f9890dcd0d2da7215c4c21581f135b95abc9 stargz-snapshotter-v0.18.2-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service From 175277e712e48d826c8b5b25dcc24f58522f7526 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:33:46 +0900 Subject: [PATCH 514/868] update Nydus (2.4.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c1f82e2aa14..2a5e2252a91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ 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.3.9 +ARG NYDUS_VERSION=v2.4.1 ARG SOCI_SNAPSHOTTER_VERSION=0.12.1 ARG KUBO_VERSION=v0.40.1 From 7e87a836ede6d03a9e249046b94e4b6a578fc165 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:34:14 +0900 Subject: [PATCH 515/868] update soci-snapshotter (0.13.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2a5e2252a91..1a196ccf7f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ 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.12.1 +ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 ARG KUBO_VERSION=v0.40.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 7dfa25212e2fe95ac8056db1afb85eb16de589ed Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 25 Apr 2026 22:34:35 +0900 Subject: [PATCH 516/868] update Kubo (0.41.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1a196ccf7f2..4cbddaeaf3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ 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.40.1 +ARG KUBO_VERSION=v0.41.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From b668c8cab325a40d7ed406c36f17da887a40e178 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 26 Apr 2026 00:24:02 +0900 Subject: [PATCH 517/868] CI: split containerd-version into windows/linux Split the `containerd-version` workflow input into `windows-containerd-version` and `linux-containerd-version` so the two platforms can be pinned independently. The accompanying SHA inputs follow the same split: `containerd-sha` becomes `linux-containerd-sha`, `containerd-service-sha` becomes `linux-containerd-service-sha`, and a new `windows-containerd-sha` is introduced. The Windows containerd provisioning script now verifies the tarball SHA256 (skipped under the canary sentinel, matching the linux behavior). Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Akihiro Suda --- .github/workflows/job-test-in-host.yml | 42 +++++++++++++++++------- .github/workflows/workflow-test.yml | 12 ++++--- hack/provisioning/windows/containerd.ps1 | 14 ++++++++ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 5319dd35474..b88c368cc6f 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -25,13 +25,19 @@ on: docker-version: required: true type: string - containerd-version: + windows-containerd-version: required: true type: string - containerd-sha: + windows-containerd-sha: required: true type: string - containerd-service-sha: + linux-containerd-version: + required: true + type: string + linux-containerd-sha: + required: true + type: string + linux-containerd-service-sha: required: true type: string windows-cni-version: @@ -66,8 +72,10 @@ jobs: GO_VERSION: ${{ inputs.go-version }} # Both Docker and nerdctl on linux need rootful right now WITH_SUDO: ${{ contains(inputs.runner, 'ubuntu') }} - CONTAINERD_VERSION: ${{ inputs.containerd-version }} - CONTAINERD_SHA: ${{ inputs.containerd-sha }} + WINDOWS_CONTAINERD_VERSION: ${{ inputs.windows-containerd-version }} + WINDOWS_CONTAINERD_SHA: ${{ inputs.windows-containerd-sha }} + LINUX_CONTAINERD_VERSION: ${{ inputs.linux-containerd-version }} + LINUX_CONTAINERD_SHA: ${{ inputs.linux-containerd-sha }} steps: - name: "Init: checkout" @@ -85,11 +93,22 @@ jobs: [ "$latest_go" == "" ] || \ printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" - [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ] || { - printf "CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" - printf "CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" + + if [[ "${{ inputs.runner }}" == *windows* ]]; then + containerd_version="$WINDOWS_CONTAINERD_VERSION" + else + containerd_version="$LINUX_CONTAINERD_VERSION" + fi + [ "${latest_containerd:1}" == "$containerd_version" ] || { + if [[ "${{ inputs.runner }}" == *windows* ]]; then + printf "WINDOWS_CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" + printf "WINDOWS_CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" + else + printf "LINUX_CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" + printf "LINUX_CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" + fi } - if [ "$latest_go" == "" ] && [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ]; then + if [ "$latest_go" == "" ] && [ "${latest_containerd:1}" == "$containerd_version" ]; then echo "::warning title=No canary::There is currently no canary versions to test. Steps will not run."; printf "SHOULD_RUN=no\n" >> "$GITHUB_ENV" fi @@ -128,7 +147,7 @@ jobs: # FIXME: this is missing runc (see top level workflow note about the state of this) echo "::group:: install dependencies" sudo ./hack/provisioning/linux/containerd.sh uninstall - ./hack/provisioning/linux/containerd.sh rootful "$CONTAINERD_VERSION" "amd64" "$CONTAINERD_SHA" "${{ inputs.containerd-service-sha }}" + ./hack/provisioning/linux/containerd.sh rootful "$LINUX_CONTAINERD_VERSION" "amd64" "$LINUX_CONTAINERD_SHA" "${{ inputs.linux-containerd-service-sha }}" sudo ./hack/provisioning/linux/cni.sh uninstall ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" echo "::endgroup::" @@ -164,7 +183,8 @@ jobs: - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" env: - ctrdVersion: ${{ env.CONTAINERD_VERSION }} + ctrdVersion: ${{ env.WINDOWS_CONTAINERD_VERSION }} + ctrdSha: ${{ env.WINDOWS_CONTAINERD_SHA }} run: | # Install WinCNI echo "::group:: install wincni" diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index bd52eb8f74d..b1dd6a3c5c0 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,11 +146,13 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - containerd-version: 2.3.0-beta.2 - # FIXME: containerd-sha is not verified (only affects tests) + windows-containerd-version: 2.3.0-beta.2 + windows-containerd-sha: 174cd7dc3c1026c75610e87e07bc57d78c15582d05e4d932593cd46098ce06de + linux-containerd-version: 2.3.0-beta.2 + # FIXME: containerd SHAs are not verified for authenticity (only affects tests) # https://github.com/containerd/nerdctl/issues/4666 - # Note: these as for amd64 - containerd-sha: sha256:feabdaf784298c5389972a93bf670b80abf7e674cd20a9d629fe133552046d0e - containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 + # Note: these are for amd64 + linux-containerd-sha: feabdaf784298c5389972a93bf670b80abf7e674cd20a9d629fe133552046d0e + linux-containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/hack/provisioning/windows/containerd.ps1 b/hack/provisioning/windows/containerd.ps1 index 56f4219008c..bb01def9639 100644 --- a/hack/provisioning/windows/containerd.ps1 +++ b/hack/provisioning/windows/containerd.ps1 @@ -2,8 +2,22 @@ $ErrorActionPreference = "Stop" #install containerd $version=$env:ctrdVersion +$expectedSha=$env:ctrdSha echo "Installing containerd $version" curl.exe -L https://github.com/containerd/containerd/releases/download/v$version/containerd-$version-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz + +if ($expectedSha -eq "canary is volatile and I accept the risk") { + echo "Skipping SHA256 verification (canary)" +} else { + $expected = $expectedSha.ToLower() + $actual = (Get-FileHash -Algorithm SHA256 containerd-windows-amd64.tar.gz).Hash.ToLower() + if ($actual -ne $expected) { + Write-Error "SHA256 mismatch for containerd-windows-amd64.tar.gz: expected $expected, got $actual" + exit 1 + } + echo "SHA256 verified: $actual" +} + tar.exe xvf containerd-windows-amd64.tar.gz mkdir -force "$Env:ProgramFiles\containerd" cp ./bin/* "$Env:ProgramFiles\containerd" From ef7d38259f663d8476fabc2712688acfa871dfc2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 26 Apr 2026 00:30:27 +0900 Subject: [PATCH 518/868] CI: Windows: downgrade containerd to v2.2 Workaround for containerd/containerd issue 13254 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index b1dd6a3c5c0..bc77aff03dc 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -146,8 +146,11 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble - windows-containerd-version: 2.3.0-beta.2 - windows-containerd-sha: 174cd7dc3c1026c75610e87e07bc57d78c15582d05e4d932593cd46098ce06de + # Windows CI still requires containerd v2.2. + # [v2.3.0-beta.2 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.0-beta.2 # FIXME: containerd SHAs are not verified for authenticity (only affects tests) # https://github.com/containerd/nerdctl/issues/4666 From d60d859decb35f656afc2ded584c77688b254b36 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 26 Apr 2026 00:58:29 +0900 Subject: [PATCH 519/868] CI: Windows: set NO_HYPERV for canary Workaround for containerd/containerd issue 13254 Signed-off-by: Akihiro Suda --- .github/workflows/job-test-in-host.yml | 9 +++++++++ .github/workflows/workflow-test.yml | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index b88c368cc6f..9c21f9ed35c 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -15,6 +15,10 @@ on: required: false default: false type: boolean + no-hyperv: + required: false + default: false + type: boolean binary: required: false default: nerdctl @@ -83,6 +87,11 @@ jobs: with: fetch-depth: 1 + - if: ${{ inputs.no-hyperv }} + name: "Init (no-hyperv): Disable Hyper-V" + run: | + printf "NO_HYPERV=1\n" >> "$GITHUB_ENV" + - if: ${{ inputs.canary }} name: "Init (canary): retrieve latest go and containerd" env: diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index bc77aff03dc..09558b6ec3d 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -143,6 +143,10 @@ jobs: runner: ${{ matrix.runner }} binary: ${{ matrix.binary != '' && matrix.binary || 'nerdctl' }} canary: ${{ matrix.canary && true || false }} + # Hyper-V is broken on canary. + # [v2.3.0-beta.2 regression] The virtual machine or container JSON document is invalid. (Hyper-V container) + # https://github.com/containerd/containerd/issues/13254 + no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble From 00c13371ee8d627548df8c60d6ea276f896aeac2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 26 Apr 2026 03:52:34 +0900 Subject: [PATCH 520/868] nerdctl info: fulfill SELinux support info Signed-off-by: Akihiro Suda --- pkg/cmd/system/info.go | 2 +- pkg/infoutil/infoutil.go | 4 ++-- pkg/infoutil/infoutil_darwin.go | 2 +- pkg/infoutil/infoutil_freebsd.go | 2 +- pkg/infoutil/infoutil_linux.go | 9 ++++++--- pkg/infoutil/infoutil_windows.go | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/system/info.go b/pkg/cmd/system/info.go index 183fc577979..0c61833c13e 100644 --- a/pkg/cmd/system/info.go +++ b/pkg/cmd/system/info.go @@ -69,7 +69,7 @@ func Info(ctx context.Context, client *containerd.Client, options types.SystemIn return err } case "dockercompat": - infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager) + infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager, options.GOptions.SelinuxEnabled) if err != nil { return err } diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index 5dd6e84780d..2dee6f88a85 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -62,7 +62,7 @@ func NativeDaemonInfo(ctx context.Context, client *containerd.Client) (*native.D return daemonInfo, nil } -func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupManager string) (*dockercompat.Info, error) { +func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupManager string, selinuxEnabled bool) (*dockercompat.Info, error) { daemonVersion, err := client.Version(ctx) if err != nil { return nil, err @@ -95,7 +95,7 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan return nil, err } info.ServerVersion = daemonVersion.Version - fulfillPlatformInfo(&info) + fulfillPlatformInfo(&info, selinuxEnabled) return &info, nil } diff --git a/pkg/infoutil/infoutil_darwin.go b/pkg/infoutil/infoutil_darwin.go index 13c2ca2960c..5c60c6a75ea 100644 --- a/pkg/infoutil/infoutil_darwin.go +++ b/pkg/infoutil/infoutil_darwin.go @@ -28,7 +28,7 @@ func CgroupsVersion() string { return "" } -func fulfillPlatformInfo(info *dockercompat.Info) { +func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) { // unimplemented } diff --git a/pkg/infoutil/infoutil_freebsd.go b/pkg/infoutil/infoutil_freebsd.go index 4ef252e5981..76092798708 100644 --- a/pkg/infoutil/infoutil_freebsd.go +++ b/pkg/infoutil/infoutil_freebsd.go @@ -28,7 +28,7 @@ func CgroupsVersion() string { return "" } -func fulfillPlatformInfo(info *dockercompat.Info) { +func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) { // unimplemented } diff --git a/pkg/infoutil/infoutil_linux.go b/pkg/infoutil/infoutil_linux.go index e8c68a54b47..a6839c19ae2 100644 --- a/pkg/infoutil/infoutil_linux.go +++ b/pkg/infoutil/infoutil_linux.go @@ -42,7 +42,7 @@ func CgroupsVersion() string { return "1" } -func fulfillSecurityOptions(info *dockercompat.Info) { +func fulfillSecurityOptions(info *dockercompat.Info, selinuxEnabled bool) { if apparmorutil.CanApplyExistingProfile() { info.SecurityOptions = append(info.SecurityOptions, "name=apparmor") if rootlessutil.IsRootless() && !apparmorutil.CanApplySpecificExistingProfile(defaults.AppArmorProfileName) { @@ -52,6 +52,9 @@ WARNING: AppArmor profile %q is not loaded. This warning is negligible if you do not intend to use AppArmor.`), defaults.AppArmorProfileName)) } } + if selinuxEnabled { + info.SecurityOptions = append(info.SecurityOptions, "name=selinux") + } info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile="+defaults.SeccompProfileName) if defaults.CgroupnsMode() == "private" { info.SecurityOptions = append(info.SecurityOptions, "name=cgroupns") @@ -65,8 +68,8 @@ WARNING: AppArmor profile %q is not loaded. // // fulfillPlatformInfo requires the following fields to be set: // SecurityOptions, CgroupDriver, CgroupVersion -func fulfillPlatformInfo(info *dockercompat.Info) { - fulfillSecurityOptions(info) +func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) { + fulfillSecurityOptions(info, selinuxEnabled) mobySysInfo := mobySysInfo(info) if info.CgroupDriver == "none" { diff --git a/pkg/infoutil/infoutil_windows.go b/pkg/infoutil/infoutil_windows.go index 4256ead5431..19fac9b4ae1 100644 --- a/pkg/infoutil/infoutil_windows.go +++ b/pkg/infoutil/infoutil_windows.go @@ -194,7 +194,7 @@ func CgroupsVersion() string { return "" } -func fulfillPlatformInfo(info *dockercompat.Info) { +func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) { mobySysInfo := mobySysInfo(info) // NOTE: cgroup fields are not available on Windows From 48e8d41fabf408832c634c9c254acd37ceffa5ee Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 26 Apr 2026 03:54:35 +0900 Subject: [PATCH 521/868] test: replace `Not(NoSelinux)` with `Selinux` Signed-off-by: Akihiro Suda --- .../container/container_run_security_linux_test.go | 7 +++---- pkg/testutil/nerdtest/requirements.go | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index 4313cad4e3f..7826323a78f 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -27,7 +27,6 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" - "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -194,7 +193,7 @@ func TestRunApparmor(t *testing.T) { func TestRunSelinuxWithSecurityOpt(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.NoSelinux) + testCase.Require = nerdtest.Selinux testContainer := testutil.Identifier(t) testCase.SubTests = []*test.Case{ @@ -227,7 +226,7 @@ func TestRunSelinuxWithSecurityOpt(t *testing.T) { } func TestRunSelinux(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.NoSelinux) + testCase.Require = nerdtest.Selinux testContainer := testutil.Identifier(t) testCase.SubTests = []*test.Case{ @@ -261,7 +260,7 @@ func TestRunSelinux(t *testing.T) { func TestRunSelinuxWithVolumeLabel(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.NoSelinux) + testCase.Require = nerdtest.Selinux testContainer := testutil.Identifier(t) testCase.SubTests = []*test.Case{ diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 5b8463903ea..7fc0ff005de 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -162,14 +162,14 @@ var Rootless = &test.Requirement{ }, } -// NoSelinux marks a test as suitable only for the noselinux enable environment -var NoSelinux = &test.Requirement{ +// Selinux marks a test as suitable only for the selinux-enabled environment +var Selinux = &test.Requirement{ Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { - ret = !selinux.GetEnabled() + ret = selinux.GetEnabled() if ret { - mess = "selinux is disabled" - } else { mess = "selinux is enabled" + } else { + mess = "selinux is disabled" } return ret, mess }, From bbb5c32fa16755838b63d7009d65182d906e2a22 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Fri, 24 Apr 2026 07:59:24 +0900 Subject: [PATCH 522/868] test: refactor container_run_security_linux_test.go to use Tigron Signed-off-by: Park jungtae --- .../container_run_security_linux_test.go | 428 +++++++++++------- 1 file changed, 255 insertions(+), 173 deletions(-) diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index 7826323a78f..dd99f2d4699 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -17,9 +17,11 @@ package container import ( + "errors" "fmt" "os" "os/exec" + "regexp" "strconv" "strings" "testing" @@ -27,168 +29,216 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/apparmorutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -func getCapEff(base *testutil.Base, args ...string) uint64 { +const ( + CapNetRaw = 13 + CapIPCLock = 14 + + capEffShellCmd = "grep -w ^CapEff: /proc/self/status | sed -e \"s/^CapEff:[[:space:]]*//g\"" +) + +func getCapEff(helpers test.Helpers, args ...string) uint64 { fullArgs := []string{"run", "--rm"} fullArgs = append(fullArgs, args...) fullArgs = append(fullArgs, testutil.AlpineImage, "sh", "-euc", - "grep -w ^CapEff: /proc/self/status | sed -e \"s/^CapEff:[[:space:]]*//g\"", + capEffShellCmd, ) - cmd := base.Cmd(fullArgs...) - res := cmd.Run() - assert.NilError(base.T, res.Error) - s := strings.TrimSpace(res.Stdout()) + s := strings.TrimSpace(helpers.Capture(fullArgs...)) ui64, err := strconv.ParseUint(s, 16, 64) - assert.NilError(base.T, err) + assert.NilError(helpers.T(), err) return ui64 } -const ( - CapNetRaw = 13 - CapIPCLock = 14 -) - func TestRunCap(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - // allCaps varies depending on the target version and the kernel version. - allCaps := getCapEff(base, "--privileged") + testCase := nerdtest.Setup() // https://github.com/containerd/containerd/blob/9a9bd097564b0973bfdb0b39bf8262aa1b7da6aa/oci/spec.go#L93 - defaultCaps := uint64(0xa80425fb) + var defaultCaps uint64 = 0xa80425fb - t.Logf("allCaps=%016x", allCaps) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // allCaps varies depending on the target version and the kernel version. + allCaps := getCapEff(helpers, "--privileged") + helpers.T().Log(fmt.Sprintf("allCaps=%016x", allCaps)) + data.Labels().Set("allCaps", strconv.FormatUint(allCaps, 10)) + } - type testCase struct { - args []string - capEff uint64 + capCmd := func(args ...string) func(test.Data, test.Helpers) test.TestableCommand { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmdArgs := append([]string{"run", "--rm"}, args...) + cmdArgs = append(cmdArgs, testutil.AlpineImage, "sh", "-euc", capEffShellCmd) + return helpers.Command(cmdArgs...) + } } - testCases := []testCase{ + + capExpected := func(capEffFn func(allCaps uint64) uint64) func(test.Data, test.Helpers) *test.Expected { + return func(data test.Data, helpers test.Helpers) *test.Expected { + allCaps, _ := strconv.ParseUint(data.Labels().Get("allCaps"), 10, 64) + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(fmt.Sprintf("%016x\n", capEffFn(allCaps))), + } + } + } + + testCase.SubTests = []*test.Case{ { - capEff: allCaps & defaultCaps, + Description: "default", + Command: capCmd(), + Expected: capExpected(func(allCaps uint64) uint64 { return allCaps & defaultCaps }), }, { - args: []string{"--cap-add=all"}, - capEff: allCaps, + Description: "--cap-add=all", + Command: capCmd("--cap-add=all"), + Expected: capExpected(func(allCaps uint64) uint64 { return allCaps }), }, { - args: []string{"--cap-add=ipc_lock"}, - capEff: (allCaps & defaultCaps) | (1 << CapIPCLock), + Description: "--cap-add=ipc_lock", + Command: capCmd("--cap-add=ipc_lock"), + Expected: capExpected(func(allCaps uint64) uint64 { return (allCaps & defaultCaps) | (1 << CapIPCLock) }), }, { - args: []string{"--cap-add=all", "--cap-drop=net_raw"}, - capEff: allCaps ^ (1 << CapNetRaw), + Description: "--cap-add=all --cap-drop=net_raw", + Command: capCmd("--cap-add=all", "--cap-drop=net_raw"), + Expected: capExpected(func(allCaps uint64) uint64 { return allCaps ^ (1 << CapNetRaw) }), }, { - args: []string{"--cap-drop=all", "--cap-add=net_raw"}, - capEff: 1 << CapNetRaw, + Description: "--cap-drop=all --cap-add=net_raw", + Command: capCmd("--cap-drop=all", "--cap-add=net_raw"), + Expected: capExpected(func(allCaps uint64) uint64 { return 1 << CapNetRaw }), }, { - args: []string{"--cap-drop=all", "--cap-add=NET_RAW"}, - capEff: 1 << CapNetRaw, + Description: "--cap-drop=all --cap-add=NET_RAW", + Command: capCmd("--cap-drop=all", "--cap-add=NET_RAW"), + Expected: capExpected(func(allCaps uint64) uint64 { return 1 << CapNetRaw }), }, { - args: []string{"--cap-drop=all", "--cap-add=cap_net_raw"}, - capEff: 1 << CapNetRaw, + Description: "--cap-drop=all --cap-add=cap_net_raw", + Command: capCmd("--cap-drop=all", "--cap-add=cap_net_raw"), + Expected: capExpected(func(allCaps uint64) uint64 { return 1 << CapNetRaw }), }, { - args: []string{"--cap-drop=all", "--cap-add=CAP_NET_RAW"}, - capEff: 1 << CapNetRaw, + Description: "--cap-drop=all --cap-add=CAP_NET_RAW", + Command: capCmd("--cap-drop=all", "--cap-add=CAP_NET_RAW"), + Expected: capExpected(func(allCaps uint64) uint64 { return 1 << CapNetRaw }), }, } - for _, tc := range testCases { - tc := tc // IMPORTANT - name := "default" - if len(tc.args) > 0 { - name = strings.Join(tc.args, "_") - } - t.Run(name, func(t *testing.T) { - t.Parallel() - got := getCapEff(base, tc.args...) - assert.Equal(t, tc.capEff, got) - }) - } + + testCase.Run(t) } func TestRunSecurityOptSeccomp(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - type testCase struct { - args []string - seccomp int + testCase := nerdtest.Setup() + + seccompCmd := func(args ...string) func(test.Data, test.Helpers) test.TestableCommand { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmdArgs := append([]string{"run", "--rm"}, args...) + // NOTE: busybox grep does not support -oP \K + cmdArgs = append(cmdArgs, testutil.AlpineImage, "grep", "-Eo", `^Seccomp:\s*([0-9]+)`, "/proc/1/status") + return helpers.Command(cmdArgs...) + } + } + + seccompExpected := func(expectedSeccomp int) test.Manager { + return test.Expects(0, nil, expect.Match( + regexp.MustCompile(fmt.Sprintf(`Seccomp:\s*%d`, expectedSeccomp)), + )) } - testCases := []testCase{ + + testCase.SubTests = []*test.Case{ { - seccomp: 2, + Description: "default", + Command: seccompCmd(), + Expected: seccompExpected(2), }, { - args: []string{"--security-opt", "seccomp=unconfined"}, - seccomp: 0, + Description: "seccomp=unconfined", + Command: seccompCmd("--security-opt", "seccomp=unconfined"), + Expected: seccompExpected(0), }, { - args: []string{"--privileged"}, - seccomp: 0, + Description: "--privileged", + Command: seccompCmd("--privileged"), + Expected: seccompExpected(0), }, } - for _, tc := range testCases { - tc := tc // IMPORTANT - name := "default" - if len(tc.args) > 0 { - name = strings.Join(tc.args, "_") - } - t.Run(name, func(t *testing.T) { - t.Parallel() - args := []string{"run", "--rm"} - args = append(args, tc.args...) - // NOTE: busybox grep does not support -oP \K - args = append(args, testutil.AlpineImage, "grep", "-Eo", `^Seccomp:\s*([0-9]+)`, "/proc/1/status") - cmd := base.Cmd(args...) - f := func(expectedSeccomp int) func(string) error { - return func(stdout string) error { - s := strings.TrimPrefix(stdout, "Seccomp:") - s = strings.TrimSpace(s) - i, err := strconv.Atoi(s) - if err != nil { - return fmt.Errorf("failed to parse line %q: %w", stdout, err) - } - if i != expectedSeccomp { - return fmt.Errorf("expected Seccomp to be %d, got %d", expectedSeccomp, i) - } - return nil - } - } - cmd.AssertOutWithFunc(f(tc.seccomp)) - }) - } + + testCase.Run(t) } func TestRunApparmor(t *testing.T) { - base := testutil.NewBase(t) - defaultProfile := fmt.Sprintf("%s-default", base.Target) - if !apparmorutil.CanLoadNewProfile() && !apparmorutil.CanApplySpecificExistingProfile(defaultProfile) { - t.Skipf("needs to be able to apply %q profile", defaultProfile) + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + defaultProfile := fmt.Sprintf("%s-default", testutil.GetTarget()) + if !apparmorutil.CanLoadNewProfile() && !apparmorutil.CanApplySpecificExistingProfile(defaultProfile) { + helpers.T().Skip(fmt.Sprintf("needs to be able to apply %q profile", defaultProfile)) + } + data.Labels().Set("defaultProfile", defaultProfile) + + attrCurrentPath := "/proc/self/attr/apparmor/current" + if _, err := os.Stat(attrCurrentPath); err != nil { + attrCurrentPath = "/proc/self/attr/current" + } + data.Labels().Set("attrCurrentPath", attrCurrentPath) } - attrCurrentPath := "/proc/self/attr/apparmor/current" - if _, err := os.Stat(attrCurrentPath); err != nil { - attrCurrentPath = "/proc/self/attr/current" + + testCase.SubTests = []*test.Case{ + { + Description: "default profile is enforced", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", testutil.AlpineImage, "cat", data.Labels().Get("attrCurrentPath")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(fmt.Sprintf("%s (enforce)\n", data.Labels().Get("defaultProfile"))), + } + }, + }, + { + Description: "explicit default profile is enforced", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "apparmor="+data.Labels().Get("defaultProfile"), + testutil.AlpineImage, "cat", data.Labels().Get("attrCurrentPath")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(fmt.Sprintf("%s (enforce)\n", data.Labels().Get("defaultProfile"))), + } + }, + }, + { + Description: "apparmor=unconfined", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--security-opt", "apparmor=unconfined", + testutil.AlpineImage, "cat", data.Labels().Get("attrCurrentPath")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("unconfined")), + }, + { + Description: "privileged implies unconfined", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--privileged", + testutil.AlpineImage, "cat", data.Labels().Get("attrCurrentPath")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("unconfined")), + }, } - attrCurrentEnforceExpected := fmt.Sprintf("%s (enforce)\n", defaultProfile) - base.Cmd("run", "--rm", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) - base.Cmd("run", "--rm", "--security-opt", "apparmor="+defaultProfile, testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) - base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined") - base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined") + + testCase.Run(t) } func TestRunSelinuxWithSecurityOpt(t *testing.T) { @@ -207,7 +257,7 @@ func TestRunSelinuxWithSecurityOpt(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) @@ -240,7 +290,7 @@ func TestRunSelinux(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: expect.All( func(stdout string, t tig.T) { inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) @@ -275,7 +325,7 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: expect.All( func(stdout string, t tig.T) { cmd := exec.Command("ls", "-Z", fmt.Sprintf("/%s", testContainer)) @@ -293,44 +343,65 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { // TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976 func TestRunSeccompCapSysPtrace(t *testing.T) { - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "--cap-add", "sys_ptrace", testutil.AlpineImage, "sh", "-euxc", "apk add -q strace && strace true").AssertOK() + testCase := nerdtest.Setup() + + 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") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) // Docker/Moby 's seccomp profile allows ptrace(2) by default, but containerd does not (yet): https://github.com/containerd/containerd/issues/6802 } func TestRunSystemPathsUnconfined(t *testing.T) { - base := testutil.NewBase(t) - - const findmnt = "`apk add -q findmnt && findmnt -R /proc && findmnt -R /sys`" - result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmnt).Run() - defaultContainerOutput := result.Combined() - - var confined []string - - for _, path := range []string{ - "/proc/kcore", - "/proc/keys", - "/proc/latency_stats", - "/proc/sched_debug", - "/proc/scsi", - "/proc/timer_list", - "/proc/timer_stats", - "/sys/firmware", - "/sys/fs/selinux", - } { - // Not each distribution will support every masked path here. - if strings.Contains(defaultContainerOutput, path) { - confined = append(confined, path) - } - } + testCase := nerdtest.Setup() - assert.Check(t, len(confined) != 0, "Default container has no confined paths to validate") + const findmntRCmd = "apk add -q findmnt && findmnt -R /proc && findmnt -R /sys" - result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmnt).Run() - unconfinedContainerOutput := result.Combined() + testCase.SubTests = []*test.Case{ + { + Description: "masked paths are unconfined", + Setup: func(data test.Data, helpers test.Helpers) { + defaultOut := helpers.Capture("run", "--rm", testutil.AlpineImage, "sh", "-euc", findmntRCmd) + + var confined []string + for _, path := range []string{ + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/sched_debug", + "/proc/scsi", + "/proc/timer_list", + "/proc/timer_stats", + "/sys/firmware", + "/sys/fs/selinux", + } { + // Not each distribution will support every masked path here. + if strings.Contains(defaultOut, path) { + confined = append(confined, path) + } + } - for _, path := range confined { - assert.Assert(t, !strings.Contains(unconfinedContainerOutput, path), fmt.Sprintf("%s should not be masked when unconfined", path)) + assert.Check(helpers.T(), len(confined) != 0, "Default container has no confined paths to validate") + data.Labels().Set("confined", strings.Join(confined, ",")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "systempaths=unconfined", + testutil.AlpineImage, "sh", "-euc", findmntRCmd) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + confined := strings.Split(data.Labels().Get("confined"), ",") + comparators := make([]test.Comparator, 0, len(confined)) + for _, path := range confined { + comparators = append(comparators, expect.DoesNotContain(path)) + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All(comparators...), + } + }, + }, } for _, path := range []string{ @@ -341,51 +412,62 @@ func TestRunSystemPathsUnconfined(t *testing.T) { "/proc/sysrq-trigger", "/proc/sys", } { - findmntPath := fmt.Sprintf("`apk add -q findmnt && findmnt %s`", path) - - result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run() - // Not each distribution will support every read-only path here. - if strings.Contains(result.Combined(), path) { - result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run() - assert.Assert(t, !strings.Contains(result.Combined(), "ro,"), fmt.Sprintf("%s should not be read-only when unconfined", path)) - } + findmntCmd := fmt.Sprintf("apk add -q findmnt && findmnt %s || true", path) + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: "path " + path + " is writable when unconfined", + Setup: func(data test.Data, helpers test.Helpers) { + out := helpers.Capture("run", "--rm", testutil.AlpineImage, "sh", "-euc", findmntCmd) + if !strings.Contains(out, path) { + helpers.T().Skip(fmt.Sprintf("%s not present, skipping", path)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "systempaths=unconfined", + testutil.AlpineImage, "sh", "-euc", findmntCmd) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("ro,")), + }) } + + testCase.Run(t) } func TestRunPrivileged(t *testing.T) { - // docker does not support --privileged-without-host-devices - testutil.DockerIncompatible(t) - - if rootlessutil.IsRootless() { - t.Skip("test skipped for rootless privileged containers") - } + testCase := nerdtest.Setup() - base := testutil.NewBase(t) + // docker does not support --privileged-without-host-devices + testCase.Require = require.All(require.Not(nerdtest.Docker), require.Not(nerdtest.Rootless)) + testCase.NoParallel = true - devPath := "/dev/dummy-zero" + const devPath = "/dev/dummy-zero" - // a dummy zero device: mknod /dev/dummy-zero c 1 5 - helperCmd := exec.Command("mknod", []string{devPath, "c", "1", "5"}...) - if out, err := helperCmd.CombinedOutput(); err != nil { - err = fmt.Errorf("cannot create %q: %q: %w", devPath, string(out), err) - t.Fatal(err) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // a dummy zero device: mknod /dev/dummy-zero c 1 5 + helpers.Custom("mknod", devPath, "c", "1", "5").Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } - // ensure the file will be removed in case of failed in the test - defer func() { - exec.Command("rm", devPath).Run() - }() - - // get device with host devices - base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "ls", devPath).AssertOutExactly(devPath + "\n") - - // get device without host devices - res := base.Cmd("run", "--rm", "--privileged", "--security-opt", "privileged-without-host-devices", testutil.AlpineImage, "ls", devPath).Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Custom("rm", "-f", devPath).Run(nil) + } - // normally for not a exists file, the `ls` will return `1``. - assert.Check(t, res.ExitCode != 0, res) + testCase.SubTests = []*test.Case{ + { + Description: "with host devices", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--privileged", testutil.AlpineImage, "ls", devPath) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(devPath+"\n")), + }, + { + Description: "without host devices", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--privileged", + "--security-opt", "privileged-without-host-devices", testutil.AlpineImage, "ls", devPath) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("No such file or directory")}, nil), + }, + } - // something like `ls: /dev/dummy-zero: No such file or directory` - assert.Check(t, strings.Contains(res.Combined(), "No such file or directory")) + testCase.Run(t) } From 68982c9c9dcde5fa82cca4110cb93e4c6908bc10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:22:23 +0000 Subject: [PATCH 523/868] build(deps): bump github.com/mattn/go-isatty from 0.0.21 to 0.0.22 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.21 to 0.0.22. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-version: 0.0.22 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1bfc2371c78..6a62761ab92 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.5 - github.com/mattn/go-isatty v0.0.21 //gomodjail:unconfined + 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.11 github.com/moby/sys/mount v0.3.4 diff --git a/go.sum b/go.sum index 168e23545e4..dc08558e1ec 100644 --- a/go.sum +++ b/go.sum @@ -190,8 +190,8 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= From 065061d8e2f26eab9b80de43b37cc8fc96a67994 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 29 Apr 2026 21:39:40 +0800 Subject: [PATCH 524/868] MAINTAINERS: update ChengyuZhu6's gpg key Signed-off-by: ChengyuZhu6 --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index be8add49510..c7623695aae 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -17,7 +17,7 @@ "Zheaoli", "Zheao Li", "me@manjusaka.me","6E0D D9FA BAD5 AF61 D884 01EE 878F 445D 9C6C E65E" "djdongjin", "Jin Dong", "djdongjin95@gmail.com","" "yankay", "Kay Yan", "kay.yan@daocloud.io", "" -"ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","" +"ChengyuZhu6","Chengyu Zhu","hudson@cyzhu.com","521B 0CBA 0657 089B 2B81 41FD E14F 08B8 908E D6E8" # REVIEWERS # GitHub ID, Name, Email address, GPG fingerprint From 2299ffbd9aad8137ced4d2a66245b37390f02a71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:33:08 +0000 Subject: [PATCH 525/868] build(deps): bump github.com/containerd/continuity from 0.4.5 to 0.5.0 Bumps [github.com/containerd/continuity](https://github.com/containerd/continuity) from 0.4.5 to 0.5.0. - [Release notes](https://github.com/containerd/continuity/releases) - [Commits](https://github.com/containerd/continuity/compare/v0.4.5...v0.5.0) --- updated-dependencies: - dependency-name: github.com/containerd/continuity dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1bfc2371c78..0fda334bc37 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.11.0-beta.2 github.com/containerd/containerd/v2 v2.3.0-beta.2 //gomodjail:unconfined - github.com/containerd/continuity v0.4.5 //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 github.com/containerd/go-cni v1.1.13 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 168e23545e4..9a73dbb5b8f 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/containerd/containerd/api v1.11.0-beta.2 h1:bMyDRSESgCyGQ3xYboaU7fQlU github.com/containerd/containerd/api v1.11.0-beta.2/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= github.com/containerd/containerd/v2 v2.3.0-beta.2 h1:ra5zjIRSukNZSmNEBd8Rc4U/k1UySHlAYfWroz8vmcA= github.com/containerd/containerd/v2 v2.3.0-beta.2/go.mod h1:XzMeqc+joRAaJ4ceelXPQhOJ7ApekaR1zRs85cceutE= -github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= -github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +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= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= From cead61efe0511bb4fa4f0a5d0b28086fe6f87078 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 30 Apr 2026 10:54:32 +0800 Subject: [PATCH 526/868] hack: update release note Signed-off-by: ChengyuZhu6 --- hack/generate-release-note.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/generate-release-note.sh b/hack/generate-release-note.sh index 54124b79500..9d5a49d7f4f 100755 --- a/hack/generate-release-note.sh +++ b/hack/generate-release-note.sh @@ -25,7 +25,7 @@ cat <<-EOX (To be documented) ## Compatible containerd versions -This release of nerdctl is expected to be used with containerd v1.7, v2.0, v2.1, or v2.2. +This release of nerdctl is expected to be used with containerd v1.7, v2.0, v2.1, v2.2, or v2.3. Some features may not work with other releases of containerd. ## About the binaries From e8e886fc2baf54a5980c35a2d732ab716d3a5c2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:33:24 +0000 Subject: [PATCH 527/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.0-beta.2 to 2.3.0. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.0-beta.2...v2.3.0) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 0fda334bc37..b947d96dfbc 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/containerd/accelerated-container-image v1.4.3 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.0-beta.2 - github.com/containerd/containerd/v2 v2.3.0-beta.2 //gomodjail:unconfined + github.com/containerd/containerd/api v1.11.0 + github.com/containerd/containerd/v2 v2.3.0 //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 @@ -80,7 +80,7 @@ require ( github.com/cilium/ebpf v0.17.3 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect - github.com/containerd/plugin v1.0.0 // indirect + github.com/containerd/plugin v1.1.0 // indirect github.com/containerd/ttrpc v1.2.8 // indirect github.com/containers/ocicrypt v1.2.1 // indirect github.com/creack/pty v1.1.24 // indirect diff --git a/go.sum b/go.sum index 9a73dbb5b8f..949f413910b 100644 --- a/go.sum +++ b/go.sum @@ -32,10 +32,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.11.0-beta.2 h1:bMyDRSESgCyGQ3xYboaU7fQlUPc6g0HufZ7LDSQnsJ0= -github.com/containerd/containerd/api v1.11.0-beta.2/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.0-beta.2 h1:ra5zjIRSukNZSmNEBd8Rc4U/k1UySHlAYfWroz8vmcA= -github.com/containerd/containerd/v2 v2.3.0-beta.2/go.mod h1:XzMeqc+joRAaJ4ceelXPQhOJ7ApekaR1zRs85cceutE= +github.com/containerd/containerd/api v1.11.0 h1:smv4e74S/wwIx0Sj7lhwO1t3M/oi+mSzk2VXqHq8aO0= +github.com/containerd/containerd/api v1.11.0/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= +github.com/containerd/containerd/v2 v2.3.0 h1:qpB5dyToxPqea1OdedyAiAnnor5wxTM+Py9nWt5CnWY= +github.com/containerd/containerd/v2 v2.3.0/go.mod h1:+chyhxLNeqUVOcTJGgaSu/IbDGX6p3+d8AJjAaerAS8= 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= @@ -56,8 +56,8 @@ github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVW github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= -github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= -github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= +github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= github.com/containerd/stargz-snapshotter v0.18.2/go.mod h1:iS0a4lgCFjGbdBJNrm1jwvaMFGGnQ6PZ5Sd09i060h8= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= @@ -105,8 +105,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erofs/go-erofs v0.2.1 h1:6tFEewfzPTAVrLmNR16hdzQJGH62an9m75gJTbnGEPw= -github.com/erofs/go-erofs v0.2.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= +github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I= +github.com/erofs/go-erofs v0.3.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -274,8 +274,8 @@ 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.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= From ec02ca448c2d73155d53d0e2f59589aa1b48fac1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:33:36 +0000 Subject: [PATCH 528/868] build(deps): bump github.com/Masterminds/semver/v3 from 3.4.0 to 3.5.0 Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/Masterminds/semver/releases) - [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md) - [Commits](https://github.com/Masterminds/semver/compare/v3.4.0...v3.5.0) --- updated-dependencies: - dependency-name: github.com/Masterminds/semver/v3 dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0fda334bc37..bdb2997a2bd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ module github.com/containerd/nerdctl/v2 go 1.26.2 require ( - github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.1 github.com/compose-spec/compose-go/v2 v2.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 9a73dbb5b8f..3f0f3baffdc 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= From ea64ff9ced7dd3640fb7cbe7de1ab5f0adfd439d Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 1 May 2026 12:10:17 +0100 Subject: [PATCH 529/868] docs: fix missing list prefix for --security-opt label entry The --security-opt label flag was added by #4639 but was indented as a continuation of the apparmor line rather than its own list item. Add the missing '- ' prefix to restore consistent Markdown formatting. Partial fix for #3867 Signed-off-by: Ogulcan Aydogan --- docs/command-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index aecbd997a8f..7f3a1aa34f1 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -255,7 +255,7 @@ Security flags: - :whale: `--security-opt seccomp=`: specify custom seccomp profile - :whale: `--security-opt apparmor=`: specify custom AppArmor profile - :whale: `--security-opt label=`: specify custom selinux label +- :whale: `--security-opt label=`: specify custom selinux label - :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities - :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container - :whale: `--security-opt writable-cgroups`: making the cgroups writeable From c55d91be17b3a0ff048972dbd29eb9e0887874cc Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Thu, 23 Apr 2026 08:08:42 +0900 Subject: [PATCH 530/868] test: refactor container_stop_linux.go to use Tigron Signed-off-by: Park jungtae --- .../container/container_stop_linux_test.go | 337 +++++++++++------- 1 file changed, 217 insertions(+), 120 deletions(-) diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index e2f581a1ca8..388a66b8f2d 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -19,6 +19,7 @@ package container import ( "fmt" "io" + "strconv" "strings" "testing" "time" @@ -27,30 +28,22 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) func TestStopStart(t *testing.T) { - const ( - hostPort = 8080 - ) - testContainerName := testutil.Identifier(t) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName).Run() - - base.Cmd("run", "-d", - "--restart=no", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).AssertOK() + testCase := nerdtest.Setup() - 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 @@ -66,14 +59,66 @@ func TestStopStart(t *testing.T) { return nil } - assert.NilError(t, check(5)) - base.Cmd("stop", testContainerName).AssertOK() - base.Cmd("exec", testContainerName, "ps").AssertFail() - if check(1) == nil { - t.Fatal("expected to get an error") + 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 { + portlock.Release(port) + } + } + + 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)) + data.Labels().Set("containerName", data.Identifier()) + helpers.Ensure("run", "-d", + "--restart=no", + "--name", data.Identifier(), + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + testutil.NginxAlpineImage) + + assert.NilError(helpers.T(), httpCheck(data, 5)) + } + + testCase.SubTests = []*test.Case{ + { + Description: "container is stopped", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", data.Labels().Get("containerName")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + helpers.Fail("exec", data.Labels().Get("containerName"), "ps") + assert.Assert(t, httpCheck(data, 1) != nil, "expected HTTP to fail after stop") + }, + } + }, + }, + { + Description: "container is restarted", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", data.Labels().Get("containerName")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assert.NilError(t, httpCheck(data, 5)) + }, + } + }, + }, } - base.Cmd("start", testContainerName).AssertOK() - assert.NilError(t, check(5)) + + testCase.Run(t) } func TestStopWithStopSignal(t *testing.T) { @@ -91,131 +136,183 @@ func TestStopWithStopSignal(t *testing.T) { } // Verify that SIGQUIT was sent to the container AND that the container did forcefully exit - testCase.Expected = test.Expects(137, nil, expect.Contains(nerdtest.SignalCaught)) + testCase.Expected = test.Expects(expect.ExitCodeSigkill, nil, expect.Contains(nerdtest.SignalCaught)) testCase.Run(t) } func TestStopCleanupForwards(t *testing.T) { - const ( - hostPort = 9999 - testContainerName = "ngx" - ) - base := testutil.NewBase(t) - defer func() { - base.Cmd("rm", "-f", testContainerName).Run() - }() - - // skip if rootless - if rootlessutil.IsRootless() { - t.Skip("pkg/testutil/iptables does not support rootless") - } - - ipt, err := iptables.New() - assert.NilError(t, err) - - containerID := base.Cmd("run", "-d", - "--restart=no", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).Run().Stdout() - containerID = strings.TrimSuffix(containerID, "\n") - - containerIP := base.Cmd("inspect", - "-f", - "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", - testContainerName).Run().Stdout() - containerIP = strings.ReplaceAll(containerIP, "'", "") - containerIP = strings.TrimSuffix(containerIP, "\n") - - // define iptables chain name depending on the target (docker/nerdctl) - var chain string - if nerdtest.IsDocker() { - chain = "DOCKER" - } else { - redirectChain := "CNI-HOSTPORT-DNAT" - chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) - } - assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true) - - base.Cmd("stop", testContainerName).AssertOK() - assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false) -} + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) -// Regression test for https://github.com/containerd/nerdctl/issues/3353 -func TestStopCreated(t *testing.T) { - t.Parallel() + 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 { + portlock.Release(port) + } + } + + 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)) + + containerID := strings.TrimSpace(helpers.Capture("run", "-d", + "--restart=no", + "--name", data.Identifier(), + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + testutil.NginxAlpineImage)) + + containerIP := strings.TrimSpace(helpers.Capture("inspect", + "-f", "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", + data.Identifier())) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) + data.Labels().Set("containerIP", containerIP) + + ipt, err := iptables.New() + assert.NilError(helpers.T(), err) + + // define iptables chain name depending on the target (docker/nerdctl) + var chain string + if nerdtest.IsDocker() { + chain = "DOCKER" + } else { + chain = iptablesutil.GetRedirectedChain(t, ipt, "CNI-HOSTPORT-DNAT", testutil.Namespace, containerID) + } + data.Labels().Set("chain", chain) + + assert.Equal(helpers.T(), iptablesutil.ForwardExists(t, ipt, chain, containerIP, port), true) + } - tearDown := func() { - base.Cmd("rm", "-f", tID).Run() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", data.Identifier()) } - setup := func() { - base.Cmd("create", "--name", tID, testutil.CommonImage).AssertOK() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, _ tig.T) { + ipt, err := iptables.New() + assert.NilError(t, err) + chain := data.Labels().Get("chain") + containerIP := data.Labels().Get("containerIP") + port, _ := strconv.Atoi(data.Labels().Get("hostPort")) + assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, port), false) + }, + } } - t.Cleanup(tearDown) - tearDown() - setup() + testCase.Run(t) +} - base.Cmd("stop", tID).AssertOK() +// Regression test for https://github.com/containerd/nerdctl/issues/3353 +func TestStopCreated(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--name", data.Identifier(), testutil.CommonImage) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", data.Identifier()) + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + testCase.Run(t) } func TestStopWithLongTimeoutAndSIGKILL(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testContainerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainerName).Run() + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Start a container that sleeps forever + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + } - // Start a container that sleeps forever - base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Stop the container with a 5-second timeout and SIGKILL + // The container should be stopped almost immediately, well before the 5-second timeout + cmd := helpers.Command("stop", "--time=5", "--signal", "SIGKILL", data.Identifier()) + cmd.WithTimeout(5 * time.Second) + return cmd + } - // Stop the container with a 5-second timeout and SIGKILL - start := time.Now() - base.Cmd("stop", "--time=5", "--signal", "SIGKILL", testContainerName).AssertOK() - elapsed := time.Since(start) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) - // The container should be stopped almost immediately, well before the 5-second timeout - assert.Assert(t, elapsed < 5*time.Second, "Container wasn't stopped immediately with SIGKILL") + testCase.Run(t) } func TestStopWithTimeout(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testContainerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", testContainerName).Run() + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Start a container that sleeps forever + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + } - // Start a container that sleeps forever - base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Stop the container with a 3-second timeout + // The container should get the SIGKILL before the 10s default timeout + cmd := helpers.Command("stop", "--time=3", data.Identifier()) + cmd.WithTimeout(10 * time.Second) + return cmd + } - // Stop the container with a 3-second timeout - start := time.Now() - base.Cmd("stop", "--time=3", testContainerName).AssertOK() - elapsed := time.Since(start) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) - // The container should get the SIGKILL before the 10s default timeout - assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag") + testCase.Run(t) } func TestStopCleanupFIFOs(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("/run/containerd/fifo/ doesn't exist on rootless") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - testContainerName := testutil.Identifier(t) - oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") - assert.NilError(t, err) - // Stop the container after 2 seconds - go func() { + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + 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) { + oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(helpers.T(), err) + data.Labels().Set("oldNumFifos", strconv.Itoa(oldNumFifos)) + + cmd := helpers.Command("run", "--rm", "--name", data.Identifier(), testutil.NginxAlpineImage) + cmd.Background() + time.Sleep(2 * time.Second) - base.Cmd("stop", testContainerName).AssertOK() - newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") - assert.NilError(t, err) - assert.Equal(t, oldNumFifos, newNumFifos) - }() - // Start a container that is automatically removed after it exits - base.Cmd("run", "--rm", "--name", testContainerName, testutil.NginxAlpineImage).AssertOK() + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, _ tig.T) { + oldNumFifos, _ := strconv.Atoi(data.Labels().Get("oldNumFifos")) + newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") + assert.NilError(t, err) + assert.Equal(t, oldNumFifos, newNumFifos) + }, + } + } + + testCase.Run(t) } From 28985ae1b6121b38d0fcbe02ce57c4375727120c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 05:12:48 +0000 Subject: [PATCH 531/868] build(deps): bump github.com/klauspost/compress from 1.18.5 to 1.18.6 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.5 to 1.18.6. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.6) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e362c66ccff..132aacc2574 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.1 - github.com/klauspost/compress v1.18.5 + 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.11 diff --git a/go.sum b/go.sum index 4ae5d51f69f..9d09cb58b50 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX 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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +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= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 3b331db0ea252ec0fdf7c411706cde1d56165401 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 05:19:24 +0000 Subject: [PATCH 532/868] build(deps): bump github.com/docker/cli Bumps the docker group with 1 update in the / directory: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.4.1+incompatible to 29.4.3+incompatible - [Commits](https://github.com/docker/cli/compare/v29.4.1...v29.4.3) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.4.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8033890a169..f2f750b3e45 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.4.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.4.3+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 diff --git a/go.sum b/go.sum index 28e348d3e84..9c10917720d 100644 --- a/go.sum +++ b/go.sum @@ -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.4.1+incompatible h1:02RT8QqqwtGRn+6SYypv8IUEbD/ltY6sfKCJIoUcGzk= -github.com/docker/cli v29.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= +github.com/docker/cli v29.4.3+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= From b5e6eff66fde709eabb29ca413e7c0b7684de2c5 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 10 May 2026 14:06:46 +0400 Subject: [PATCH 533/868] fix: handle CNI config stat errors without panic Signed-off-by: immanuwell --- pkg/netutil/netutil_test.go | 13 +++++++++++++ pkg/netutil/store.go | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index 4c5459e706d..383054ebd3e 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -366,3 +366,16 @@ func TestNetworkWithDefaultNameAlreadyExists(t *testing.T) { assert.Assert(t, len(defaultNamedNetworksFileDefinitions) == 1) assert.Assert(t, defaultNamedNetworksFileDefinitions[0] == testConfFile) } + +func TestFSExistsPropagatesStatError(t *testing.T) { + path := filepath.Join(t.TempDir(), "cni-conf-root") + assert.NilError(t, filesystem.WriteFile(path, nil, 0600)) + + cniEnv := CNIEnv{ + NetconfPath: path, + } + + exists, err := fsExists(&cniEnv, DefaultNetworkName) + assert.Assert(t, !exists) + assert.Assert(t, err != nil) +} diff --git a/pkg/netutil/store.go b/pkg/netutil/store.go index 7376b3510c5..65247152794 100644 --- a/pkg/netutil/store.go +++ b/pkg/netutil/store.go @@ -53,7 +53,10 @@ func fsRemove(e *CNIEnv, net *NetworkConfig) error { func fsExists(e *CNIEnv, name string) (bool, error) { fi, err := os.Stat(getConfigPathForNetworkName(e, name)) - return !os.IsNotExist(err) && !fi.IsDir(), err + if err != nil { + return false, err + } + return !fi.IsDir(), nil } func fsWrite(e *CNIEnv, net *NetworkConfig) error { From 520a3df953323f2cc948cca2ffc641fcf180c7f0 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Mon, 27 Apr 2026 20:46:49 +0900 Subject: [PATCH 534/868] feat(compose): support healthcheck field in service config Signed-off-by: Park jungtae --- docs/compose.md | 2 +- pkg/composer/serviceparser/serviceparser.go | 57 ++++++++++++++++ .../serviceparser/serviceparser_test.go | 66 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/docs/compose.md b/docs/compose.md index a07c91a5207..02e59be61e9 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -28,7 +28,7 @@ which was derived from [Docker Compose file version 3 specification](https://doc - `services..deploy.resources.reservations` - `services..deploy.placement` - `services..deploy.endpoint_mode` -- `services..healthcheck` +- `services..healthcheck.start_interval` - `services..stop_grace_period` - `services..stop_signal` - `configs..external` diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index afd665fca6f..ad5c32c3e3b 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/identifiers" "github.com/containerd/nerdctl/v2/pkg/reflectutil" ) @@ -78,6 +79,7 @@ func warnUnknownFields(svc types.ServiceConfig) { "Extends", // handled by the loader "Extensions", "ExtraHosts", + "HealthCheck", "Hostname", "Image", "Init", @@ -121,6 +123,21 @@ func warnUnknownFields(svc types.ServiceConfig) { } } + if svc.HealthCheck != nil { + if unknown := reflectutil.UnknownNonEmptyFields(svc.HealthCheck, + "Test", + "Timeout", + "Interval", + "Retries", + "StartPeriod", + "Disable", + "Extensions", + // TODO: add support 'StartInterval' + ); len(unknown) > 0 { + log.L.Warnf("Ignoring: service %s: healthcheck: %+v", svc.Name, unknown) + } + } + for depName, dep := range svc.DependsOn { if unknown := reflectutil.UnknownNonEmptyFields(&dep, "Condition", @@ -747,6 +764,46 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e c.RunArgs = append(c.RunArgs, "-w="+svc.WorkingDir) } + if svc.HealthCheck != nil { + hc := svc.HealthCheck + disabled := hc.Disable + + if !disabled && len(hc.Test) > 0 { + switch hc.Test[0] { + case healthcheck.CmdNone: + disabled = true + case healthcheck.CmdShell: + if len(hc.Test) >= 2 { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-cmd=%s", hc.Test[1])) + } + case healthcheck.Cmd: + // CMD exec form is converted to CMD-SHELL because --health-cmd always stores + // the command as CMD-SHELL (see pkg/cmd/container/create.go: withHealthcheck). + // This means the command will be executed via /bin/sh -c instead of exec directly. + if len(hc.Test) >= 2 { + log.L.Warnf("service %s: healthcheck: CMD exec form is not supported, converting to CMD-SHELL", svc.Name) + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-cmd=%s", strings.Join(hc.Test[1:], " "))) + } + } + } + if disabled { + c.RunArgs = append(c.RunArgs, "--no-healthcheck") + } else { + if hc.Interval != nil { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-interval=%s", time.Duration(*hc.Interval).String())) + } + if hc.Timeout != nil { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-timeout=%s", time.Duration(*hc.Timeout).String())) + } + if hc.Retries != nil { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-retries=%d", *hc.Retries)) + } + if hc.StartPeriod != nil { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--health-start-period=%s", time.Duration(*hc.StartPeriod).String())) + } + } + } + c.RunArgs = append(c.RunArgs, parsed.Image) // NOT svc.Image c.RunArgs = append(c.RunArgs, svc.Command...) return &c, nil diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index 856d732cbf4..6754546d7b1 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -20,7 +20,9 @@ import ( "fmt" "path/filepath" "runtime" + "slices" "strconv" + "strings" "testing" "github.com/compose-spec/compose-go/v2/types" @@ -623,3 +625,67 @@ services: c = getContainersFromService("unless_stopped")[0] assert.Assert(t, in(c.RunArgs, "--restart=unless-stopped")) } + +func TestParseHealthCheck(t *testing.T) { + t.Parallel() + const dockerComposeYAML = ` +services: + cmd_shell: + image: alpine:3.14 + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + cmd_exec: + image: alpine:3.14 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 1m + disabled_flag: + image: alpine:3.14 + healthcheck: + disable: true + test: ["CMD", "curl", "-f", "http://localhost"] + disabled_none: + image: alpine:3.14 + healthcheck: + test: ["NONE"] +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + getContainersFromService := func(svcName string) []Container { + svcConfig, err := project.GetService(svcName) + assert.NilError(t, err) + svc, err := Parse(project, svcConfig) + assert.NilError(t, err) + return svc.Containers + } + + var c Container + + c = getContainersFromService("cmd_shell")[0] + assert.Assert(t, in(c.RunArgs, "--health-cmd=curl -f http://localhost || exit 1")) + assert.Assert(t, in(c.RunArgs, "--health-interval=30s")) + assert.Assert(t, in(c.RunArgs, "--health-timeout=10s")) + assert.Assert(t, in(c.RunArgs, "--health-retries=3")) + assert.Assert(t, in(c.RunArgs, "--health-start-period=5s")) + + c = getContainersFromService("cmd_exec")[0] + assert.Assert(t, in(c.RunArgs, "--health-cmd=curl -f http://localhost")) + assert.Assert(t, in(c.RunArgs, "--health-interval=1m0s")) + + c = getContainersFromService("disabled_flag")[0] + assert.Assert(t, in(c.RunArgs, "--no-healthcheck")) + assert.Assert(t, !slices.ContainsFunc(c.RunArgs, func(s string) bool { + return strings.HasPrefix(s, "--health-cmd=") + })) + + c = getContainersFromService("disabled_none")[0] + assert.Assert(t, in(c.RunArgs, "--no-healthcheck")) +} From e51bc852ec0f1c6a463f6369cc6c7c1034345b98 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Mon, 11 May 2026 09:08:12 +0900 Subject: [PATCH 535/868] test(compose): add test for healthcheck config Signed-off-by: Park jungtae --- cmd/nerdctl/compose/compose_up_linux_test.go | 109 ++++++++++++++++++ .../serviceparser/serviceparser_test.go | 42 +++---- 2 files changed, 126 insertions(+), 25 deletions(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 6e0476b859c..97f06b37fe0 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -23,6 +23,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" @@ -1314,3 +1315,111 @@ services: testCase.Run(t) } + +func TestComposeUpHealthcheck(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` +services: + web: + image: %s + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 2s +`, testutil.NginxAlpineImage) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + containerName := serviceparser.DefaultContainerName(projectName, "web", "1") + + data.Labels().Set("composePath", composePath) + data.Labels().Set("containerName", containerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, containerName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "inspect", data.Labels().Get("containerName")) + } + + 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), "unexpected number of containers") + hc := dc[0].Config.Healthcheck + assert.Assert(t, hc != nil, "healthcheck config should not be nil") + assert.Assert(t, len(hc.Test) >= 2, "healthcheck test should have at least 2 elements") + assert.Equal(t, "CMD-SHELL", hc.Test[0]) + assert.Equal(t, "curl -f http://localhost", hc.Test[1]) + assert.Equal(t, 10*time.Second, hc.Interval) + assert.Equal(t, 5*time.Second, hc.Timeout) + assert.Equal(t, 3, hc.Retries) + assert.Equal(t, 2*time.Second, hc.StartPeriod) + }), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composePath") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composePath"), "down", "-v") + } + } + + testCase.Run(t) +} + +func TestComposeUpHealthcheckDisabled(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` +services: + web: + image: %s + command: sleep infinity + healthcheck: + disable: true +`, testutil.CommonImage) + + composePath := data.Temp().Save(composeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + containerName := serviceparser.DefaultContainerName(projectName, "web", "1") + + data.Labels().Set("composePath", composePath) + data.Labels().Set("containerName", containerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, containerName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "inspect", data.Labels().Get("containerName")) + } + + 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), "unexpected number of containers") + hc := dc[0].Config.Healthcheck + assert.Assert(t, hc != nil, "healthcheck config should not be nil") + assert.Assert(t, len(hc.Test) >= 1, "healthcheck test should have at least 1 element") + assert.Equal(t, "NONE", hc.Test[0]) + }), + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composePath") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composePath"), "down", "-v") + } + } + + testCase.Run(t) +} diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index 6754546d7b1..8b567512097 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -33,6 +33,15 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil" ) +func getContainersFromService(t *testing.T, project *types.Project, svcName string) []Container { + t.Helper() + svcConfig, err := project.GetService(svcName) + assert.NilError(t, err) + svc, err := Parse(project, svcConfig) + assert.NilError(t, err) + return svc.Containers +} + func TestServicePortConfigToFlagP(t *testing.T) { t.Parallel() type testCase struct { @@ -603,26 +612,17 @@ services: project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) assert.NilError(t, err) - getContainersFromService := func(svcName string) []Container { - svcConfig, err := project.GetService(svcName) - assert.NilError(t, err) - svc, err := Parse(project, svcConfig) - assert.NilError(t, err) - - return svc.Containers - } - var c Container - c = getContainersFromService("onfailure_no_count")[0] + c = getContainersFromService(t, project, "onfailure_no_count")[0] assert.Assert(t, in(c.RunArgs, "--restart=on-failure")) - c = getContainersFromService("onfailure_with_count")[0] + c = getContainersFromService(t, project, "onfailure_with_count")[0] assert.Assert(t, in(c.RunArgs, "--restart=on-failure:10")) - c = getContainersFromService("onfailure_ignore")[0] + c = getContainersFromService(t, project, "onfailure_ignore")[0] assert.Assert(t, !in(c.RunArgs, "--restart=on-failure:3.14")) - c = getContainersFromService("unless_stopped")[0] + c = getContainersFromService(t, project, "unless_stopped")[0] assert.Assert(t, in(c.RunArgs, "--restart=unless-stopped")) } @@ -659,33 +659,25 @@ services: project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) assert.NilError(t, err) - getContainersFromService := func(svcName string) []Container { - svcConfig, err := project.GetService(svcName) - assert.NilError(t, err) - svc, err := Parse(project, svcConfig) - assert.NilError(t, err) - return svc.Containers - } - var c Container - c = getContainersFromService("cmd_shell")[0] + c = getContainersFromService(t, project, "cmd_shell")[0] assert.Assert(t, in(c.RunArgs, "--health-cmd=curl -f http://localhost || exit 1")) assert.Assert(t, in(c.RunArgs, "--health-interval=30s")) assert.Assert(t, in(c.RunArgs, "--health-timeout=10s")) assert.Assert(t, in(c.RunArgs, "--health-retries=3")) assert.Assert(t, in(c.RunArgs, "--health-start-period=5s")) - c = getContainersFromService("cmd_exec")[0] + c = getContainersFromService(t, project, "cmd_exec")[0] assert.Assert(t, in(c.RunArgs, "--health-cmd=curl -f http://localhost")) assert.Assert(t, in(c.RunArgs, "--health-interval=1m0s")) - c = getContainersFromService("disabled_flag")[0] + c = getContainersFromService(t, project, "disabled_flag")[0] assert.Assert(t, in(c.RunArgs, "--no-healthcheck")) assert.Assert(t, !slices.ContainsFunc(c.RunArgs, func(s string) bool { return strings.HasPrefix(s, "--health-cmd=") })) - c = getContainersFromService("disabled_none")[0] + c = getContainersFromService(t, project, "disabled_none")[0] assert.Assert(t, in(c.RunArgs, "--no-healthcheck")) } From 0e3e7ae62238cd552ca4f5925dc8828ff3908359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 04:37:04 +0000 Subject: [PATCH 536/868] build(deps): bump the golang-x group with 5 updates Bumps the golang-x group with 5 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.50.0` | `0.51.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.53.0` | `0.54.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.43.0` | `0.44.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.42.0` | `0.43.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.36.0` | `0.37.0` | Updates `golang.org/x/crypto` from 0.50.0 to 0.51.0 - [Commits](https://github.com/golang/crypto/compare/v0.50.0...v0.51.0) Updates `golang.org/x/net` from 0.53.0 to 0.54.0 - [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0) Updates `golang.org/x/sys` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/sys/compare/v0.43.0...v0.44.0) Updates `golang.org/x/term` from 0.42.0 to 0.43.0 - [Commits](https://github.com/golang/term/compare/v0.42.0...v0.43.0) Updates `golang.org/x/text` from 0.36.0 to 0.37.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 0945aa33a21..a5fc639e7fe 100644 --- a/go.mod +++ b/go.mod @@ -65,12 +65,12 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.50.0 - golang.org/x/net v0.53.0 + golang.org/x/crypto v0.51.0 + golang.org/x/net v0.54.0 golang.org/x/sync v0.20.0 //gomodjail:unconfined - golang.org/x/sys v0.43.0 //gomodjail:unconfined - golang.org/x/term v0.42.0 //gomodjail:unconfined - golang.org/x/text v0.36.0 + golang.org/x/sys v0.44.0 //gomodjail:unconfined + golang.org/x/term v0.43.0 //gomodjail:unconfined + golang.org/x/text v0.37.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index 744b3e87034..41148bdeec1 100644 --- a/go.sum +++ b/go.sum @@ -366,8 +366,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= 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= @@ -399,8 +399,8 @@ 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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= 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= @@ -434,8 +434,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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.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= @@ -445,8 +445,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.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +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/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= @@ -456,8 +456,8 @@ 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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= 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= @@ -470,8 +470,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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= From 8207c2e33ffae60dcceaa878455371a854e83edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 04:38:05 +0000 Subject: [PATCH 537/868] build(deps): bump github.com/opencontainers/selinux Bumps [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) from 1.13.1 to 1.14.1. - [Release notes](https://github.com/opencontainers/selinux/releases) - [Commits](https://github.com/opencontainers/selinux/compare/v1.13.1...v1.14.1) --- updated-dependencies: - dependency-name: github.com/opencontainers/selinux dependency-version: 1.14.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0945aa33a21..e98c11b6d7f 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ 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.13.1 + github.com/opencontainers/selinux v1.14.1 github.com/pelletier/go-toml/v2 v2.3.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 744b3e87034..531684cea07 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ 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.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= -github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= +github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= +github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= From de4166a58c4fa99a663fb14bbf3e3b20abdcadd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 07:03:11 +0000 Subject: [PATCH 538/868] build(deps): bump github.com/moby/moby/v2 Bumps the docker group with 1 update in the / directory: [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/moby/moby/v2` from 2.0.0-beta.11 to 2.0.0-beta.12 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.11...v2.0.0-beta.12) --- updated-dependencies: - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index ff09af8ca4c..55efb06338f 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( 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.11 + github.com/moby/moby/v2 v2.0.0-beta.12 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.14.1 - github.com/pelletier/go-toml/v2 v2.3.0 + github.com/pelletier/go-toml/v2 v2.3.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined @@ -126,7 +126,7 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/tinylib/msgp v1.3.0 // indirect - github.com/vbatts/tar-split v0.12.2 // indirect + github.com/vbatts/tar-split v0.12.3 // indirect 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 @@ -134,9 +134,9 @@ require ( 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.yaml.in/yaml/v2 v2.4.3 // 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.35.0 // indirect + golang.org/x/mod v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect //gomodjail:unconfined google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index 32c6e9d7136..a0562c27f7b 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ 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.11 h1:0s92vMOUn8H/MDXGeYyEqI/ulYlVpQ1Dh1zSgprgQ/Y= -github.com/moby/moby/v2 v2.0.0-beta.11/go.mod h1:CJq/k6N+wkom/MTVsGVZfsB4FxrHt4sVS0yJZphif6w= +github.com/moby/moby/v2 v2.0.0-beta.12 h1:ckaoRb/GobxNpsU8H8himeEZvoiRORfMgkGJd1GJt3s= +github.com/moby/moby/v2 v2.0.0-beta.12/go.mod h1:eJm2E+/uFWGlIaI54/QZ9/wUbFpMJFv7xQon2pZt/eM= 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= @@ -262,8 +262,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= -github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= -github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/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= @@ -311,8 +311,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= -github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= +github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -352,8 +352,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 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= @@ -381,8 +381,8 @@ 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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= 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= @@ -470,8 +470,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +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= @@ -517,8 +517,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= -pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= +pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk= sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= From ae533fba32eb19d46fd9d497c1119f0be9c8e92a Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 11:05:57 +0100 Subject: [PATCH 539/868] refactor: migrate container_run_log_driver_syslog_test.go to nerdtest.Setup Part of the ongoing Tigron migration tracked in issue #4613. Converts the three syslog log-driver tests (TestSyslogNetwork, TestSyslogFacilities, TestSyslogFormat) from the legacy testutil.Base pattern to the nerdtest.Setup / test.Case framework. Key design decisions: - The cross-product of (network x facility x format) is expanded into independent SubTests via buildSyslogSubTests, mirroring the structure of the original table-driven loop. - Each sub-case owns its syslog server lifecycle (Start in Setup, receive in Cleanup) through closure-captured variables (addr, done, closer, containerName, tag, msg) so the channel-based validation can survive the Setup -> Command -> Cleanup split. - CA and cert are shared from the outer fixture to sub-cases via pointer-to-pointer (**testca.CA, **testca.Cert) populated in the outer testCase.Setup and read by each sub-case. - testca.New requires testing.TB; passed as *testing.T through the newSyslogTestCase(t) helper rather than helpers.T() which only implements the narrower tig.T interface. - Network skip logic honours rootless: the rootless path produces a more descriptive skip message per the existing pattern in the repo. Validator functions (rfc5424Validator, rfc3164Validator, emptyFormatValidator) are extracted as package-level helpers, identical in logic to the originals. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 370 ++++++++++-------- 1 file changed, 212 insertions(+), 158 deletions(-) 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..da64436951a 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,176 @@ import ( syslog "github.com/yuchanns/srslog" + "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") - } +// buildSyslogSubTests expands the (network x facility x format) cross product +// into independent Tigron sub-cases. Each sub-case starts its own syslog +// listener in Setup, runs a detached container in Command, and validates the +// received frame 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 cases []*test.Case - 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 { + network := network for rFK, rFV := range syslogFacilities { + rFK := rFK fPriV := rFV - // test both string and number facility for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { + fPriK := fPriK for fmtK, fmtValidFunc := range fmtValidFuncs { + fmtK := fmtK + fmtValidFunc := fmtValidFunc + 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) + subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) + + var ( + addr string + done chan string + closer io.Closer + containerName string + tag string + msg string + ) + + cases = append(cases, &test.Case{ + Description: subName, + Setup: func(data test.Data, helpers test.Helpers) { + if !testsyslog.TestableNetwork(network) { + if rootlessutil.IsRootless() { + helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", network)) + } + helpers.T().Skip(fmt.Sprintf("%q is not supported", network)) + } + tID := data.Identifier() + tag = tID + "_syslog_driver" + msg = "hello, " + tID + "_syslog_driver" + containerName = fmt.Sprintf("%s-%s", tID, fPriK) + done = make(chan string) + addr, closer = testsyslog.StartServer(network, "", done, *certRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + args := []string{ + "run", + "-d", + "--name", containerName, + "--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" { + 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, + ) } - 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) + args = append(args, testutil.CommonImage, "echo", msg) + return helpers.Command(args...) + }, + Expected: test.Expects(0, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if containerName != "" { + helpers.Anyhow("rm", "-f", containerName) } - case <-time.Tick(time.Second * 3): - t.Errorf("timeout with %s", subTestName) - } + if closer == nil || done == nil { + return + } + defer closer.Close() + defer close(done) + select { + case rcvd := <-done: + if err := fmtValidFunc(rcvd, msg, tag, *hostnameRef, fPriV, 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 +205,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 +242,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 } From 4477c6b6abcd0caa6abcc8db4ba082c05d01c911 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 14:19:38 +0100 Subject: [PATCH 540/868] fix: start syslog server in Command to prevent premature timeout The runPacketSyslog goroutine in testsyslog has a 300ms window (3 x 100ms deadlines) before it gives up and sends an empty string on the done channel. When StartServer was called in Setup, the Tigron framework overhead between Setup and Command could exceed 300ms, causing the goroutine to time out before the container sent its first log entry -- resulting in rcvd="" and a validation failure. Moving StartServer to the Command callback ensures the goroutine starts immediately before nerdctl run -d, eliminating the gap. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 da64436951a..7f6b01a4693 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -39,8 +39,9 @@ import ( // buildSyslogSubTests expands the (network x facility x format) cross product // into independent Tigron sub-cases. Each sub-case starts its own syslog -// listener in Setup, runs a detached container in Command, and validates the -// received frame in Cleanup. +// 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, @@ -90,10 +91,14 @@ func buildSyslogSubTests( tag = tID + "_syslog_driver" msg = "hello, " + tID + "_syslog_driver" containerName = fmt.Sprintf("%s-%s", tID, fPriK) - done = make(chan string) - addr, closer = testsyslog.StartServer(network, "", done, *certRef) }, 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(network, "", done, *certRef) args := []string{ "run", "-d", From fd113efa789a5d40a8dd23863a85c332858a477c Mon Sep 17 00:00:00 2001 From: ysedira Date: Thu, 14 May 2026 18:55:51 +0200 Subject: [PATCH 541/868] Compose Down to accept a list of Services Signed-off-by: ysedira --- cmd/nerdctl/compose/compose_down.go | 9 +-- .../compose/compose_down_linux_test.go | 64 +++++++++++++++++++ pkg/composer/down.go | 5 +- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/compose/compose_down.go b/cmd/nerdctl/compose/compose_down.go index 1099273dcca..3465dcfbe96 100644 --- a/cmd/nerdctl/compose/compose_down.go +++ b/cmd/nerdctl/compose/compose_down.go @@ -27,9 +27,8 @@ import ( func downCommand() *cobra.Command { var cmd = &cobra.Command{ - Use: "down", + Use: "down [flags] [SERVICE...]", Short: "Remove containers and associated resources", - Args: cobra.NoArgs, RunE: downAction, SilenceUsage: true, SilenceErrors: true, @@ -39,7 +38,7 @@ func downCommand() *cobra.Command { return cmd } -func downAction(cmd *cobra.Command, args []string) error { +func downAction(cmd *cobra.Command, services []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { return err @@ -62,6 +61,8 @@ func downAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + options.Services = services + c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) if err != nil { return err @@ -71,5 +72,5 @@ func downAction(cmd *cobra.Command, args []string) error { RemoveVolumes: volumes, RemoveOrphans: removeOrphans, } - return c.Down(ctx, downOpts) + return c.Down(ctx, downOpts, services) } diff --git a/cmd/nerdctl/compose/compose_down_linux_test.go b/cmd/nerdctl/compose/compose_down_linux_test.go index 2eea0c01827..709c9f4877d 100644 --- a/cmd/nerdctl/compose/compose_down_linux_test.go +++ b/cmd/nerdctl/compose/compose_down_linux_test.go @@ -152,3 +152,67 @@ services: testCase.Run(t) } + +func TestComposeDownRemoveSpecifiedService(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` +services: + test1: + image: %s + command: "sleep infinity" + test2: + image: %s + command: "sleep infinity" +`, testutil.CommonImage, testutil.CommonImage) + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + + projectName := data.Identifier("project") + t.Logf("projectName=%q", projectName) + + testContainer1 := serviceparser.DefaultContainerName(projectName, "test1", "1") + testContainer2 := serviceparser.DefaultContainerName(projectName, "test2", "1") + + data.Labels().Set("composePath", composePath) + data.Labels().Set("projectName", projectName) + data.Labels().Set("testContainer1", testContainer1) + data.Labels().Set("testContainer2", testContainer2) + + helpers.Ensure("compose", "-p", projectName, "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, testContainer1) + nerdtest.EnsureContainerStarted(helpers, testContainer2) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composePath"), "down", "test1") + } + + testCase.Expected = test.Expects(0, nil, nil) + + testCase.SubTests = []*test.Case{ + { + Description: "only specified service is removed", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composePath"), "ps", "-a") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.All( + expect.DoesNotContain(data.Labels().Get("testContainer1")), + expect.Contains(data.Labels().Get("testContainer2")), + ), + } + }, + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if composePath := data.Labels().Get("composePath"); composePath != "" { + helpers.Anyhow("compose", "-p", data.Labels().Get("projectName"), "-f", composePath, "down", "-v") + } + } + + testCase.Run(t) +} diff --git a/pkg/composer/down.go b/pkg/composer/down.go index d7d7c2f0f6b..e38e689426f 100644 --- a/pkg/composer/down.go +++ b/pkg/composer/down.go @@ -28,10 +28,11 @@ import ( type DownOptions struct { RemoveVolumes bool RemoveOrphans bool + Services []string } -func (c *Composer) Down(ctx context.Context, downOptions DownOptions) error { - serviceNames, err := c.ServiceNames() +func (c *Composer) Down(ctx context.Context, downOptions DownOptions, services []string) error { + serviceNames, err := c.ServiceNames(services...) if err != nil { return err } From bd8704edd4eb625a200a94907950a3661e691518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 22:33:10 +0000 Subject: [PATCH 542/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.4.3+incompatible to 29.5.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.4.3...v29.5.0) Updates `github.com/moby/moby/v2` from 2.0.0-beta.12 to 2.0.0-beta.13 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.12...v2.0.0-beta.13) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.5.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 55efb06338f..11af046598c 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.4.3+incompatible //gomodjail:unconfined + github.com/docker/cli v29.5.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 @@ -44,7 +44,7 @@ require ( 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.12 + github.com/moby/moby/v2 v2.0.0-beta.13 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index a0562c27f7b..23092c39833 100644 --- a/go.sum +++ b/go.sum @@ -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.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= -github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4= +github.com/docker/cli v29.5.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= @@ -212,8 +212,8 @@ 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.12 h1:ckaoRb/GobxNpsU8H8himeEZvoiRORfMgkGJd1GJt3s= -github.com/moby/moby/v2 v2.0.0-beta.12/go.mod h1:eJm2E+/uFWGlIaI54/QZ9/wUbFpMJFv7xQon2pZt/eM= +github.com/moby/moby/v2 v2.0.0-beta.13 h1:Hwl5dI34UVr/ZSUZuMYuaoun3+JaQgBzwzzAgCduZ44= +github.com/moby/moby/v2 v2.0.0-beta.13/go.mod h1:SmAxBJ3ELHfRpQVTMeT7aD5YfVfwT437Kogh094jG/I= 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= From 33cd71712295edef41177b99047350e699ba3ee4 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 23:41:07 +0100 Subject: [PATCH 543/868] fix: run syslog subtests sequentially to respect server deadline runPacketSyslog uses 4x100ms read deadlines (~400ms total). When subtests run in parallel on slow arm runners, container startup latency exceeds that window, causing the server to drain and send an empty string on the done channel before the log entry arrives. Setting NoParallel matches the original sequential t.Run behaviour. Signed-off-by: Ogulcan Aydogan --- .../container/container_run_log_driver_syslog_test.go | 5 +++++ 1 file changed, 5 insertions(+) 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 7f6b01a4693..7f412c367e6 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -80,6 +80,11 @@ func buildSyslogSubTests( 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(network) { if rootlessutil.IsRootless() { From e6655ce31cda661d9085752a06f8c4f5f7eff2e7 Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Thu, 14 May 2026 19:26:20 -0700 Subject: [PATCH 544/868] docs: fix sig-proxy flag spelling Signed-off-by: mukunda katta --- docs/command-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 7f3a1aa34f1..f18376e6b39 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -155,7 +155,7 @@ Basic flags: - :whale: `-i, --interactive`: Keep STDIN open even if not attached" - :whale: `-t, --tty`: Allocate a pseudo-TTY - :warning: WIP: currently `-t` conflicts with `-d` -- :whale: `-sig-proxy`: Proxy received signals to the process (default true) +- :whale: `--sig-proxy`: Proxy received signals to the process (default true) - :whale: `-d, --detach`: Run container in background and print container ID - :whale: `--restart=(no|always|on-failure|unless-stopped)`: Restart policy to apply when a container exits - Default: "no" From 66c23586c76a59b1aea3386f7d0460719ce6b8d4 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 09:32:35 +0100 Subject: [PATCH 545/868] fix: extend runPacketSyslog wait window to 2s before first packet UDP and unixgram transports rely on a deadline-based loop in runPacketSyslog. The original 4x100ms (400ms) window was too short on slow ARM runners where container startup + syslog driver initialization exceeds the budget, causing the server to close the socket and send an empty string on the done channel. Increase the pre-packet retry count from 3 to 20 (2s total). Once the first datagram arrives, reset to the original 3 retries (400ms) to drain any remaining bytes promptly. Signed-off-by: Ogulcan Aydogan --- pkg/testutil/testsyslog/testsyslog.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 } From 2ff6c0dc2b2f711b484c2c66742f9762b3c8d6d0 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 14:30:40 +0100 Subject: [PATCH 546/868] refactor: use params struct and exit code constant in syslog sub-tests Replace nested loop variable shadowing (x := x) with a syslogCombination struct that pre-builds all (network x facility x format) combinations into a slice before creating test.Case entries. Each closure then captures the struct value from the range variable, making the data flow explicit. Also replace the hardcoded 0 literal in test.Expects with expect.ExitCodeSuccess for consistency with the rest of the Tigron test suite. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 203 ++++++++++-------- 1 file changed, 109 insertions(+), 94 deletions(-) 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 7f412c367e6..2521ea6f709 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -27,6 +27,7 @@ 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" @@ -37,6 +38,15 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/testsyslog" ) +// 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 @@ -50,110 +60,115 @@ func buildSyslogSubTests( certRef **testca.Cert, hostnameRef *string, ) []*test.Case { - var cases []*test.Case + var combinations []syslogCombination for _, network := range networks { - network := network for rFK, rFV := range syslogFacilities { - rFK := rFK - fPriV := rFV - for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { - fPriK := fPriK + for _, fPriK := range []string{rFK, strconv.Itoa(int(rFV) >> 3)} { for fmtK, fmtValidFunc := range fmtValidFuncs { - fmtK := fmtK - fmtValidFunc := fmtValidFunc - - fmtKT := "empty" - if fmtK != "" { - fmtKT = fmtK - } - subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), 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(network) { - if rootlessutil.IsRootless() { - helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", network)) - } - helpers.T().Skip(fmt.Sprintf("%q is not supported", network)) - } - tID := data.Identifier() - tag = tID + "_syslog_driver" - msg = "hello, " + tID + "_syslog_driver" - containerName = fmt.Sprintf("%s-%s", tID, 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(network, "", done, *certRef) - args := []string{ - "run", - "-d", - "--name", containerName, - "--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" { - 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(0, 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 := fmtValidFunc(rcvd, msg, tag, *hostnameRef, fPriV, 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() - } - }, + 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 } From 22ebf232099782d7cf721006bc91cfe996901847 Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Thu, 14 May 2026 19:31:01 -0700 Subject: [PATCH 547/868] docs: remove stray quote from interactive flag Signed-off-by: mukunda katta --- docs/command-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 7f3a1aa34f1..7fc59e5e039 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -152,7 +152,7 @@ Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]` Basic flags: - :whale: `-a, --attach`: Attach STDIN, STDOUT, or STDERR -- :whale: `-i, --interactive`: Keep STDIN open even if not attached" +- :whale: `-i, --interactive`: Keep STDIN open even if not attached - :whale: `-t, --tty`: Allocate a pseudo-TTY - :warning: WIP: currently `-t` conflicts with `-d` - :whale: `-sig-proxy`: Proxy received signals to the process (default true) From efc8f737be4c618add16e6df7cb8e77a5b2dafdd Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 15:56:29 +0100 Subject: [PATCH 548/868] refactor: migrate login_linux_test.go to nerdtest.Setup Replace testutil.NewBase-based test helpers with the Tigron test framework (nerdtest.Setup). Key changes: - Client.Run(base, host) replaced with Client.Cmd(helpers, host) returning test.TestableCommand; DOCKER_CONFIG set via Setenv. - testregistry.NewRegistry / testca.New replaced with nerdtest.RegistryWithBasicAuth, nerdtest.RegistryWithTokenAuth, and lower-level registry.NewCesantaAuthServer + registry.NewDockerRegistry for the token-auth/no-TLS case (HTTP registry, matching original behaviour). - TestLoginPersistence: two SubTests (basic, token) each owning their registry lifecycle via Setup/Cleanup. - TestLoginAgainstVariants: dynamically generated SubTests from the existing test-case table; inner regHost and assertion loops run sequentially inside Setup (equivalent coverage without nested t.Run). - testutil.DockerIncompatible replaced with require.Not(nerdtest.Docker). - TestAgainstNoAuth remains commented out, unchanged. Closes #4613 Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/login/login_linux_test.go | 691 ++++++++++++-------------- 1 file changed, 310 insertions(+), 381 deletions(-) diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 55544b33ad8..8ca77ecac00 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) } /* @@ -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) } From ec4fc3916826d031c085dc905aaeaef0f6b6f048 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Sat, 16 May 2026 23:10:55 +0100 Subject: [PATCH 549/868] refactor: use randomPort constant instead of raw 0 in registry init Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/login/login_linux_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 8ca77ecac00..90834490508 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -190,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() { From cc52ce6248ff3d0228af75e5a2fa4434fff6ce9a Mon Sep 17 00:00:00 2001 From: Omri SirComp Date: Mon, 18 May 2026 08:57:24 +0300 Subject: [PATCH 550/868] ci: add zizmor workflow linting Signed-off-by: Omri SirComp --- .../ghcr-image-build-and-publish.yml | 2 ++ .github/workflows/job-build.yml | 1 + .github/workflows/job-lint-go.yml | 5 ++- .github/workflows/job-lint-other.yml | 1 + .github/workflows/job-lint-project.yml | 1 + .github/workflows/job-test-dependencies.yml | 6 ++-- .github/workflows/job-test-in-container.yml | 36 ++++++++++++------- .github/workflows/job-test-in-host.yml | 34 ++++++++++++------ .github/workflows/job-test-in-lima.yml | 4 ++- .github/workflows/job-test-in-vagrant.yml | 1 + .github/workflows/job-test-unit.yml | 9 +++-- .github/workflows/release.yml | 3 ++ .github/workflows/workflow-flaky.yml | 4 +++ .github/workflows/workflow-lint.yml | 20 +++++++++++ .github/workflows/workflow-test.yml | 3 ++ .github/workflows/workflow-tigron.yml | 5 +++ 16 files changed, 106 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index a592fa41951..253ba2db905 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -32,6 +32,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index fcaee2c7273..7969ef16c7c 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -38,6 +38,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 6b26b2830d7..dbe6c08cf83 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -42,6 +42,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - if: ${{ inputs.canary }} name: "Init (canary): retrieve GO_VERSION" @@ -74,5 +75,7 @@ jobs: if [ "${{ inputs.canary }}" == "true" ]; then NO_COLOR=true make lint-go-all else - NO_COLOR=true GOOS="${{ inputs.goos }}" make lint-go + NO_COLOR=true GOOS="${INPUTS_GOOS}" make lint-go fi + env: + INPUTS_GOOS: ${{ inputs.goos }} diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 25826f41226..5ebcfb1a24c 100644 --- a/.github/workflows/job-lint-other.yml +++ b/.github/workflows/job-lint-other.yml @@ -28,6 +28,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - name: "Run: yaml" run: | diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 0cef414e883..eeea1a363f7 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -34,6 +34,7 @@ jobs: with: fetch-depth: 100 path: src/github.com/containerd/nerdctl + persist-credentials: false - name: "Init: install go" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index be00e2c349b..4c269d8d01c 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -34,6 +34,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 @@ -41,14 +42,15 @@ jobs: - name: "Run: build dependencies for the integration test environment image" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUTS_CONTAINERD_VERSION: ${{ inputs.containerd-version }} run: | # Cache is sharded per-architecture arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} docker buildx create --name with-gha --use # Honor old containerd if requested args=() - if [ "${{ inputs.containerd-version }}" != "" ]; then - args=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) + if [ "${INPUTS_CONTAINERD_VERSION}" != "" ]; then + args=(--build-arg CONTAINERD_VERSION=${INPUTS_CONTAINERD_VERSION}) fi docker buildx build \ --secret id=github_token,env=GITHUB_TOKEN \ diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 794b04d2a7b..b4626d83e67 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -70,6 +70,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - name: "Init: expose GitHub Runtime variables for gha" uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 @@ -98,18 +99,21 @@ jobs: name: "Init: prepare test image" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUTS_CONTAINERD_VERSION: ${{ inputs.containerd-version }} + INPUTS_TARGET: ${{ inputs.target }} + INPUTS_ROOTLESSKIT_VERSION: ${{ inputs.rootlesskit-version }} run: | buildargs=() # If the runner is old, use old ubuntu inside the container as well [ "${{ contains(inputs.runner, '22.04') }}" != "true" ] || buildargs=(--build-arg UBUNTU_VERSION=22.04) # Honor if we want old containerd - [ "${{ inputs.containerd-version }}" == "" ] || buildargs+=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) + [ "${INPUTS_CONTAINERD_VERSION}" == "" ] || buildargs+=(--build-arg CONTAINERD_VERSION=${INPUTS_CONTAINERD_VERSION}) # Honor custom targets and if we want old rootlesskit target=test-integration - if [ "${{ inputs.target }}" != "rootful" ]; then - target+=-${{ inputs.target }} - if [ "${{ inputs.rootlesskit-version }}" != "" ]; then - buildargs+=(--build-arg ROOTLESSKIT_VERSION=${{ inputs.rootlesskit-version }}) + if [ "${INPUTS_TARGET}" != "rootful" ]; then + target+=-${INPUTS_TARGET} + if [ "${INPUTS_ROOTLESSKIT_VERSION}" != "" ]; then + buildargs+=(--build-arg ROOTLESSKIT_VERSION=${INPUTS_ROOTLESSKIT_VERSION}) fi fi # Cache is sharded per-architecture @@ -175,14 +179,17 @@ jobs: # On the other side, using the host network is easier at configuration. # Besides, each job is running on a different instance, which means using host network here # is safe and has no side effects on others. - [ "${{ inputs.target }}" == "rootful" ] \ + [ "${INPUTS_TARGET}" == "rootful" ] \ && args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \ - || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) + || args=(test-integration-${INPUTS_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh) if [ "${{ inputs.ipv6 }}" == true ]; then - docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }} + docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${INPUTS_BINARY} else - docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${{ inputs.binary }} + docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${INPUTS_BINARY} fi + env: + INPUTS_TARGET: ${{ inputs.target }} + INPUTS_BINARY: ${{ inputs.binary }} # FIXME: this NEEDS to go away - name: "Run: integration tests (flaky)" if: ${{ !fromJSON(inputs.skip-flaky) }} @@ -190,11 +197,14 @@ jobs: . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" - [ "${{ inputs.target }}" == "rootful" ] \ + [ "${INPUTS_TARGET}" == "rootful" ] \ && args=(test-integration ./hack/test-integration.sh) \ - || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) + || args=(test-integration-${INPUTS_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh) if [ "${{ inputs.ipv6 }}" == true ]; then - docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${{ inputs.binary }} + docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${INPUTS_BINARY} else - docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${{ inputs.binary }} + docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${INPUTS_BINARY} fi + env: + INPUTS_TARGET: ${{ inputs.target }} + INPUTS_BINARY: ${{ inputs.binary }} diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 9c21f9ed35c..6de3830ae00 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -86,6 +86,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - if: ${{ inputs.no-hyperv }} name: "Init (no-hyperv): Disable Hyper-V" @@ -96,6 +97,7 @@ jobs: name: "Init (canary): retrieve latest go and containerd" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUTS_RUNNER: ${{ inputs.runner }} run: | latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" latest_containerd="$(. ./hack/provisioning/version/fetch.sh; github::project::latest "containerd/containerd")" @@ -103,13 +105,13 @@ jobs: [ "$latest_go" == "" ] || \ printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" - if [[ "${{ inputs.runner }}" == *windows* ]]; then + if [[ "${INPUTS_RUNNER}" == *windows* ]]; then containerd_version="$WINDOWS_CONTAINERD_VERSION" else containerd_version="$LINUX_CONTAINERD_VERSION" fi [ "${latest_containerd:1}" == "$containerd_version" ] || { - if [[ "${{ inputs.runner }}" == *windows* ]]; then + if [[ "${INPUTS_RUNNER}" == *windows* ]]; then printf "WINDOWS_CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" printf "WINDOWS_CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" else @@ -139,7 +141,7 @@ jobs: sudo mkdir -p /etc/docker sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json echo "::endgroup::" - echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" + echo "::group:: downgrade docker to the specific version we want to test (${INPUTS_DOCKER_VERSION})" sudo apt-get update -qq sudo apt-get install -qq ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings @@ -149,16 +151,16 @@ jobs: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq - sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} + sudo apt-get install -qq --allow-downgrades docker-ce=${INPUTS_DOCKER_VERSION} docker-ce-cli=${INPUTS_DOCKER_VERSION} sudo systemctl restart docker echo "::endgroup::" else # FIXME: this is missing runc (see top level workflow note about the state of this) echo "::group:: install dependencies" sudo ./hack/provisioning/linux/containerd.sh uninstall - ./hack/provisioning/linux/containerd.sh rootful "$LINUX_CONTAINERD_VERSION" "amd64" "$LINUX_CONTAINERD_SHA" "${{ inputs.linux-containerd-service-sha }}" + ./hack/provisioning/linux/containerd.sh rootful "$LINUX_CONTAINERD_VERSION" "amd64" "$LINUX_CONTAINERD_SHA" "${INPUTS_LINUX_CONTAINERD_SERVICE_SHA}" sudo ./hack/provisioning/linux/cni.sh uninstall - ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + ./hack/provisioning/linux/cni.sh install "${INPUTS_LINUX_CNI_VERSION}" "amd64" "${INPUTS_LINUX_CNI_SHA}" echo "::endgroup::" echo "::group:: build nerctl" @@ -188,16 +190,22 @@ jobs: # This ensures that bridged traffic goes through netfilter sudo modprobe br-netfilter + env: + INPUTS_DOCKER_VERSION: ${{ inputs.docker-version }} + INPUTS_LINUX_CONTAINERD_SERVICE_SHA: ${{ inputs.linux-containerd-service-sha }} + INPUTS_LINUX_CNI_VERSION: ${{ inputs.linux-cni-version }} + INPUTS_LINUX_CNI_SHA: ${{ inputs.linux-cni-sha }} - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" env: ctrdVersion: ${{ env.WINDOWS_CONTAINERD_VERSION }} ctrdSha: ${{ env.WINDOWS_CONTAINERD_SHA }} + INPUTS_WINDOWS_CNI_VERSION: ${{ inputs.windows-cni-version }} run: | # Install WinCNI echo "::group:: install wincni" - GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh + GOPATH=$(go env GOPATH) WINCNI_VERSION=${INPUTS_WINDOWS_CNI_VERSION} ./hack/provisioning/windows/cni.sh echo "::endgroup::" # Install containerd @@ -226,7 +234,9 @@ jobs: . ./hack/github/action-helpers.sh github::md::h2 "ipv6" >> "$GITHUB_STEP_SUMMARY" - ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-ipv6 + ./hack/test-integration.sh -test.target=${INPUTS_BINARY} -test.only-ipv6 + env: + INPUTS_BINARY: ${{ inputs.binary }} - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Run: integration tests" @@ -234,7 +244,9 @@ jobs: . ./hack/github/action-helpers.sh github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" - ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=false + ./hack/test-integration.sh -test.target=${INPUTS_BINARY} -test.only-flaky=false + env: + INPUTS_BINARY: ${{ inputs.binary }} # FIXME: this must go - if: ${{ env.SHOULD_RUN == 'yes' }} @@ -243,4 +255,6 @@ jobs: . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" - ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=true + ./hack/test-integration.sh -test.target=${INPUTS_BINARY} -test.only-flaky=true + env: + INPUTS_BINARY: ${{ inputs.binary }} diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 22874665162..c339f738611 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -28,11 +28,13 @@ jobs: runs-on: "${{ inputs.runner }}" env: TARGET: ${{ inputs.target }} + GUEST: ${{ inputs.guest }} steps: - name: "Init: checkout" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - name: "Init: lima" uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 @@ -54,7 +56,7 @@ jobs: --memory=12 \ --containerd=none \ --set '.mounts=null | .portForwards=[{"guestSocket":"/var/run/docker.sock","hostSocket":"{{.Dir}}/sock/docker.sock"}]' \ - template://${{ inputs.guest }} + template://${GUEST} # FIXME: the tests should be directly executed in the VM without nesting Docker inside it # https://github.com/containerd/nerdctl/issues/3858 diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index 53062f27dc3..cfb9c1feef0 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -23,6 +23,7 @@ jobs: 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 diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 5eb30e75a2f..6bea385bdc5 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -49,6 +49,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false # If canary is requested, check for the latest unstable release - if: ${{ inputs.canary }} @@ -73,13 +74,17 @@ jobs: name: "Init: set up CNI and CRIU" run: | if [ "$RUNNER_OS" == "Windows" ]; then - GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh + GOPATH=$(go env GOPATH) WINCNI_VERSION=${INPUTS_WINDOWS_CNI_VERSION} ./hack/provisioning/windows/cni.sh elif [ "$RUNNER_OS" == "Linux" ]; then - ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + ./hack/provisioning/linux/cni.sh install "${INPUTS_LINUX_CNI_VERSION}" "amd64" "${INPUTS_LINUX_CNI_SHA}" sudo apt-get update -qq sudo add-apt-repository ppa:criu/ppa -y sudo apt-get install -qq criu fi + env: + INPUTS_WINDOWS_CNI_VERSION: ${{ inputs.windows-cni-version }} + INPUTS_LINUX_CNI_VERSION: ${{ inputs.linux-cni-version }} + INPUTS_LINUX_CNI_SHA: ${{ inputs.linux-cni-sha }} - if: ${{ env.GO_VERSION != '' }} name: "Run" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c123fd4912..879319812c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,8 @@ jobs: attestations: write # for provenances steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 @@ -32,6 +34,7 @@ jobs: with: go-version: "1.26" check-latest: true + cache: false - name: "Compile binaries" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 5640cb337a7..ab750f61212 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -10,6 +10,9 @@ on: paths-ignore: - '**.md' +permissions: + contents: read + jobs: test-integration-el: name: "EL${{ inputs.hack }}" @@ -49,6 +52,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 + persist-credentials: false - name: "Run" run: | # FIXME: this should be a bit more elegant to use. diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 36d97bd573a..a9386e4defd 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -7,6 +7,9 @@ on: - 'release/**' pull_request: +permissions: + contents: read + jobs: # Runs golangci to ensure that: # 1. the tooling is working on the target platform @@ -76,3 +79,20 @@ jobs: go-version: ${{ matrix.go-version }} runner: ubuntu-24.04 canary: ${{ matrix.canary && true || false }} + + zizmor: + name: "zizmor" + runs-on: ubuntu-24.04 + steps: + - name: "Init: checkout" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + - name: "Run: zizmor" + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + inputs: .github/workflows + # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. + # Keep this as a local CI check. + advanced-security: false diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 09558b6ec3d..e52f661de7c 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -9,6 +9,9 @@ on: paths-ignore: - '**.md' +permissions: + contents: read + jobs: test-unit: # Note: inputs.hack is undefined - its purpose is to prevent GitHub Actions from displaying all matrix variants as part of the name. diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 3878cb12053..d9aec7baaf3 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -8,6 +8,9 @@ on: pull_request: paths: 'mod/tigron/**' +permissions: + contents: read + env: GO_VERSION: "1.26" GOTOOLCHAIN: local @@ -35,6 +38,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 100 + persist-credentials: false - if: ${{ matrix.canary }} name: "Init (canary): retrieve GO_VERSION" env: @@ -50,6 +54,7 @@ jobs: with: go-version: ${{ env.GO_VERSION }} check-latest: true + cache: false - if: ${{ env.GO_VERSION != '' }} name: "Install tools" run: | From 7168e7139b711f76aab842edbcc0ee143c7fdefc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 18:36:34 +0000 Subject: [PATCH 551/868] build(deps): bump github.com/moby/moby/v2 Bumps [github.com/moby/moby/v2](https://github.com/moby/moby) from 2.0.0-beta.13 to 2.0.0-beta.14. - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.13...v2.0.0-beta.14) --- updated-dependencies: - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.14 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 11af046598c..b7cb5a051e4 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( 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.13 + github.com/moby/moby/v2 v2.0.0-beta.14 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 23092c39833..4e9727504f8 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ 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.13 h1:Hwl5dI34UVr/ZSUZuMYuaoun3+JaQgBzwzzAgCduZ44= -github.com/moby/moby/v2 v2.0.0-beta.13/go.mod h1:SmAxBJ3ELHfRpQVTMeT7aD5YfVfwT437Kogh094jG/I= +github.com/moby/moby/v2 v2.0.0-beta.14 h1:oD9sqnonFAWCWgIxOpVYwMWtwYU2lQNGV7cDNRY4Iq0= +github.com/moby/moby/v2 v2.0.0-beta.14/go.mod h1:SmAxBJ3ELHfRpQVTMeT7aD5YfVfwT437Kogh094jG/I= 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= From fd2358210f48d993b47e6cdb226902b5c30f7b2b Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 19 May 2026 13:08:47 +0900 Subject: [PATCH 552/868] fix typo and missing import in tools.md Signed-off-by: Park jungtae --- docs/testing/tools.md | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/testing/tools.md b/docs/testing/tools.md index 1274c51b4a7..d19e768c77f 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -33,7 +33,7 @@ func TestMyThing(t *testing.T) { // Declare your test myTest := nerdtest.Setup() // This is going to run `nerdctl info` (or `docker info`) - mytest.Command = test.Command("info") + myTest.Command = test.Command("info") // Verify the command exits with 0, and stdout contains the word `Kernel` myTest.Expected = test.Expects(0, nil, expect.Contains("Kernel")) // Run it @@ -82,9 +82,10 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/mod/tigron/test" - "github.com/containerd/nerdctl/mod/tigron/require" "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" ) func MyComparator(compare string) test.Comparator { @@ -118,10 +119,12 @@ import ( "github.com/containerd/errdefs" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/mod/tigron/test" - "github.com/containerd/nerdctl/mod/tigron/require" "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/testutil/nerdtest" ) func TestMyThing(t *testing.T) { @@ -229,10 +232,12 @@ import ( "github.com/containerd/errdefs" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/mod/tigron/test" - "github.com/containerd/nerdctl/mod/tigron/require" "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/testutil/nerdtest" ) func TestMyThing(t *testing.T) { @@ -310,10 +315,12 @@ import ( "github.com/containerd/errdefs" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/mod/tigron/test" - "github.com/containerd/nerdctl/mod/tigron/require" "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/testutil/nerdtest" ) func TestMyThing(t *testing.T) { @@ -421,7 +428,7 @@ nerdtest.CNIFirewallVersion("1.7.1") // CNI firewall plugin version check ### About `nerdtest.Private` -While all requirements above are self-descriptive or obvious, `nerdtest.Private` is a +While all requirements above are self-descriptive or obvious, `nerdtest.Private` is a special case. If set, it will run tests inside a dedicated namespace that is private to the test. From 228c5ff9056104254640e3d85772b12c0a38e079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 00:25:54 +0000 Subject: [PATCH 553/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.10.2 to 2.11.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.10.2...v2.11.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.11.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 11af046598c..b9281cbb1e3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.1 - github.com/compose-spec/compose-go/v2 v2.10.2 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.11.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 23092c39833..98b6eb476b0 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= -github.com/compose-spec/compose-go/v2 v2.10.2/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM= +github.com/compose-spec/compose-go/v2 v2.11.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= From a4ad4f42271c37e885dbd03cd2a2f1e07aab10b5 Mon Sep 17 00:00:00 2001 From: Chengyu Zhu Date: Wed, 6 May 2026 21:57:10 +0800 Subject: [PATCH 554/868] Dockerfile: bump containerd to v2.3.1 Signed-off-by: Chengyu Zhu --- .github/workflows/workflow-test.yml | 6 +++--- Dockerfile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 09558b6ec3d..984ca744f05 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -151,15 +151,15 @@ jobs: windows-cni-version: v0.3.1 docker-version: 5:28.0.4-1~ubuntu.24.04~noble # Windows CI still requires containerd v2.2. - # [v2.3.0-beta.2 regression] The virtual machine or container JSON document is invalid. (Hyper-V container) + # [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.0-beta.2 + linux-containerd-version: 2.3.1 # 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: feabdaf784298c5389972a93bf670b80abf7e674cd20a9d629fe133552046d0e + linux-containerd-sha: 628448bd973610c656c1cbea8e88b32fafd85b23cc1aa4a3372eb7198478c054 linux-containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index 4cbddaeaf3b..75538c93b4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.0-beta.2@8a5337317f3216cd920283334f69b2f9003f75b2 +ARG CONTAINERD_VERSION=v2.3.1@64b425cf570b3b8dd1d4cc46da7c1fce65c6651a ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From 2203f242b6f5655f2cff3a07b286c581caa13f65 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 21 May 2026 10:07:12 +0800 Subject: [PATCH 555/868] Dockerfile: bump buildkit version to 0.30.0 Signed-off-by: ChengyuZhu6 --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 diff --git a/Dockerfile b/Dockerfile index 75538c93b4f..f6afede379f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.29.0@BINARY +ARG BUILDKIT_VERSION=v0.30.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 deleted file mode 100644 index 9c29c3409f0..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.29.0 +++ /dev/null @@ -1,2 +0,0 @@ -ab8d93c72253b450f34a43e1c480abc52380f4aec3a8a395aebf09489efef7a0 buildkit-v0.29.0.linux-amd64.tar.gz -99a279e30be2947294eece98d82d1461fcfdc47da59514cb85252bb5ef414801 buildkit-v0.29.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 new file mode 100644 index 00000000000..0beafcc769b --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 @@ -0,0 +1,2 @@ +2da148c50540409988c837e62b72176e4a9ad7855548a2dd6ea1cd0c0d2d360d buildkit-v0.30.0.linux-amd64.tar.gz +d0c9601e49f441dcc5c6493c884275a5470dcc9c188c96573c4dd503f2fde97d buildkit-v0.30.0.linux-arm64.tar.gz From 9e31c406c6855f6f739fb03a9a941abcc5c6d311 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 07:24:35 +0000 Subject: [PATCH 556/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.0 to 2.3.1. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.0...v2.3.1) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index b7cb5a051e4..072311eb79c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.26.2 +go 1.26.3 require ( github.com/Masterminds/semver/v3 v3.5.0 @@ -11,8 +11,8 @@ require ( github.com/containerd/accelerated-container-image v1.4.3 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.0 - github.com/containerd/containerd/v2 v2.3.0 //gomodjail:unconfined + github.com/containerd/containerd/api v1.11.1 + github.com/containerd/containerd/v2 v2.3.1 //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 diff --git a/go.sum b/go.sum index 4e9727504f8..bd4f121da80 100644 --- a/go.sum +++ b/go.sum @@ -32,10 +32,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.11.0 h1:smv4e74S/wwIx0Sj7lhwO1t3M/oi+mSzk2VXqHq8aO0= -github.com/containerd/containerd/api v1.11.0/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.0 h1:qpB5dyToxPqea1OdedyAiAnnor5wxTM+Py9nWt5CnWY= -github.com/containerd/containerd/v2 v2.3.0/go.mod h1:+chyhxLNeqUVOcTJGgaSu/IbDGX6p3+d8AJjAaerAS8= +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/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= From 124dfc0c3679e0a219b6643fa22e85756f5212f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 07:25:12 +0000 Subject: [PATCH 557/868] build(deps): bump the docker group across 1 directory with 2 updates Bumps the docker group with 2 updates in the / directory: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.5.0+incompatible to 29.5.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.5.0...v29.5.2) Updates `github.com/moby/moby/v2` from 2.0.0-beta.14 to 2.0.0-beta.15 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.14...v2.0.0-beta.15) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b7cb5a051e4..d5a22d93ecf 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.5.2+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 @@ -44,7 +44,7 @@ require ( 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.14 + github.com/moby/moby/v2 v2.0.0-beta.15 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4e9727504f8..4b11eb1c1fa 100644 --- a/go.sum +++ b/go.sum @@ -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.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4= -github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +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/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= @@ -212,8 +212,8 @@ 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.14 h1:oD9sqnonFAWCWgIxOpVYwMWtwYU2lQNGV7cDNRY4Iq0= -github.com/moby/moby/v2 v2.0.0-beta.14/go.mod h1:SmAxBJ3ELHfRpQVTMeT7aD5YfVfwT437Kogh094jG/I= +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/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= From 46b313e45a5f1467575615f6efda30666071f48b Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Thu, 21 May 2026 16:30:49 +0800 Subject: [PATCH 558/868] update gomod jail to v0.3.2 Signed-off-by: ChengyuZhu6 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f6afede379f..70250e01afc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug ARG BUILDG_VERSION=v0.5.3@BINARY # Extra deps: gomodjail -ARG GOMODJAIL_VERSION=v0.1.3@cea529ddd971b677c67d8af7e936fbc62b35b98c +ARG GOMODJAIL_VERSION=v0.3.2@c145bb1e36fe0939c5fa0467f2477878dea8e3d9 # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash From 8e4bee0e50fd623f9c8cb7d5f627e3e942fdedb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 22:32:27 +0000 Subject: [PATCH 559/868] build(deps): bump zizmorcore/zizmor-action from 0.5.3 to 0.5.6 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.3 to 0.5.6. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/b1d7e1fb5de872772f31590499237e7cce841e8e...5f14fd08f7cf1cb1609c1e344975f152c7ee938d) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index a9386e4defd..decb53fe2c0 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -90,7 +90,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From c8766fe447c3b4bf5e22f13f0318ef7ca391593e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 22:32:32 +0000 Subject: [PATCH 560/868] build(deps): bump docker/build-push-action from 7.1.0 to 7.2.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 253ba2db905..49fe50328bb 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -63,7 +63,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64 From 18728fe790df01be63a3fc0d8d0dfdb8ce8678c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 22:32:21 +0000 Subject: [PATCH 561/868] build(deps): bump docker/login-action from 4.1.0 to 4.2.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 253ba2db905..1ed8ca5f128 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -46,7 +46,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 0c21d50fb27daae01d7b45b7bc4aa9c9094e4b57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 22:32:25 +0000 Subject: [PATCH 562/868] build(deps): bump docker/metadata-action from 6.0.0 to 6.1.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/030e881283bb7a6894de51c315a6bfe6a94e05cf...80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 253ba2db905..7a2d6301b7d 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -56,7 +56,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From 2dab3f713aafef27754781a3fd3cd9ef5ac4b149 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 22:32:38 +0000 Subject: [PATCH 563/868] build(deps): bump the golang-x group across 1 directory with 3 updates Bumps the golang-x group with 2 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/crypto` from 0.51.0 to 0.52.0 - [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0) Updates `golang.org/x/net` from 0.54.0 to 0.55.0 - [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0) Updates `golang.org/x/sys` from 0.44.0 to 0.45.0 - [Commits](https://github.com/golang/sys/compare/v0.44.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.52.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.55.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.45.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 20c5b71c3cd..f1c16f3ac79 100644 --- a/go.mod +++ b/go.mod @@ -65,10 +65,10 @@ require ( github.com/yuchanns/srslog v1.1.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.51.0 - golang.org/x/net v0.54.0 + 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.44.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 gotest.tools/v3 v3.5.2 diff --git a/go.sum b/go.sum index 17e84692a0d..e011d3816c0 100644 --- a/go.sum +++ b/go.sum @@ -366,8 +366,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +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/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= @@ -399,8 +399,8 @@ 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.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +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/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= @@ -434,8 +434,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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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/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= From 3c039f176ccaaa33690f78cdc738bd5ffa5a729c Mon Sep 17 00:00:00 2001 From: Metbcy Date: Sat, 23 May 2026 00:00:18 +0000 Subject: [PATCH 564/868] chore: remove go.uber.org/mock dependency Replace the generated mocks under pkg/infoutil/infoutilmock and the inline gomock-based MockParse in pkg/cmd/builder/build_test.go with hand-rolled fakes that use function fields. This drops the go.uber.org/mock require from go.mod / go.sum. Fixes #3325 Signed-off-by: Metbcy --- go.mod | 1 - go.sum | 2 - pkg/cmd/builder/build_test.go | 149 ++++++++++----------- pkg/infoutil/infoutil_windows_test.go | 125 +++++++---------- pkg/infoutil/infoutilmock/infoutil_mock.go | 105 +++++---------- 5 files changed, 151 insertions(+), 231 deletions(-) diff --git a/go.mod b/go.mod index 20c5b71c3cd..23e37f00d00 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,6 @@ require ( 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.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.51.0 golang.org/x/net v0.54.0 diff --git a/go.sum b/go.sum index 17e84692a0d..3622145104b 100644 --- a/go.sum +++ b/go.sum @@ -350,8 +350,6 @@ 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= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/pkg/cmd/builder/build_test.go b/pkg/cmd/builder/build_test.go index 246c64efc17..6566fdd46d4 100644 --- a/pkg/cmd/builder/build_test.go +++ b/pkg/cmd/builder/build_test.go @@ -19,104 +19,99 @@ package builder import ( "fmt" "path/filepath" - "reflect" "runtime" "testing" specs "github.com/opencontainers/image-spec/specs-go/v1" - "go.uber.org/mock/gomock" "gotest.tools/v3/assert" ) -type MockParse struct { - ctrl *gomock.Controller - recorder *MockParseRecorder +// fakePlatformParser is a hand-rolled test double for PlatformParser. +// Tests assign the function fields to control behavior. +type fakePlatformParser struct { + ParseFunc func(platform string) (specs.Platform, error) + DefaultSpecFunc func() specs.Platform } -type MockParseRecorder struct { - mock *MockParse -} - -func newMockParser(ctrl *gomock.Controller) *MockParse { - mock := &MockParse{ctrl: ctrl} - mock.recorder = &MockParseRecorder{mock} - return mock -} - -func (m *MockParse) EXPECT() *MockParseRecorder { - return m.recorder -} - -func (m *MockParse) Parse(platform string) (specs.Platform, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Parse") - ret0, _ := ret[0].(specs.Platform) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -func (m *MockParseRecorder) Parse(platform string) *gomock.Call { - m.mock.ctrl.T.Helper() - return m.mock.ctrl.RecordCallWithMethodType(m.mock, "Parse", reflect.TypeOf((*MockParse)(nil).Parse)) -} - -func (m *MockParse) DefaultSpec() specs.Platform { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DefaultSpec") - ret0, _ := ret[0].(specs.Platform) - return ret0 +func (f *fakePlatformParser) Parse(platform string) (specs.Platform, error) { + if f.ParseFunc == nil { + return specs.Platform{}, nil + } + return f.ParseFunc(platform) } -func (m *MockParseRecorder) DefaultSpec() *gomock.Call { - m.mock.ctrl.T.Helper() - return m.mock.ctrl.RecordCallWithMethodType(m.mock, "DefaultSpec", reflect.TypeOf((*MockParse)(nil).DefaultSpec)) +func (f *fakePlatformParser) DefaultSpec() specs.Platform { + if f.DefaultSpecFunc == nil { + return specs.Platform{} + } + return f.DefaultSpecFunc() } func TestIsMatchingRuntimePlatform(t *testing.T) { t.Parallel() testCases := []struct { - name string - mock func(*MockParse) - want bool + name string + parser *fakePlatformParser + want bool }{ { name: "Image is shareable when Runtime and build platform match for os, arch and variant", - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: true, }, { name: "Image is shareable when Runtime and build platform match for os, arch. Variant is not defined", - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: ""}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: ""}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: true, }, { name: "Image is not shareable when Runtime and build platform donot math OS", - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "OS", Architecture: "mockArch", Variant: ""}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "OS", Architecture: "mockArch", Variant: ""}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: false, }, { name: "Image is not shareable when Runtime and build platform donot math Arch", - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "Arch", Variant: ""}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "mockOS", Architecture: "Arch", Variant: ""}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: false, }, { name: "Image is not shareable when Runtime and build platform donot math Variant", - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "Variant"}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "Variant"}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: false, }, @@ -126,11 +121,7 @@ func TestIsMatchingRuntimePlatform(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - - ctrl := gomock.NewController(t) - mockParser := newMockParser(ctrl) - tc.mock(mockParser) - r := isMatchingRuntimePlatform("test", mockParser) + r := isMatchingRuntimePlatform("test", tc.parser) assert.Equal(t, r, tc.want, tc.name) }) } @@ -141,7 +132,7 @@ func TestIsBuildPlatformDefault(t *testing.T) { testCases := []struct { name string - mock func(*MockParse) + parser *fakePlatformParser platform []string want bool }{ @@ -153,18 +144,26 @@ func TestIsBuildPlatformDefault(t *testing.T) { { name: "Image is shareable when Runtime and build platform match for os, arch and variant", platform: []string{"test"}, - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: true, }, { name: "Image is not shareable when Runtime build platform dont match", platform: []string{"test"}, - mock: func(mockParser *MockParse) { - mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "OS", Architecture: "mockArch", Variant: "mockVariant"}, nil) - mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}) + parser: &fakePlatformParser{ + ParseFunc: func(string) (specs.Platform, error) { + return specs.Platform{OS: "OS", Architecture: "mockArch", Variant: "mockVariant"}, nil + }, + DefaultSpecFunc: func() specs.Platform { + return specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"} + }, }, want: false, }, @@ -179,13 +178,11 @@ func TestIsBuildPlatformDefault(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - - ctrl := gomock.NewController(t) - mockParser := newMockParser(ctrl) - if len(tc.platform) == 1 { - tc.mock(mockParser) + parser := tc.parser + if parser == nil { + parser = &fakePlatformParser{} } - r := isBuildPlatformDefault(tc.platform, mockParser) + r := isBuildPlatformDefault(tc.platform, parser) assert.Equal(t, r, tc.want, tc.name) }) } diff --git a/pkg/infoutil/infoutil_windows_test.go b/pkg/infoutil/infoutil_windows_test.go index 173cf1927e4..27e8a9c4c3a 100644 --- a/pkg/infoutil/infoutil_windows_test.go +++ b/pkg/infoutil/infoutil_windows_test.go @@ -19,7 +19,6 @@ package infoutil import ( "testing" - "go.uber.org/mock/gomock" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "gotest.tools/v3/assert" @@ -27,34 +26,24 @@ import ( mocks "github.com/containerd/nerdctl/v2/pkg/infoutil/infoutilmock" ) -func setUpMocks(t *testing.T) *mocks.MockWindowsInfoUtil { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) - - // Mock registry value: CurrentBuildNumber - mockInfoUtil. - EXPECT(). - GetRegistryStringValue(gomock.Any(), gomock.Any(), "CurrentBuildNumber"). - Return("19041", nil). - AnyTimes() - - // Mock registry value: DisplayVersion - mockInfoUtil. - EXPECT(). - GetRegistryStringValue(gomock.Any(), gomock.Any(), "DisplayVersion"). - Return("22H4", nil). - AnyTimes() - - // Mock registry value: UBR - mockInfoUtil. - EXPECT(). - GetRegistryIntValue(gomock.Any(), gomock.Any(), "UBR"). - Return(558, nil). - AnyTimes() - - return mockInfoUtil +func newRegistryFake() *mocks.FakeWindowsInfoUtil { + f := mocks.NewFakeWindowsInfoUtil() + f.GetRegistryStringValueFunc = func(_ registry.Key, _ string, name string) (string, error) { + switch name { + case "CurrentBuildNumber": + return "19041", nil + case "DisplayVersion": + return "22H4", nil + } + return "", nil + } + f.GetRegistryIntValueFunc = func(_ registry.Key, _ string, name string) (int, error) { + if name == "UBR" { + return 558, nil + } + return 0, nil + } + return f } const ( @@ -63,8 +52,6 @@ const ( ) func TestDistroName(t *testing.T) { - mockInfoUtil := setUpMocks(t) - baseVersion := windows.OsVersionInfoEx{ MajorVersion: 10, MinorVersion: 0, @@ -86,13 +73,13 @@ func TestDistroName(t *testing.T) { } for _, tt := range tests { - // Mock sys/windows RtlGetVersion + fake := newRegistryFake() osvi := baseVersion osvi.ProductType = tt.productType - mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1) + fake.RtlGetVersionFunc = func() *windows.OsVersionInfoEx { return &osvi } t.Run(tt.expected, func(t *testing.T) { - actual, err := distroName(mockInfoUtil) + actual, err := distroName(fake) assert.Equal(t, tt.expected, actual, "distroName should return the name of the operating system") assert.NilError(t, err) }) @@ -100,73 +87,54 @@ func TestDistroName(t *testing.T) { } func TestDistroNameError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) - - mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0) - mockInfoUtil. - EXPECT(). - GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()). - Return("19041", registry.ErrNotExist).AnyTimes() + fake := mocks.NewFakeWindowsInfoUtil() + fake.GetRegistryStringValueFunc = func(_ registry.Key, _ string, _ string) (string, error) { + return "19041", registry.ErrNotExist + } - actual, err := distroName(mockInfoUtil) + actual, err := distroName(fake) assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "distroName should return an error on error") assert.Equal(t, "", actual, "distroname should return an empty string on error") } func TestGetKernelVersion(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) - - // Mock registry value: BuildLabEx - mockInfoUtil. - EXPECT(). - GetRegistryStringValue(gomock.Any(), gomock.Any(), "BuildLabEx"). - Return("10240.16412.amd64fre.th1.150729-1800", nil). - Times(1) + fake := mocks.NewFakeWindowsInfoUtil() + fake.GetRegistryStringValueFunc = func(_ registry.Key, _ string, name string) (string, error) { + if name == "BuildLabEx" { + return "10240.16412.amd64fre.th1.150729-1800", nil + } + return "", nil + } baseVersion := windows.OsVersionInfoEx{ MajorVersion: 10, MinorVersion: 0, BuildNumber: 19041, } + fake.RtlGetVersionFunc = func() *windows.OsVersionInfoEx { + v := baseVersion + return &v + } expected := "10.0 19041 (10240.16412.amd64fre.th1.150729-1800)" - // Mock sys/windows RtlGetVersion - osvi := baseVersion - mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1) - - actual, err := getKernelVersion(mockInfoUtil) + actual, err := getKernelVersion(fake) assert.NilError(t, err) assert.Equal(t, expected, actual, "getKernelVersion should return the kernel version") } func TestGetKernelVersionError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl) - - mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0) - mockInfoUtil. - EXPECT(). - GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()). - Return("", registry.ErrNotExist).Times(1) + fake := mocks.NewFakeWindowsInfoUtil() + fake.GetRegistryStringValueFunc = func(_ registry.Key, _ string, _ string) (string, error) { + return "", registry.ErrNotExist + } - actual, err := getKernelVersion(mockInfoUtil) + actual, err := getKernelVersion(fake) assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "getKernelVersion should return an error on error") assert.Equal(t, "", actual, "getKernelVersion should return an empty string on error") } func TestIsWindowsServer(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - tests := []struct { productType string osvi windows.OsVersionInfoEx @@ -189,12 +157,13 @@ func TestIsWindowsServer(t *testing.T) { }, } - mockSysCall := mocks.NewMockWindowsInfoUtil(ctrl) for _, tt := range tests { - mockSysCall.EXPECT().RtlGetVersion().Return(&tt.osvi) + tt := tt + fake := mocks.NewFakeWindowsInfoUtil() + fake.RtlGetVersionFunc = func() *windows.OsVersionInfoEx { return &tt.osvi } t.Run(tt.productType, func(t *testing.T) { - actual := isWindowsServer(mockSysCall) + actual := isWindowsServer(fake) assert.Equal(t, tt.expected, actual, "isWindowsServer should return true on Windows Server") }) } diff --git a/pkg/infoutil/infoutilmock/infoutil_mock.go b/pkg/infoutil/infoutilmock/infoutil_mock.go index 298597ece67..46dec6ac567 100644 --- a/pkg/infoutil/infoutilmock/infoutil_mock.go +++ b/pkg/infoutil/infoutilmock/infoutil_mock.go @@ -16,93 +16,50 @@ limitations under the License. */ +// Package infoutilmock provides a hand-rolled fake for the windowsInfoUtil +// interface used in tests. It replaces the previous gomock-generated mock so +// that nerdctl does not depend on go.uber.org/mock. package infoutilmock import ( - "reflect" - - "go.uber.org/mock/gomock" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) -// MockWindowsInfoUtil is a mock of windowsInfoUtil interface -type MockWindowsInfoUtil struct { - ctrl *gomock.Controller - recorder *MockWindowsInfoUtilMockRecorder -} - -// MockWindowsInfoUtilMockRecorder is the mock recorder for MockWindowsInfoUtil -type MockWindowsInfoUtilMockRecorder struct { - mock *MockWindowsInfoUtil -} - -// NewMockWindowsInfoUtil creates a new mock instance -func NewMockWindowsInfoUtil(ctrl *gomock.Controller) *MockWindowsInfoUtil { - mock := &MockWindowsInfoUtil{ctrl: ctrl} - mock.recorder = &MockWindowsInfoUtilMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockWindowsInfoUtil) EXPECT() *MockWindowsInfoUtilMockRecorder { - return m.recorder -} - -// Create mocks the RtlGetVersion method of windowsInfoUtil -func (m *MockWindowsInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RtlGetVersion") - ret0, _ := ret[0].(*windows.OsVersionInfoEx) - return ret0 -} - -// Expected call of RtlGetVersion -func (m *MockWindowsInfoUtilMockRecorder) RtlGetVersion() *gomock.Call { - m.mock.ctrl.T.Helper() - return m.mock.ctrl.RecordCallWithMethodType( - m.mock, - "RtlGetVersion", - reflect.TypeOf((*MockWindowsInfoUtil)(nil).RtlGetVersion), - ) +// FakeWindowsInfoUtil is a configurable test double for the windowsInfoUtil +// interface. Tests assign the function fields to control behavior per call. +type FakeWindowsInfoUtil struct { + RtlGetVersionFunc func() *windows.OsVersionInfoEx + GetRegistryStringValueFunc func(key registry.Key, path string, name string) (string, error) + GetRegistryIntValueFunc func(key registry.Key, path string, name string) (int, error) } -// Create mocks the GetRegistryStringValue method of windowsInfoUtil -func (m *MockWindowsInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRegistryStringValue", key, path, name) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 +// NewFakeWindowsInfoUtil returns an empty fake. Callers populate the function +// fields they need for a given test. +func NewFakeWindowsInfoUtil() *FakeWindowsInfoUtil { + return &FakeWindowsInfoUtil{} } -// Expected call of GetRegistryStringValue -func (m *MockWindowsInfoUtilMockRecorder) GetRegistryStringValue(key any, path any, name any) *gomock.Call { - m.mock.ctrl.T.Helper() - return m.mock.ctrl.RecordCallWithMethodType( - m.mock, - "GetRegistryStringValue", - reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryStringValue), - key, path, name, - ) +// RtlGetVersion calls the configured function, or returns nil if unset. +func (f *FakeWindowsInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx { + if f.RtlGetVersionFunc == nil { + return nil + } + return f.RtlGetVersionFunc() } -// Create mocks the GetRegistryIntValue method of windowsInfoUtil -func (m *MockWindowsInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRegistryIntValue", key, path, name) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 +// GetRegistryStringValue calls the configured function, or returns ("", nil) if unset. +func (f *FakeWindowsInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) { + if f.GetRegistryStringValueFunc == nil { + return "", nil + } + return f.GetRegistryStringValueFunc(key, path, name) } -// Expected call of GetRegistryIntValue -func (m *MockWindowsInfoUtilMockRecorder) GetRegistryIntValue(key any, path any, name any) *gomock.Call { - m.mock.ctrl.T.Helper() - return m.mock.ctrl.RecordCallWithMethodType( - m.mock, - "GetRegistryIntValue", - reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryIntValue), - key, path, name, - ) +// GetRegistryIntValue calls the configured function, or returns (0, nil) if unset. +func (f *FakeWindowsInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) { + if f.GetRegistryIntValueFunc == nil { + return 0, nil + } + return f.GetRegistryIntValueFunc(key, path, name) } From 04e9823c75fa463f503f02dfaa760b541ab69f3c Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 24 May 2026 11:20:44 +0400 Subject: [PATCH 565/868] fix: handle long fractional Unix timestamps Signed-off-by: immanuwell --- pkg/timestamp/timestamp.go | 8 ++++++-- pkg/timestamp/timestamp_test.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/timestamp/timestamp.go b/pkg/timestamp/timestamp.go index f349cb03092..9938fbfbc24 100644 --- a/pkg/timestamp/timestamp.go +++ b/pkg/timestamp/timestamp.go @@ -25,7 +25,6 @@ package timestamp import ( "fmt" - "math" "strconv" "strings" "time" @@ -144,11 +143,16 @@ func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) { if !ok { return sec, 0, nil } + if len(n) > 9 { + n = n[:9] + } nsec, err := strconv.ParseInt(n, 10, 64) if err != nil { return sec, nsec, err } // should already be in nanoseconds but just in case convert n to nanoseconds - nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n)))) + for range 9 - len(n) { + nsec *= 10 + } return sec, nsec, nil } diff --git a/pkg/timestamp/timestamp_test.go b/pkg/timestamp/timestamp_test.go index 176ba3c08af..a26bcdbf379 100644 --- a/pkg/timestamp/timestamp_test.go +++ b/pkg/timestamp/timestamp_test.go @@ -67,6 +67,7 @@ func TestGetTimestamp(t *testing.T) { // unix timestamps returned as is {"1136073600", "1136073600", false}, {"1136073600.000000001", "1136073600.000000001", false}, + {"1136073600.123456789123456789123", "1136073600.123456789123456789123", false}, // Durations {"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false}, {"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false}, @@ -99,6 +100,7 @@ func TestParseTimestamps(t *testing.T) { {"1136073600.0000000010", 0, 1136073600, 1, false}, {"1136073600.0000000001", 0, 1136073600, 0, false}, {"1136073600.0000000009", 0, 1136073600, 0, false}, + {"1136073600.123456789123456789123", 0, 1136073600, 123456789, false}, {"1136073600.00000001", 0, 1136073600, 10, false}, {"foo.bar", 0, 0, 0, true}, {"1136073600.bar", 0, 1136073600, 0, true}, From e8f7791229ac53278b535ece369ccc13941d643e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 05:24:08 +0000 Subject: [PATCH 566/868] build(deps): bump github.com/opencontainers/selinux Bumps [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) from 1.14.1 to 1.15.0. - [Release notes](https://github.com/opencontainers/selinux/releases) - [Commits](https://github.com/opencontainers/selinux/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: github.com/opencontainers/selinux dependency-version: 1.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f1c16f3ac79..8394c683cb1 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ 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.14.1 + github.com/opencontainers/selinux v1.15.0 github.com/pelletier/go-toml/v2 v2.3.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index e011d3816c0..32ca56e39b1 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ 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.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= -github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +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/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= From 6701200342cd0a57e0527fc8a576eb5cfadffe3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 05:28:13 +0000 Subject: [PATCH 567/868] build(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 88db93ff682..0cbd280c1d9 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -40,7 +40,7 @@ jobs: uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action From da3187ef446eb16ea04f73590a7d657b130c9d96 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Thu, 21 May 2026 09:51:43 +0900 Subject: [PATCH 568/868] feat(ps): show container health status in STATUS column `docker ps` displays the health check results for healthchecked containers in the STATUS column: ```bash $ docker run -d --name health --health-cmd=true --health-interval=3s alpine sleep inf ace14dd125355ac04e8cbad9f1cf2eaaa7209d76cb648ccefb81e45b986c58f7 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ace14dd12535 alpine "sleep inf" 23 seconds ago Up 22 seconds (healthy) health ``` However, `nerdctl ps` doesn't show the health status. Therefore, to ensure compatibility with Docker, this change makes `nerdctl ps` display the health check results in the STATUS column for containers that perform health checks. After this change, the output looks like: ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 02c64243528a docker.io/library/alpine:latest "sleep inf" 7 seconds ago Up (health: starting) hoge ``` ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 690aa502978c docker.io/library/alpine:latest "sleep inf" 1 second ago Up (healthy) hoge ``` ```bash $ sudo nerdctl ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 02c64243528a docker.io/library/alpine:latest "sleep inf" 2 hours ago Up (unhealthy) hoge ``` Signed-off-by: Hayato Kiwata --- .../container/container_list_linux_test.go | 118 ++++++++++++++++++ pkg/cmd/container/list.go | 14 +++ 2 files changed, 132 insertions(+) diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index cee48d7eb9e..38420293575 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -26,10 +26,13 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/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/formatter" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -699,3 +702,118 @@ func TestContainerListStatusFilter(t *testing.T) { testCase.Run(t) } + +func TestContainerListWithHealthStatus(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + // Docker CLI does not provide a standalone healthcheck command. + require.Not(nerdtest.Docker), + require.Not(nerdtest.Rootless), + ) + + testCase.SubTests = []*test.Case{ + { + Description: "ps shows healthy status after a successful probe", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "true", "--health-interval", "3s", + testutil.CommonImage, "sleep", nerdtest.Infinity, + ) + helpers.Ensure("container", "healthcheck", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "name="+data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(fmt.Sprintf("(%s)", healthcheck.Healthy)), + } + }, + }, + { + Description: "ps shows starting status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "exit 1", + "--health-interval", "1s", + "--health-start-period", "60s", + "--health-retries", "2", + testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("container", "healthcheck", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "name="+data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(fmt.Sprintf("(health: %s)", healthcheck.Starting)), + } + }, + }, + { + Description: "ps shows unhealthy status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "not-a-real-cmd", + "--health-interval", "1s", + "--health-retries", "1", + testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("container", "healthcheck", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "name="+data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(fmt.Sprintf("(%s)", healthcheck.Unhealthy)), + } + }, + }, + { + Description: "ps does not show health suffix for stopped containers", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "true", + testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("container", "healthcheck", data.Identifier()) + helpers.Ensure("stop", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--filter", "name="+data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("Exited"), + expect.DoesNotContain( + "(health:", + fmt.Sprintf("(%s)", healthcheck.Healthy), + fmt.Sprintf("(%s)", healthcheck.Unhealthy), + ), + ), + } + }, + }, + } + + testCase.Run(t) +} diff --git a/pkg/cmd/container/list.go b/pkg/cmd/container/list.go index 3a1d28269e9..72de32b34bf 100644 --- a/pkg/cmd/container/list.go +++ b/pkg/cmd/container/list.go @@ -36,6 +36,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerdutil" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/healthcheck" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/portutil" @@ -161,6 +162,19 @@ func prepareContainers(ctx context.Context, client *containerd.Client, container var status string if s, ok := statusPerContainer[c.ID()]; ok { status = s + if strings.HasPrefix(status, "Up") && info.Labels[labels.HealthState] != "" { + healthState, err := healthcheck.HealthStateFromJSON(info.Labels[labels.HealthState]) + if err != nil { + log.G(ctx).WithError(err).Debugf("failed to parse health state for container %s", c.ID()) + } else { + switch healthState.Status { + case healthcheck.Healthy, healthcheck.Unhealthy: + status = fmt.Sprintf("%s (%s)", status, healthState.Status) + case healthcheck.Starting: + status = fmt.Sprintf("%s (health: %s)", status, healthState.Status) + } + } + } } else { return nil, fmt.Errorf("can't get container %s status", c.ID()) } From adfb7d4b216de2f6ee3010b8c626f8e328db3816 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 25 May 2026 23:55:14 +0900 Subject: [PATCH 569/868] fix(healthcheck): cleanup transient units on container exit and start Suppose a container with healthcheck enabled has exited. ```bash > sudo nerdctl ps -a --filter "name=hoge" CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES dd94022f7dd0 docker.io/library/alpine:latest "sleep 1" About a minute ago Exited (0) About a minute ago hoge ``` When we try to run `nerdctl start` on that container, the following error occurs, and the container cannot be started. ```bash > sudo nerdctl start hoge FATA[0000] 1 errors: failed to create healthcheck timer: systemd-run failed: exit status 1 output: Failed to start transient timer unit: Unit dd94022f7dd0f8b60cb49e803c52de3a7a49c2b39276883ed7c606e13ca1a918.timer was already loaded or has a fragment file. ``` The cause of the failure is the presence of the systemd transient timer unit used when executing health checks. When checking the output of `systemctl status`, the status of the transient timer unit is `active`, but an error has occurred in the transient service unit that executes the healthcheck command. In nerdctl, container health check is performed by running the `systemd-run` command to periodically execute the `exec` command on the target container via a transient service unit and a transient timer unit, and executing the command specified with the `--health-cmd` option. However, the current implementation does not account for the case where the container has exited. Therefore, this commit will ensure that transient units are deleted when a container with a health check enabled exits. It will also ensure that the system checks for the presence of transient units when restarting a stopped container with a health check enabled. The specific approach is as follows: - Use the `--collect` option of the `systemd-run` command so that the transient service unit can be garbage-collected even when it is in a failed state. - Delete the transient timer unit when the process exits and the container is in a stopped state. - Before creating a new transient timer unit in CreateTimer, check whether a transient timer unit with the same name already exists and remove it if so. Note that if the `--collect` option is specified when executing the `systemd-run` command, deleting the transient timer unit will cause it to be unloaded by systemd's garbage collection. References: - https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html#-G - https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#CollectMode= Signed-off-by: Hayato Kiwata --- .../container_health_check_linux_test.go | 54 +++++++++ pkg/cmd/container/health_check.go | 36 +++--- pkg/healthcheck/healthcheck_manager_darwin.go | 3 + .../healthcheck_manager_freebsd.go | 3 + pkg/healthcheck/healthcheck_manager_linux.go | 113 ++++++++++++------ .../healthcheck_manager_windows.go | 3 + 6 files changed, 156 insertions(+), 56 deletions(-) diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index 1217045a3ed..ebdf150fb26 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -1155,3 +1155,57 @@ func TestHealthCheck_SystemdIntegration_Advanced(t *testing.T) { } testCase.Run(t) } + +func TestStartHealthcheckedContainerAfterExited(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", "1s", + testutil.CommonImage, "sleep", "1", + ) + nerdtest.EnsureContainerExited(helpers, data.Identifier(), expect.ExitCodeSuccess) + + data.Labels().Set("containerName", data.Identifier()) + } + + testCase.SubTests = []*test.Case{ + { + Description: "transient service unit is garbage-collected from systemd after container exit", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerID := nerdtest.InspectContainer(helpers, data.Labels().Get("containerName")).ID + return helpers.Custom("systemctl", "list-units", "--all", containerID+".service", containerID+".timer") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + containerID := nerdtest.InspectContainer(helpers, data.Labels().Get("containerName")).ID + assert.Assert(t, !strings.Contains(stdout, containerID), + "expected transient service unit to be cleaned up, but got: %s", stdout) + }, + } + }, + }, + { + Description: "exited container with healthcheck can be started again", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } + + testCase.Run(t) +} diff --git a/pkg/cmd/container/health_check.go b/pkg/cmd/container/health_check.go index e2646497c3f..1a96028eb50 100644 --- a/pkg/cmd/container/health_check.go +++ b/pkg/cmd/container/health_check.go @@ -29,10 +29,21 @@ import ( // HealthCheck executes the health check command for a container func HealthCheck(ctx context.Context, client *containerd.Client, container containerd.Container) error { - // verify container status and get task - task, err := isContainerRunning(ctx, container) + task, err := container.Task(ctx, nil) + if err != nil { + return fmt.Errorf("failed to get container task: %w", err) + } + // Check if container is running + status, err := task.Status(ctx) if err != nil { - return err + return fmt.Errorf("failed to get container status: %w", err) + } + s := status.Status + if s != containerd.Running { + if s == containerd.Stopped { + healthcheck.CleanupStaleHealthcheckTimer(ctx, container.ID()) + } + return fmt.Errorf("container is not running (status: %s)", status.Status) } // Check if container has health check configured @@ -67,25 +78,6 @@ func HealthCheck(ctx context.Context, client *containerd.Client, container conta return healthcheck.ExecuteHealthCheck(ctx, task, container, hcConfig) } -func isContainerRunning(ctx context.Context, container containerd.Container) (containerd.Task, error) { - // Get container task to check status - task, err := container.Task(ctx, nil) - if err != nil { - return nil, fmt.Errorf("failed to get container task: %w", err) - } - - // Check if container is running - status, err := task.Status(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get container status: %w", err) - } - if status.Status != containerd.Running { - return nil, fmt.Errorf("container is not running (status: %s)", status.Status) - } - - return task, nil -} - // If configuredValue is zero, use defaultValue instead. func timeoutWithDefault(configuredValue time.Duration, defaultValue time.Duration) time.Duration { if configuredValue == 0 { diff --git a/pkg/healthcheck/healthcheck_manager_darwin.go b/pkg/healthcheck/healthcheck_manager_darwin.go index 289d0c16704..f48e5df6a38 100644 --- a/pkg/healthcheck/healthcheck_manager_darwin.go +++ b/pkg/healthcheck/healthcheck_manager_darwin.go @@ -39,6 +39,9 @@ func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.C return nil } +// CleanupStaleHealthcheckTimer removes any pre-existing transient timer unit for the given container. +func CleanupStaleHealthcheckTimer(ctx context.Context, containerID string) {} + // ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service // using just the container ID. This function is non-blocking and uses timeouts to prevent hanging // on systemd operations. On Darwin, this is a no-op since systemd is not available. diff --git a/pkg/healthcheck/healthcheck_manager_freebsd.go b/pkg/healthcheck/healthcheck_manager_freebsd.go index 289d0c16704..f48e5df6a38 100644 --- a/pkg/healthcheck/healthcheck_manager_freebsd.go +++ b/pkg/healthcheck/healthcheck_manager_freebsd.go @@ -39,6 +39,9 @@ func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.C return nil } +// CleanupStaleHealthcheckTimer removes any pre-existing transient timer unit for the given container. +func CleanupStaleHealthcheckTimer(ctx context.Context, containerID string) {} + // ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service // using just the container ID. This function is non-blocking and uses timeouts to prevent hanging // on systemd operations. On Darwin, this is a no-op since systemd is not available. diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index 92b49bd0cc4..80e71bffb7b 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -63,12 +63,23 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi } // Always use health-interval for timer frequency - cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s") + // + // --collect: + // Even when the healthcheck fails with the error "container is not running" after the container has + // stopped, and the transient service unit enters a failed state, it will still be subject to garbage + // collection due to the --collect option. Without this option, `systemctl reset-failed` would explicitly be needed. + // See: https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html#-G + cmdOpts = append(cmdOpts, "--unit", containerID, "--on-unit-inactive="+hc.Interval.String(), "--timer-property=AccuracySec=1s", "--collect") cmdOpts = append(cmdOpts, nerdctlCmd) cmdOpts = append(cmdOpts, nerdctlArgs...) cmdOpts = append(cmdOpts, "container", "healthcheck", containerID) + // Defensively remove any pre-existing transient timer unit that may have leaked from a previous run + // (e.g. when restarted before the self-cleanup in HealthCheck has a chance to run). Without this, + // the systemd-run below would fail with "Unit .timer was already loaded". + CleanupStaleHealthcheckTimer(ctx, containerID) + log.G(ctx).Debugf("creating healthcheck timer with: systemd-run %s", strings.Join(cmdOpts, " ")) run := exec.Command("systemd-run", cmdOpts...) if out, err := run.CombinedOutput(); err != nil { @@ -78,6 +89,22 @@ func CreateTimer(ctx context.Context, container containerd.Container, cfg *confi return nil } +func createDbusConn(ctx context.Context) (*dbus.Conn, error) { + var conn *dbus.Conn + var err error + + if rootlessutil.IsRootless() { + conn, err = dbus.NewUserConnectionContext(ctx) + } else { + conn, err = dbus.NewSystemConnectionContext(ctx) + } + if err != nil { + return nil, fmt.Errorf("systemd DBUS connect error: %w", err) + } + + return conn, nil +} + // StartTimer starts the healthcheck timer unit. func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { hc := extractHealthcheck(ctx, container) @@ -89,13 +116,7 @@ func StartTimer(ctx context.Context, container containerd.Container, cfg *config } containerID := container.ID() - var conn *dbus.Conn - var err error - if rootlessutil.IsRootless() { - conn, err = dbus.NewUserConnectionContext(ctx) - } else { - conn, err = dbus.NewSystemConnectionContext(ctx) - } + conn, err := createDbusConn(ctx) if err != nil { return fmt.Errorf("systemd DBUS connect error: %w", err) } @@ -122,6 +143,54 @@ func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.C return ForceRemoveTransientHealthCheckFiles(ctx, container.ID()) } +func CleanupStaleHealthcheckTimer(ctx context.Context, containerID string) { + conn, err := createDbusConn(ctx) + if err != nil { + log.G(ctx).Warnf("dbus connect failed during stale cleanup: %v", err) + return + } + defer conn.Close() + + timer := containerID + ".timer" + units, err := conn.ListUnitsByNamesContext(ctx, []string{timer}) + if err != nil { + log.G(ctx).Warnf("list units failed during stale cleanup: %v", err) + return + } + // The status of a systemd unit is described below: + // See: https://github.com/systemd/systemd/blob/v260.1/src/basic/unit-def.c + if len(units) == 0 || units[0].LoadState == "not-found" { + return + } + u := units[0] + + log.G(ctx).Warnf("found stale healthcheck timer %s (load=%s, active=%s, sub=%s), cleaning up", + timer, u.LoadState, u.ActiveState, u.SubState) + timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + stopSystemdUnit(ctx, timeoutCtx, conn, timer) +} + +func stopSystemdUnit(ctx context.Context, timeoutCtx context.Context, conn *dbus.Conn, unit string) { + if err := timeoutCtx.Err(); err != nil { + log.G(ctx).Warnf("context already done before stopping unit %s: %v", unit, err) + return + } + ch := make(chan string, 1) + if _, err := conn.StopUnitContext(timeoutCtx, unit, "ignore-dependencies", ch); err != nil { + log.G(ctx).Warnf("failed to stop unit %s: %v", unit, err) + return + } + select { + case msg := <-ch: + if msg != "done" { + log.G(ctx).Warnf("stop unit %s: unexpected result %s", unit, msg) + } + case <-timeoutCtx.Done(): + log.G(ctx).Warnf("timeout waiting for stop confirmation of unit %s", unit) + } +} + // ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service // using just the container ID. This function is non-blocking and uses timeouts to prevent hanging // on systemd operations. It logs errors as warnings but continues cleanup attempts. @@ -142,13 +211,7 @@ func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID strin go func() { defer close(errChan) - var conn *dbus.Conn - var err error - if rootlessutil.IsRootless() { - conn, err = dbus.NewUserConnectionContext(ctx) - } else { - conn, err = dbus.NewSystemConnectionContext(ctx) - } + conn, err := createDbusConn(ctx) if err != nil { log.G(ctx).Warnf("systemd DBUS connect error during force cleanup: %v", err) errChan <- fmt.Errorf("systemd DBUS connect error: %w", err) @@ -158,25 +221,7 @@ func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID strin // Stop timer with timeout go func() { - select { - case <-timeoutCtx.Done(): - log.G(ctx).Warnf("timeout stopping timer %s during force cleanup", timer) - return - default: - tChan := make(chan string, 1) - if _, err := conn.StopUnitContext(timeoutCtx, timer, "ignore-dependencies", tChan); err == nil { - select { - case msg := <-tChan: - if msg != "done" { - log.G(ctx).Warnf("timer stop message during force cleanup: %s", msg) - } - case <-timeoutCtx.Done(): - log.G(ctx).Warnf("timeout waiting for timer stop confirmation: %s", timer) - } - } else { - log.G(ctx).Warnf("failed to stop timer %s during force cleanup: %v", timer, err) - } - } + stopSystemdUnit(ctx, timeoutCtx, conn, timer) }() // Stop service with timeout diff --git a/pkg/healthcheck/healthcheck_manager_windows.go b/pkg/healthcheck/healthcheck_manager_windows.go index e5fa58a4a08..efd606e7da1 100644 --- a/pkg/healthcheck/healthcheck_manager_windows.go +++ b/pkg/healthcheck/healthcheck_manager_windows.go @@ -39,6 +39,9 @@ func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.C return nil } +// CleanupStaleHealthcheckTimer removes any pre-existing transient timer unit for the given container. +func CleanupStaleHealthcheckTimer(ctx context.Context, containerID string) {} + // ForceRemoveTransientHealthCheckFiles forcefully stops and cleans up the transient timer and service // using just the container ID. This function is non-blocking and uses timeouts to prevent hanging // on systemd operations. On Windows, this is a no-op since systemd is not available. From 08b99f8cc975eee98d9b415979e4829e0307f73e Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 25 May 2026 23:55:47 +0900 Subject: [PATCH 570/868] refactor(healthcheck): simplify ForceRemoveTransientHealthCheckFiles Signed-off-by: Hayato Kiwata --- pkg/healthcheck/healthcheck_manager_linux.go | 33 +------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index 80e71bffb7b..f3a992c2bc4 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -226,38 +226,7 @@ func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID strin // Stop service with timeout go func() { - select { - case <-timeoutCtx.Done(): - log.G(ctx).Warnf("timeout stopping service %s during force cleanup", service) - return - default: - sChan := make(chan string, 1) - if _, err := conn.StopUnitContext(timeoutCtx, service, "ignore-dependencies", sChan); err == nil { - select { - case msg := <-sChan: - if msg != "done" { - log.G(ctx).Warnf("service stop message during force cleanup: %s", msg) - } - case <-timeoutCtx.Done(): - log.G(ctx).Warnf("timeout waiting for service stop confirmation: %s", service) - } - } else { - log.G(ctx).Warnf("failed to stop service %s during force cleanup: %v", service, err) - } - } - }() - - // Reset failed units (best effort, non-blocking) - go func() { - select { - case <-timeoutCtx.Done(): - log.G(ctx).Warnf("timeout resetting failed unit %s during force cleanup", service) - return - default: - if err := conn.ResetFailedUnitContext(timeoutCtx, service); err != nil { - log.G(ctx).Warnf("failed to reset failed unit %s during force cleanup: %v", service, err) - } - } + stopSystemdUnit(ctx, timeoutCtx, conn, service) }() // Wait a short time for operations to complete, but don't block indefinitely From fad90465828101f0ca4efe451d0e2a92836079dd Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Wed, 6 May 2026 17:14:35 +0800 Subject: [PATCH 571/868] fix: update status label should call after task is started Signed-off-by: ningmingxiao --- cmd/nerdctl/container/container_run.go | 13 ++++++++++++ .../container_run_restart_linux_test.go | 17 +++++++++++++++ pkg/cmd/container/create.go | 2 +- pkg/cmd/container/run_restart.go | 8 ++----- pkg/containerutil/containerutil.go | 21 ++++++++++--------- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 5daead7b8f4..38c85a097fa 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" @@ -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_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 795550696f6..c81729cdf0b 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -27,6 +27,8 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/poll" + "github.com/containerd/containerd/v2/core/runtime/restart" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" @@ -183,3 +185,18 @@ func TestAddRestartPolicy(t *testing.T) { inspect = base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 1) } + +func TestRunRestartStatusLabel(t *testing.T) { + base := testutil.NewBase(t) + if !nerdtest.IsDocker() { + testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"always"}) + } + tID := testutil.Identifier(t) + defer base.Cmd("rm", "-f", tID).Run() + base.Cmd("create", "--restart=always", "--name", tID, testutil.CommonImage, "sleep", "infinity").AssertOK() + + inspect := base.InspectContainer(tID) + label := inspect.Config.Labels + statusLabel := label[restart.StatusLabel] + assert.Assert(t, statusLabel == "") +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index bf44e02c22c..20f1dcd6ef5 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -280,7 +280,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 } 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/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 94a8e043069..fc17995dff9 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) From 2550e0ca93609f38ac6e87633eddf59ccec460d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=81=E6=98=8E=E6=99=9310296073?= Date: Thu, 7 May 2026 21:03:08 +0800 Subject: [PATCH 572/868] fix: fix load image failed Signed-off-by: ningmingxiao --- cmd/nerdctl/image/image_save.go | 12 ++- pkg/imgutil/load/load.go | 27 ++----- pkg/transferutil/progress.go | 127 +++++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 24 deletions(-) diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 79c0c9cfd0a..89a1eac1e5a 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -17,12 +17,15 @@ package image import ( + "context" "fmt" "os" "github.com/mattn/go-isatty" "github.com/spf13/cobra" + "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" @@ -91,7 +94,14 @@ func saveAction(cmd *cobra.Command, args []string) error { return err } output = f - defer f.Close() + defer func() { + if err := f.Sync(); err != nil { + f.Close() + log.G(context.Background()).Error(err) + return + } + f.Close() + }() } else if out, ok := output.(*os.File); ok && isatty.IsTerminal(out.Fd()) { return fmt.Errorf("cowardly refusing to save to a terminal. Use the -o flag or redirect") } diff --git a/pkg/imgutil/load/load.go b/pkg/imgutil/load/load.go index c8b4c629a56..0c3114c55ac 100644 --- a/pkg/imgutil/load/load.go +++ b/pkg/imgutil/load/load.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "strings" + "time" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" @@ -83,34 +84,16 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I } storeOpts = append(storeOpts, transferimage.WithUnpack(platUnpack, options.GOptions.Snapshotter)) storeOpts = append(storeOpts, transferimage.WithDigestRef("import", true, true)) + storeOpts = append(storeOpts, transferimage.WithNamedPrefix(fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")), true)) - var loadedImages []images.Image - pf, done := transferutil.ProgressHandler(ctx, options.Stdout) - + pf, done, loadedImages := transferutil.ProgressHandlerLoadImage(ctx, client, beforeSet, options) err = client.Transfer(ctx, tarchive.NewImageImportStream(options.Stdin, ""), transferimage.NewStore("", storeOpts...), - transfer.WithProgress(func(p transfer.Progress) { - if p.Event == "saved" { - if img, err := imageService.Get(ctx, p.Name); err == nil { - if !beforeSet[img.Name] { - loadedImages = append(loadedImages, img) - } - } - } - pf(p) - }), + transfer.WithProgress(pf), ) - done() - - if !options.Quiet { - for _, img := range loadedImages { - fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img.Name) - } - } - - return loadedImages, err + return *loadedImages, err } // FromOCIArchive loads and unpacks the images from the OCI formatted archive at the provided file system path. diff --git a/pkg/transferutil/progress.go b/pkg/transferutil/progress.go index 15baf5d508d..a84aa97aaef 100644 --- a/pkg/transferutil/progress.go +++ b/pkg/transferutil/progress.go @@ -25,8 +25,12 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/transfer" "github.com/containerd/containerd/v2/pkg/progress" + + "github.com/containerd/nerdctl/v2/pkg/api/types" ) // From https://github.com/containerd/containerd/blob/v2.2.0-rc.0/cmd/ctr/commands/image/pull.go#L240-L473 @@ -156,6 +160,127 @@ func ProgressHandler(ctx context.Context, out io.Writer) (transfer.ProgressFunc, return progressFn, done } +func ProgressHandlerLoadImage(ctx context.Context, client *containerd.Client, beforeSet map[string]bool, options types.ImageLoadOptions) (transfer.ProgressFunc, func(), *[]images.Image) { + ctx, cancel := context.WithCancel(ctx) + var ( + fw = progress.NewWriter(options.Stdout) + start = time.Now() + statuses = map[string]*progressNode{} + roots = []*progressNode{} + pc = make(chan transfer.Progress, 5) + status string + closeC = make(chan struct{}) + loadedImages []images.Image + imagesDisplay []string + ) + + result := &loadedImages + progressFn := func(p transfer.Progress) { + select { + case pc <- p: + case <-ctx.Done(): + } + } + + done := func() { + cancel() + <-closeC + if !options.Quiet { + for _, img := range imagesDisplay { + fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img) + } + } + } + + go func() { + defer close(closeC) + for { + select { + case p := <-pc: + if p.Name == "" { + status = p.Event + continue + } + if p.Event == "saved" { + if img, err := client.ImageService().Get(ctx, p.Name); err == nil { + if !beforeSet[img.Name] { + loadedImages = append(loadedImages, img) + } + imagesDisplay = append(imagesDisplay, p.Name) + } + } + if node, ok := statuses[p.Name]; !ok { + node = &progressNode{ + Progress: p, + root: true, + } + if len(p.Parents) == 0 { + roots = append(roots, node) + } else { + var parents []string + for _, parent := range p.Parents { + pStatus, ok := statuses[parent] + if ok { + parents = append(parents, parent) + pStatus.children = append(pStatus.children, node) + node.root = false + } + } + node.Progress.Parents = parents + if node.root { + roots = append(roots, node) + } + } + statuses[p.Name] = node + } else { + if len(node.Progress.Parents) != len(p.Parents) { + var parents []string + var removeRoot bool + for _, parent := range p.Parents { + pStatus, ok := statuses[parent] + if ok { + parents = append(parents, parent) + var found bool + for _, child := range pStatus.children { + if child.Progress.Name == p.Name { + found = true + break + } + } + if !found { + pStatus.children = append(pStatus.children, node) + } + if node.root { + removeRoot = true + } + node.root = false + } + } + p.Parents = parents + // Check if needs to remove from root + if removeRoot { + for i := range roots { + if roots[i] == node { + roots = append(roots[:i], roots[i+1:]...) + break + } + } + } + } + node.Progress = p + } + + displayHierarchy(fw, status, roots, start) + fw.Flush() + + case <-ctx.Done(): + return + } + } + }() + return progressFn, done, result +} + func displayHierarchy(w io.Writer, status string, roots []*progressNode, start time.Time) { total := displayNode(w, "", roots) for _, r := range roots { @@ -207,7 +332,7 @@ func displayNode(w io.Writer, prefix string, nodes []*progressNode) int64 { status.Event, bar) default: - fmt.Fprintf(w, "%-40.40s\t%s\t\n", + fmt.Fprintf(w, "%s\t%s\t\n", name, status.Event) } From 12fd8ef34e4f79f22763fe4ef3e5823589c640fe Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 27 May 2026 19:38:33 +0900 Subject: [PATCH 573/868] Update RootlessKit (3.0.1) https://github.com/rootless-containers/rootlesskit/releases/tag/v3.0.1 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 diff --git a/Dockerfile b/Dockerfile index 70250e01afc..669dc764bbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ 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 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 From ed918b47bd3d1560a8cc3da6691d46a232324eec Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 17 May 2026 21:40:59 +0900 Subject: [PATCH 574/868] fix(healthcheck): release exec process resources after probe Each invocation of a health check runs the user-defined command inside the container via containerd's `task.Exec()` API. We can verify this in the `probeHealthCheck` section of the healthcheck package as follows: ```golang func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck, processSpec *specs.Process) (*HealthcheckResult, error) { ... process, err := task.Exec(ctx, execID, processSpec, cio.NewCreator( cio.WithStreams(nil, outputBuf, outputBuf), )) ... if err := process.Start(ctx); err != nil { log.G(ctx).Debugf("failed to start health check: %v", err) return nil, fmt.Errorf("start error: %w", err) } exitStatusC, err := process.Wait(ctx) if err != nil { return nil, fmt.Errorf("failed to wait for health check: %w", err) } ``` However, the current implementation does not release the resources allocated during the health check once the check is complete. As a result, for example, pipes that should normally be deleted are not removed and continue to accumulate over time. We can verify this behavior using the following command: ```bash $ sudo nerdctl run -d --name=health --health-cmd="curl -f http://localhost" --health-interval=1s --health-timeout=1m0s --health-retries=3 --health-start-period=2s nginx:alpine f2fdd8346a546bc6ef446980efdce1132a436fa63bd64a8ce6756c6197e7ec3f $ TASK_PID=$(sudo nerdctl inspect health --format '{{.State.Pid}}') $ SHIM_PID=$(ps -o ppid= -p $TASK_PID | tr -d ' ') $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | head -3 lr-x------ 1 root root 64 May 17 20:51 100 -> pipe:[1093131] lr-x------ 1 root root 64 May 17 20:52 101 -> pipe:[1092248] lr-x------ 1 root root 64 May 17 20:52 102 -> pipe:[1092228] $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | wc -l 16 $ sleep 10 $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | wc -l 26 ``` So, this change adds a deferred `process.Delete()` after `task.Exec()` so that the allocated resources are released when the exec process completes. Signed-off-by: Hayato Kiwata --- .../container_health_check_linux_test.go | 89 +++++++++++++++++++ pkg/healthcheck/executor.go | 5 ++ 2 files changed, 94 insertions(+) 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/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) From f5bf3cc2f225a0f458eb4903693c76f36b1370c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:11 +0000 Subject: [PATCH 575/868] build(deps): bump docker/setup-qemu-action from 4.0.0 to 4.1.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 0cbd280c1d9..75385f1ad74 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -37,7 +37,7 @@ jobs: # 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/release.yml b/.github/workflows/release.yml index 879319812c2..22a837a8688 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: 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: From 819ba7a66afd9ee5f2d9c4907480c83ede4c5e1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:22 +0000 Subject: [PATCH 576/868] build(deps): bump github.com/rootless-containers/rootlesskit/v3 Bumps [github.com/rootless-containers/rootlesskit/v3](https://github.com/rootless-containers/rootlesskit) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/rootless-containers/rootlesskit/releases) - [Commits](https://github.com/rootless-containers/rootlesskit/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: github.com/rootless-containers/rootlesskit/v3 dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d105cbc64f..60cf26d6615 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/opencontainers/selinux v1.15.0 github.com/pelletier/go-toml/v2 v2.3.1 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 diff --git a/go.sum b/go.sum index 19d12545873..ae3be6309de 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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= From 3a2bfc0ced698465f5f19c2854bbc5e4fef61f79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:40 +0000 Subject: [PATCH 577/868] build(deps): bump github.com/containerd/typeurl/v2 from 2.2.3 to 2.3.0 Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.2.3 to 2.3.0. - [Release notes](https://github.com/containerd/typeurl/releases) - [Commits](https://github.com/containerd/typeurl/compare/v2.2.3...v2.3.0) --- updated-dependencies: - dependency-name: github.com/containerd/typeurl/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 21 ++------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 1d105cbc64f..4f8e580280a 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ 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 @@ -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 diff --git a/go.sum b/go.sum index 19d12545873..26b05b2b0c7 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -327,8 +323,6 @@ 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= @@ -357,7 +351,6 @@ 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= @@ -372,8 +365,6 @@ golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK 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= @@ -387,8 +378,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r 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= @@ -403,8 +392,6 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG 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= @@ -462,8 +449,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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= From 80cf9f88ef3e1596530219f2e5f0bcf067f6b792 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Sat, 30 May 2026 20:32:07 +0200 Subject: [PATCH 578/868] test: migrate stable compose run tests to nerdtest framework Migrates the non-flaky compose run tests to nerdtest.Setup(): - TestComposeRunWithEnv - TestComposeRunWithUser - TestComposeRunWithArgs - TestComposeRunWithEntrypoint - TestComposeRunWithLabel (uses nerdtest.InspectContainer) - TestComposeRunWithVolume (uses nerdtest.InspectContainer) Replaces the unbuffer(1) pty wrapper with cmd.WithPseudoTTY(), and uses expect.ExitCodeSuccess instead of literal 0 for exit codes. TestComposeRunWithServicePorts, TestComposeRunWithPublish, and TestComposePushAndPullWithCosignVerify are intentionally left on the legacy framework for now (flaky / complex). Signed-off-by: sathiraumesh --- cmd/nerdctl/compose/compose_run_linux_test.go | 326 ++++++++++++------ 1 file changed, 217 insertions(+), 109 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index c68d9fa258d..71efbb9d070 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,41 @@ 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) - dockerComposeYAML := fmt.Sprintf(` services: alpine: @@ -308,31 +338,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) + } - container := base.InspectContainer(containerName) - if container.Config == nil { - log.L.Errorf("test failed, cannot fetch container config") - t.Fail() + 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 } - assert.Equal(t, container.Config.Labels["foo"], "rab") - assert.Equal(t, container.Config.Labels["x"], "y") + + 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") + }, + } + } + + 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 TestComposeRunWithArgs(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + const partialOutput = "hello world" dockerComposeYAML := fmt.Sprintf(` services: @@ -342,26 +395,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 +439,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 +483,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) { From 8631217fffb92fc82de17a70d6c05c51476d1589 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 19 May 2026 12:02:17 +0900 Subject: [PATCH 579/868] test: refactor container_run_restart_linux_test.go to use Tigron Signed-off-by: Park jungtae --- .../container_run_restart_linux_test.go | 377 +++++++++++++----- pkg/testutil/nerdtest/requirements.go | 39 ++ 2 files changed, 316 insertions(+), 100 deletions(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index c81729cdf0b..5a3431b0d47 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -19,44 +19,118 @@ 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/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() + testCase := nerdtest.Setup() + testCase.NoParallel = true - base.Cmd("run", "-d", - "--restart=always", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).AssertOK() - - 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 @@ -71,132 +145,235 @@ 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) + } + } + + 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() + }, } - time.Sleep(sleep) } - base.DumpDaemonLogs(10) - t.Fatalf("the container does not seem to be restarted") + + 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) + 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", "--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.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() { - 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{"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), } - 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) } func TestRunRestartStatusLabel(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"always"}) + 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] == "") + }), + } } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("create", "--restart=always", "--name", tID, testutil.CommonImage, "sleep", "infinity").AssertOK() - inspect := base.InspectContainer(tID) - label := inspect.Config.Labels - statusLabel := label[restart.StatusLabel] - assert.Assert(t, statusLabel == "") + testCase.Run(t) } 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{ From 267a9ce305aae8454e7bf1e0119cf50a70aa0e09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:46:49 +0000 Subject: [PATCH 580/868] build(deps): bump actions/checkout from 6.0.2 to 6.0.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 75385f1ad74..3e29cc3c49c 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 7969ef16c7c..bb04ce034f0 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..fc61d0725a8 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..7ebab308780 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..6bc1fac639d 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..6e8f823ae48 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..72eb0aeaa72 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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..e768186e1a9 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index c339f738611..d8c0911dfd1 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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 index cfb9c1feef0..1d0bca0d5e4 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 6bea385bdc5..6d81764564b 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22a837a8688..37d39d42620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index ab750f61212..b76fdfa41f6 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -49,7 +49,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index decb53fe2c0..98932baf37b 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index d9aec7baaf3..f72aa8771a8 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -35,7 +35,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 100 persist-credentials: false From b19c23282110cc649c7a9603b9bf37191716e577 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:46:59 +0000 Subject: [PATCH 581/868] build(deps): bump github.com/opencontainers/selinux Bumps [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) from 1.15.0 to 1.15.1. - [Release notes](https://github.com/opencontainers/selinux/releases) - [Commits](https://github.com/opencontainers/selinux/compare/v1.15.0...v1.15.1) --- updated-dependencies: - dependency-name: github.com/opencontainers/selinux dependency-version: 1.15.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2e8e62fe107..24a524d64e3 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ 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/opencontainers/selinux v1.15.1 github.com/pelletier/go-toml/v2 v2.3.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4f8be8673fd..952cec9a50d 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ 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/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.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= From e7f7aac2bcde2d0be43d5f33d0060d9880239ab9 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Wed, 3 Jun 2026 09:33:36 -0400 Subject: [PATCH 582/868] Update Windows installation section in README.md Signed-off-by: Ofek Lev --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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: From 737508fa644077d761c272480be9df34c67112b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 03:23:55 +0000 Subject: [PATCH 583/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.5.2+incompatible to 29.5.3+incompatible - [Commits](https://github.com/docker/cli/compare/v29.5.2...v29.5.3) Updates `github.com/moby/moby/v2` from 2.0.0-beta.15 to 2.0.0-beta.16 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.15...v2.0.0-beta.16) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.5.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2e8e62fe107..af2f82edac5 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.5.3+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 @@ -44,7 +44,7 @@ require ( 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/moby/v2 v2.0.0-beta.16 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4f8be8673fd..6c0f82ad878 100644 --- a/go.sum +++ b/go.sum @@ -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.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= +github.com/docker/cli v29.5.3+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= @@ -208,8 +208,8 @@ 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/v2 v2.0.0-beta.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+Qz4VA= +github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= 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= From 7194fec73541a49873ebbc884391c17729363961 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Wed, 27 May 2026 13:24:26 +0400 Subject: [PATCH 584/868] fix: make formatter ellipsis unicode safe Signed-off-by: immanuwell --- pkg/formatter/formatter.go | 7 ++--- pkg/formatter/formatter_test.go | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) 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..98ab70d7128 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -19,6 +19,7 @@ package formatter import ( "testing" "time" + "unicode/utf8" "gotest.tools/v3/assert" @@ -192,3 +193,49 @@ 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.Assert(t, utf8.ValidString(result), "expected valid UTF-8, got %q", result) + assert.Equal(t, tt.expected, result) + }) + } +} From 0ec7ea7a0ad807425bd69a1112feac0c06c758b2 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 7 Jun 2026 19:14:49 +0400 Subject: [PATCH 585/868] test(formatter): remove redundant utf8 validity assertion Signed-off-by: immanuwell --- pkg/formatter/formatter_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 98ab70d7128..dda59e80fe0 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -19,7 +19,6 @@ package formatter import ( "testing" "time" - "unicode/utf8" "gotest.tools/v3/assert" @@ -234,7 +233,6 @@ func TestEllipsis(t *testing.T) { t.Parallel() result := Ellipsis(tt.input, tt.maxDisplayWidth) - assert.Assert(t, utf8.ValidString(result), "expected valid UTF-8, got %q", result) assert.Equal(t, tt.expected, result) }) } From 4d5bb2c639e4bd46d365b580b8d95fc0160061c1 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 7 Jun 2026 23:25:59 +0900 Subject: [PATCH 586/868] fix: reject `/` as the `-v` destination in nerdctl run for Docker compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the current implementation, when specifying `/` as the destination of the `-v` option in the nerdctl run command, the following error occurs but the container is created. ```bash > sudo nerdctl run -d --name nginx -v ./:/ nginx FATA[0000] failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: open /proc/thread-self/attr/exec: no such file or directory > sudo nerdctl ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0f880e0e68f8 docker.io/library/nginx:latest "/docker-entrypoint.…" 5 seconds ago Created nginx ``` However, in the same situation, Docker fails to create the container. ```bash $ docker run -d --name stop -v ./:/ nginx docker: Error response from daemon: invalid volume specification: '/Users/haytok/workspace:/': invalid mount config for type "bind": invalid specification: destination can't be '/' $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ``` Therefore, this commit fixes the behavior so that the container creation fails when `/` is specified as the destination of the `-v` option for compatibility with Docker. Signed-off-by: Hayato Kiwata --- .../container_run_mount_linux_test.go | 28 +++++++++++++++ .../container_run_mount_windows_test.go | 34 +++++++++++++++++++ pkg/mountutil/mountutil_linux_test.go | 4 +++ pkg/mountutil/mountutil_unix.go | 20 +++++++++++ pkg/mountutil/mountutil_windows.go | 11 ++++++ pkg/mountutil/mountutil_windows_test.go | 4 +++ 6 files changed, 101 insertions(+) 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/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 { From 23a8437799f18c7d7a1cdf4474ede7c8cb0ba2bd Mon Sep 17 00:00:00 2001 From: Skywalkr-dev Date: Fri, 29 May 2026 10:23:47 +0000 Subject: [PATCH 587/868] fix(container): chunk mounts metadata to prevent max label size crash When a container has a large number of volume mounts, storing the marshaled JSON metadata inside a containerd label (`nerdctl/mounts`) exceeds the 4096-byte protocol buffer limit, causing container creation to fail. The current patch addresses this by storing all mounts individually as separate key value pairs to avoid any buffer limit and is returned as a JSON string while fetching mount metadata Unit tests have been added accordingly Signed-off-by: Naveen --- .../container/container_inspect_linux_test.go | 2 +- pkg/cmd/container/create.go | 6 +- pkg/cmd/container/run_mount.go | 6 +- pkg/cmd/volume/rm.go | 5 +- pkg/containerutil/containerutil.go | 11 ++- pkg/containerutil/containerutil_test.go | 39 +++++++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 2 +- .../dockercompat/dockercompat_test.go | 4 +- pkg/labels/labels.go | 3 + pkg/labels/mount.go | 70 +++++++++++++++++++ 10 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 pkg/labels/mount.go diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 8f7d956a61c..99c6fcebc17 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) }) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 20f1dcd6ef5..264c4ecede2 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -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/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/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index fc17995dff9..fa27533f080 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -584,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/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 "" +} From 0929f623311b6096d980e0bf513c3edcdfd043dd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 8 Jun 2026 12:45:26 +0900 Subject: [PATCH 588/868] CI: replace Vagrant with Lima for FreeBSD tests Signed-off-by: Akihiro Suda --- .dockerignore | 3 - .../workflows/job-test-in-lima-freebsd.yml | 65 +++++++++++++++++ .github/workflows/job-test-in-vagrant.yml | 61 ---------------- .github/workflows/workflow-flaky.yml | 2 +- .gitignore | 4 -- Vagrantfile.freebsd | 70 ------------------- hack/provisioning/gpg/hashicorp | 64 ----------------- 7 files changed, 66 insertions(+), 203 deletions(-) create mode 100644 .github/workflows/job-test-in-lima-freebsd.yml delete mode 100644 .github/workflows/job-test-in-vagrant.yml delete mode 100644 Vagrantfile.freebsd delete mode 100644 hack/provisioning/gpg/hashicorp 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/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml new file mode 100644 index 00000000000..ef22c852a99 --- /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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml deleted file mode 100644 index 1d0bca0d5e4..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index b76fdfa41f6..70a8b67aefb 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 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/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/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 From b8cc2ac763a7cee0c9901487caa6b307032c7f1f Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 15:07:10 +0100 Subject: [PATCH 589/868] refactor: migrate container_create_linux_test.go to nerdtest.Setup Migrate TestCreateWithLabel, TestCreateWithMACAddress, TestCreateWithTty, and TestCreateFromOCIArchive from testutil.NewBase to the Tigron-based nerdtest.Setup pattern. Also fix TestUsernsMappingCreateCmd which called nerdtest.Setup() without using its return value; now uses the returned *test.Case directly. Helper function removeUsernsConfig updated to accept tig.T instead of *testing.T to align with the rest of the Tigron-based test infrastructure. Part of #4613. Signed-off-by: Ogulcan Aydogan --- .../container/container_create_linux_test.go | 592 +++++++++++------- 1 file changed, 365 insertions(+), 227 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 1551cdf3555..6e8290902b4 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -36,148 +36,277 @@ 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: 0, + 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: 0, + 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: 0, + Output: func(stdout string, t tig.T) { + helpers.Ensure("start", "-a", data.Identifier()) + }, + } + }, + }, + { + 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: 0, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("container")}, + }) + }, + } } - 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())) + return &test.Expected{ + ExitCode: 1, + 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: 0, + 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: 0, + 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: 0, + 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: "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: 0, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("not support")}, + }) + }, + } + } + return &test.Expected{ + ExitCode: 1, + 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: 1, + 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") + }, + 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: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "speed 38400 baud; line = 0;"), + fmt.Sprintf("expected stdout to contain speed info: %q", stdout)) + }, + } + }, + }, + } + testCase.Run(t) } // TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993 @@ -301,110 +430,121 @@ 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) - - 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") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) + err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0644) + assert.NilError(helpers.T(), err) + + imageName := data.Identifier("image") + ":latest" + tarPath := data.Temp().Path("image.tar") + data.Labels().Set("imageName", imageName) + data.Labels().Set("tarPath", tarPath) + + 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")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } + 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: 0, + 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: 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")) + }, + } + }, }, - 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: 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, + } }, }, } @@ -473,34 +613,32 @@ func addUser(username string, hostID string, helpers test.Helpers) { ExitCode: 0}) } -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 } - } } From 673393687bdd998c5481dc48dca19ec7ad8b2f91 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 23:42:59 +0100 Subject: [PATCH 590/868] fix: use logs instead of start -a for tty container in TestCreateWithTty With -t, the container's output goes through a pseudoTTY. Attaching via "start -a" does not reliably forward PTY output through Tigron's subprocess pipe, causing the stty check to fail on certain containerd versions (e.g. v1.7.30). Match the original test's approach: start the container without -a, then read its output via "nerdctl logs" which goes through the log driver and is always available after the container exits. Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 6e8290902b4..06a712053fc 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -288,20 +288,20 @@ func TestCreateWithTty(t *testing.T) { 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("start", "-a", data.Identifier()) + return helpers.Command("logs", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, t tig.T) { - assert.Assert(t, strings.Contains(stdout, "speed 38400 baud; line = 0;"), - fmt.Sprintf("expected stdout to contain speed info: %q", stdout)) - }, + Output: expect.Contains("speed 38400 baud; line = 0;"), } }, }, From 3806454768f2bef393b5166f49ab6450a5d43799 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 11:45:06 +0100 Subject: [PATCH 591/868] refactor: use tigron expect constants for exit codes Replace hardcoded exit code literals (0, 1) in Tigron test.Expected structs with the named constants from the expect package: ExitCodeSuccess, ExitCodeGenericFail. Existing ExitCodeNoCheck usages were already correct. Signed-off-by: Ogulcan Aydogan --- .../container/container_create_linux_test.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 06a712053fc..72431064fcb 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -51,7 +51,7 @@ func TestCreateWithLabel(t *testing.T) { } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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) @@ -126,7 +126,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("host"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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)), @@ -144,7 +144,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("none"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Ensure("start", "-a", data.Identifier()) }, @@ -162,18 +162,18 @@ func TestCreateWithMACAddress(t *testing.T) { if nerdtest.IsDocker() { // Docker delays the failure to start time return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Command("start", "-i", "-a", data.Identifier()). Run(&test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("container")}, }) }, } } return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("container")}, } }, @@ -187,7 +187,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("bridge"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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)), @@ -205,7 +205,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeDynamicCreateCommand(networkBridgeKey), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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)), @@ -223,7 +223,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeDynamicCreateCommand(networkMACvlanKey), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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)), @@ -243,18 +243,18 @@ func TestCreateWithMACAddress(t *testing.T) { if nerdtest.IsDocker() { // Docker delays the failure to start time return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Command("start", "-i", "-a", data.Identifier()). Run(&test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("not support")}, }) }, } } return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("not support")}, } }, @@ -279,7 +279,7 @@ func TestCreateWithTty(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("stty: standard input: Not a tty")}, } }, @@ -363,7 +363,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)) @@ -410,7 +410,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)) @@ -464,7 +464,7 @@ func TestCreateFromOCIArchive(t *testing.T) { } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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)) @@ -504,7 +504,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + 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") @@ -528,7 +528,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, } }, }, @@ -543,7 +543,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, } }, }, @@ -608,9 +608,9 @@ 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 tig.T, userns string, helpers test.Helpers) { From a271d8b6ee640ba40e3ad907adac757d133536b5 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Sat, 16 May 2026 23:09:29 +0100 Subject: [PATCH 592/868] refactor: use explicit ExitCode constant in test.Expected struct Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 72431064fcb..e0921d1937e 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -301,7 +301,8 @@ func TestCreateWithTty(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains("speed 38400 baud; line = 0;"), + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("speed 38400 baud; line = 0;"), } }, }, From 819768e87b6ab219dcb6144101ef85007d17be2d Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Mon, 8 Jun 2026 17:21:36 +0100 Subject: [PATCH 593/868] test: address review feedback on container_create migration Capture the start -a output and assert the passed MAC is absent for the none-network case, instead of only checking the exit code. Use data.Temp().Save for the Dockerfile instead of os.WriteFile, and drop the manual builder prune --all --force which the Build requirement deliberately omits to keep build tests parallelizable. Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index e0921d1937e..81aa8be40c4 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -146,7 +146,9 @@ func TestCreateWithMACAddress(t *testing.T) { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { - helpers.Ensure("start", "-a", data.Identifier()) + 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)) }, } }, @@ -441,8 +443,7 @@ func TestCreateFromOCIArchive(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) - err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0644) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") imageName := data.Identifier("image") + ":latest" tarPath := data.Temp().Path("image.tar") @@ -458,7 +459,6 @@ func TestCreateFromOCIArchive(t *testing.T) { testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) - helpers.Anyhow("builder", "prune", "--all", "--force") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", "--attach", data.Identifier()) From b2432a46783bfd6244115a961852a90b12308325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:32:40 +0000 Subject: [PATCH 594/868] build(deps): bump the golang-x group with 5 updates Bumps the golang-x group with 5 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.20.0` | `0.21.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.45.0` | `0.46.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.43.0` | `0.44.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.37.0` | `0.38.0` | Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0 - [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.53.0) Updates `golang.org/x/sync` from 0.20.0 to 0.21.0 - [Commits](https://github.com/golang/sync/compare/v0.20.0...v0.21.0) Updates `golang.org/x/sys` from 0.45.0 to 0.46.0 - [Commits](https://github.com/golang/sys/compare/v0.45.0...v0.46.0) Updates `golang.org/x/term` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0) Updates `golang.org/x/text` from 0.37.0 to 0.38.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 9fe72491c4b..cfcf4697c39 100644 --- a/go.mod +++ b/go.mod @@ -64,12 +64,12 @@ require ( 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/crypto v0.53.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/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 ) diff --git a/go.sum b/go.sum index e7b238f02d3..f9071828469 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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= @@ -398,8 +398,8 @@ 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= @@ -419,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= @@ -430,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= @@ -441,8 +441,8 @@ 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= From cb8cf0042057b02e1bead6cda4314fafb26a4a80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:32:42 +0000 Subject: [PATCH 595/868] build(deps): bump golang.org/x/net in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.55.0 to 0.56.0 - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cfcf4697c39..8af11a60589 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.53.0 - golang.org/x/net v0.55.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 diff --git a/go.sum b/go.sum index f9071828469..1a0e9ab0970 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,8 @@ 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= From 36844386ad4c8b8128143c719aeb1434b5baa990 Mon Sep 17 00:00:00 2001 From: fourierrr Date: Thu, 11 Jun 2026 21:25:38 +0800 Subject: [PATCH 596/868] image: add overlaybd vsize option Signed-off-by: fourierrr --- cmd/nerdctl/image/image_convert.go | 6 ++ cmd/nerdctl/image/image_convert_test.go | 86 +++++++++++++++++++++++++ docs/command-reference.md | 4 ++ docs/overlaybd.md | 2 + pkg/api/types/image_types.go | 2 + pkg/cmd/image/convert.go | 1 + 6 files changed, 101 insertions(+) create mode 100644 cmd/nerdctl/image/image_convert_test.go 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/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/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/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 } From 83b54f0c8681143e0361839590beef6504fd4852 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 11 Jun 2026 20:21:08 +0100 Subject: [PATCH 597/868] refactor: migrate multi_platform_linux_test.go to nerdtest.Setup Replace the legacy testutil.NewBase pattern with the nerdtest.Setup / Tigron test.Case framework throughout multi_platform_linux_test.go. - Migrate all five test cases to test.Case{Setup, Cleanup, Command, Expected} - Replace SubTests with sequential assertMultiPlatformRun calls to fix concurrent platform invocations under the docker driver - Extract randomPort constant and requireMultiPlatformExec requirement - Use Tigron expect constants for exit codes - Add comments explaining why require.Not(nerdtest.Docker) is needed (non-buildx docker build lacks multi-platform; docker push lacks --platform) Signed-off-by: Ogulcan Aydogan --- .../container/multi_platform_linux_test.go | 315 ++++++++++++------ 1 file changed, 219 insertions(+), 96 deletions(-) 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) } From 5a04185fd9c9f5de6e2b6c7141ce43ff0515f9d5 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Fri, 12 Jun 2026 17:57:42 +0400 Subject: [PATCH 598/868] fix(network): allow inspecting pseudo networks Signed-off-by: immanuwell --- cmd/nerdctl/network/network_inspect_test.go | 2 - pkg/netutil/cni_plugin.go | 8 ++++ pkg/netutil/netutil.go | 41 +++++++++++++++++++-- pkg/netutil/netutil_test.go | 30 +++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) 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/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") +} From 92cfa9136f0273acc9b0b28c1b8e832216317042 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:07:56 +0900 Subject: [PATCH 599/868] update runc (1.4.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 669dc764bbb..9db6cd32e46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # 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 RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From b2cb37ffac3e1a5c73841537fd67fac86196b6cc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:05 +0900 Subject: [PATCH 600/868] update fuse-overlayfs (1.17) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 | 6 ------ Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 create mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 diff --git a/Dockerfile b/Dockerfile index 9db6cd32e46..4927a99fd61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ 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 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 From fc283bc5b092c4e9fff40d541535a16d307b939b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:15 +0900 Subject: [PATCH 601/868] update nydus (2.4.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4927a99fd61..5e79dd12b2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ 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 NYDUS_VERSION=v2.4.3 ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 ARG KUBO_VERSION=v0.41.0 From fd2a303034261dc52712a20a82a00d509fa76aa1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:26 +0900 Subject: [PATCH 602/868] update soci-snapshotter (0.14.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5e79dd12b2a..3b35150cec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.4.3 -ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 +ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 ARG KUBO_VERSION=v0.41.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 9ec133365c2c46d1b4408ff17a19816fcacfb25e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:47 +0900 Subject: [PATCH 603/868] update kubo (0.42.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3b35150cec8..6666a4e1cdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.4.3 ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 -ARG KUBO_VERSION=v0.41.0 +ARG KUBO_VERSION=v0.42.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 0925492a3389f29170ee54eb0caf031687e95092 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 14 Jun 2026 22:07:10 +0900 Subject: [PATCH 604/868] fix: show Pid as `0` for stopped containers in `nerdctl container inspect` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Docker, running `docker container inspect` on a stopped container shows the Pid as `0`. ```bash > docker ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c8dcc84e427f nginx "/docker-entrypoint.…" 14 seconds ago Exited (0) 8 seconds ago nginx > docker container inspect nginx | grep 'Pid"' "Pid": 0, ``` However, running `nerdctl container inspect` on a stopped container still shows a stale Pid from the already-exited process. ```bash > sudo nerdctl ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4ce4cb9b1ed1 docker.io/library/nginx:latest "/docker-entrypoint.…" 16 seconds ago Exited (0) 4 seconds ago nginx > sn container inspect nginx | grep 'Pid"' "Pid": 1531853, ``` The docker daemon subscribes to containerd's events and when it detects a task exit event, it deletes the task and updates the state of the container it manages, setting Pid to `0`. Therefore, in Docker the Pid becomes `0` after the container stops. - https://github.com/moby/moby/blob/docker-v29.5.3/daemon/monitor.go#L34 On the other hand, nerdctl has no long-running daemon like the docker daemon, so it does not subscribe to containerd's events. As a result, the task is not deleted even after the container stops, and `nerdctl container inspect` still shows the stale Pid of the already-exited process returned by `task.Pid()`. Therefore, this commit changes the behavior so that `nerdctl container inspect` on a stopped container shows the Pid as `0`, for compatibility with Docker. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/container/container_inspect_linux_test.go | 2 ++ pkg/containerinspector/containerinspector.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 99c6fcebc17..6be9a2fd510 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -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/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go index a3a77e1e60d..22ea8b2b6ba 100644 --- a/pkg/containerinspector/containerinspector.go +++ b/pkg/containerinspector/containerinspector.go @@ -57,6 +57,9 @@ 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 { From d15940d148f0b178150572651a3679c025c0945b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:29:43 +0000 Subject: [PATCH 605/868] build(deps): bump github.com/moby/sys/mount Bumps the moby-sys group with 1 update in the / directory: [github.com/moby/sys/mount](https://github.com/moby/sys). Updates `github.com/moby/sys/mount` from 0.3.4 to 0.3.5 - [Release notes](https://github.com/moby/sys/releases) - [Commits](https://github.com/moby/sys/compare/mount/v0.3.4...mount/v0.3.5) --- updated-dependencies: - dependency-name: github.com/moby/sys/mount dependency-version: 0.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: moby-sys ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8af11a60589..b9ea2201cba 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( 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.16 - github.com/moby/sys/mount v0.3.4 + 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 diff --git a/go.sum b/go.sum index 1a0e9ab0970..afe2f7a567f 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ github.com/moby/moby/v2 v2.0.0-beta.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+ github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= 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= From 8bf3c1b57e74afba962ec7efd566b334bd16e105 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 15 Jun 2026 23:26:33 +0900 Subject: [PATCH 606/868] fix: suppress warning message on a stopped container in nerdctl container inspect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, when running `nerdctl container inspect` on a stopped container, the following warning message is shown: ```bash > sudo nerdctl ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4ce4cb9b1ed1 docker.io/library/nginx:latest "/docker-entrypoint.…" 14 hours ago Exited (0) 8 seconds ago nginx > sudo nerdctl container inspect nginx WARN[0000] failed to inspect NetNS error="failed to Statfs \"/proc/1548927/ns/net\": no such file or directory" id=4ce4cb9b1ed12f65b02e0f4af8a753d0e9ad1d42d5b77dac206c396021d90ab0 ... ``` This warning occurs because `/proc//ns/net` is referenced using a stale PID, but the process for a stopped container has already been released. `/proc//ns/net` should not be referenced when the container is stopped. Therefore, this commit suppresses the warning message above when running `nerdctl container inspect` on a stopped container. Signed-off-by: Hayato Kiwata --- pkg/containerinspector/containerinspector.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go index a3a77e1e60d..c8bddd15e84 100644 --- a/pkg/containerinspector/containerinspector.go +++ b/pkg/containerinspector/containerinspector.go @@ -58,11 +58,13 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta return n, nil } 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 } From a73a720b92a8ba30d4406d6ad05b29ab9434305a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:32:33 +0000 Subject: [PATCH 607/868] build(deps): bump github.com/pelletier/go-toml/v2 from 2.3.1 to 2.4.0 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.3.1 to 2.4.0. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.3.1...v2.4.0) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..fc5039168d7 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.15.1 - github.com/pelletier/go-toml/v2 v2.3.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.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index afe2f7a567f..d73dcf35a0a 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= 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.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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= From 5397366f67b9d925759d2cd2317046ba7cd06e71 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Thu, 18 Jun 2026 23:12:19 +0400 Subject: [PATCH 608/868] fix: remove duplicated defaults from help output Signed-off-by: immanuwell --- cmd/nerdctl/container/container_run.go | 6 +-- .../container/container_run_help_test.go | 44 +++++++++++++++++++ cmd/nerdctl/image/image_history.go | 2 +- cmd/nerdctl/image/image_history_help_test.go | 40 +++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 cmd/nerdctl/container/container_run_help_test.go create mode 100644 cmd/nerdctl/image/image_history_help_test.go diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 38c85a097fa..1a7384a7101 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -89,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) { @@ -159,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)") @@ -219,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 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..91021bdcc2f --- /dev/null +++ b/cmd/nerdctl/container/container_run_help_test.go @@ -0,0 +1,44 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package 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\")")) +} 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)")) +} From c8b1e8eec9b862e4f2a20dc1088c9ea12a3fdbdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:32:28 +0000 Subject: [PATCH 609/868] build(deps): bump actions/checkout from 6.0.3 to 7.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima-freebsd.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 3e29cc3c49c..8e554fd72d8 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index bb04ce034f0..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 fc61d0725a8..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 7ebab308780..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 6bc1fac639d..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 6e8f823ae48..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 72eb0aeaa72..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 e768186e1a9..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 index ef22c852a99..a7d53d64c8d 100644 --- a/.github/workflows/job-test-in-lima-freebsd.yml +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -17,7 +17,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index d8c0911dfd1..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 6d81764564b..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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 37d39d42620..1913f7f3ed3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 70a8b67aefb..5d507c9d281 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -49,7 +49,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + 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 98932baf37b..ebc5bd6ed91 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index f72aa8771a8..9de351395f9 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -35,7 +35,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 100 persist-credentials: false From 195e5a859514fe99b5d3bf24c86a8191fb4d8328 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:32:35 +0000 Subject: [PATCH 610/868] build(deps): bump github.com/cyphar/filepath-securejoin Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.6.1 to 0.7.0. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Changelog](https://github.com/cyphar/filepath-securejoin/blob/main/CHANGELOG.md) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...v0.7.0) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..31a1d6876ec 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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.3+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.7.0 @@ -147,7 +147,7 @@ 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/sys/capability v0.4.0 // indirect diff --git a/go.sum b/go.sum index afe2f7a567f..84a43d93560 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= @@ -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= From 0879c88272172c998feb8c6f27e4cf89fde2fd64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:33:05 +0000 Subject: [PATCH 611/868] build(deps): bump the docker group across 1 directory with 3 updates Bumps the docker group with 3 updates in the / directory: [github.com/docker/cli](https://github.com/docker/cli), [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.5.3+incompatible to 29.6.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.5.3...v29.6.0) Updates `github.com/moby/moby/client` from 0.4.1 to 0.5.0 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.5.0/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.4.1...v0.5.0) Updates `github.com/moby/moby/v2` from 2.0.0-beta.16 to 2.0.0-beta.18 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.16...v2.0.0-beta.18) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.6.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/client dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 24 +++++++++++------------ go.sum | 60 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..8a467b523cc 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.3+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,8 +43,8 @@ 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.16 + 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 @@ -103,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 @@ -128,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 @@ -149,7 +149,7 @@ require ( require ( cyphar.com/go-pathrs v0.2.1 // 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 afe2f7a567f..ac47cb01419 100644 --- a/go.sum +++ b/go.sum @@ -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.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= -github.com/docker/cli v29.5.3+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= @@ -204,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.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+Qz4VA= -github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= +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.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= @@ -270,8 +270,8 @@ 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= @@ -328,18 +328,18 @@ 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= @@ -370,8 +370,8 @@ 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= @@ -464,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= From 9f714bd95b71ab7b5ec27488346e543a70954432 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:34:06 +0900 Subject: [PATCH 612/868] CI: disable canary Failing since the release of go1.27rc1 ``` level=warning msg="[runner] Can't run linter goanalysis_metalinter: inspect: failed to load package context: could not load export data: internal error in importing \"context\" (cannot decode \"context\", export data version 4 is greater than maximum supported version 2); please report an issue" level=error msg="Running error: can't run linter goanalysis_metalinter\ninspect: failed to load package context: could not load export data: internal error in importing \"context\" (cannot decode \"context\", export data version 4 is greater than maximum supported version 2); please report an issue" make[1]: *** [Makefile:142: lint-go] Error 3 make[1]: Leaving directory '/home/runner/work/nerdctl/nerdctl' make: *** [Makefile:148: lint-go-all] Error 2 ``` Workaround for issue 4979 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 12 +++++++----- .github/workflows/workflow-test.yml | 17 ++++++++++------- .github/workflows/workflow-tigron.yml | 5 +++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 98932baf37b..72643ded61f 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 }} diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 8afed875f1c..0a791265daa 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 }} @@ -104,9 +105,10 @@ jobs: 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 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index f72aa8771a8..3a7ed5cad85 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -31,8 +31,9 @@ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 From c7088353ccbc294ed786c0614d79107dbfeba4dd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 12:26:56 +0900 Subject: [PATCH 613/868] CI: docker: skip TestRunSeccompCapSysPtrace and TestUpdateRestartPolicy These tests have been failing on Docker since ubuntu-24.04 image 20260615.205.1. Workaround for issue 4978 Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_run_restart_linux_test.go | 7 ++++++- cmd/nerdctl/container/container_run_security_linux_test.go | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 5a3431b0d47..17761188e39 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -30,6 +30,7 @@ import ( "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" @@ -276,7 +277,11 @@ func TestRunRestartWithUnlessStopped(t *testing.T) { func TestUpdateRestartPolicy(t *testing.T) { testCase := nerdtest.Setup() - if !nerdtest.IsDocker() { + 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"}) } 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") } From 9e39e33441383bdc016143a0b0f910bb64170c2f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:29:30 +0900 Subject: [PATCH 614/868] update BuildKit (0.31.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 diff --git a/Dockerfile b/Dockerfile index 6666a4e1cdc..384b8cba0ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ 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 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 From db4ef3e6a8d64c1766a9e8a421f7c0fd71d43be2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 11:08:10 +0900 Subject: [PATCH 615/868] tests: mark TestIPFSAddrWithKubo flaky Workaround for issue 4838 Signed-off-by: Akihiro Suda --- cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go | 1 + 1 file changed, 1 insertion(+) 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) { From 6594ceba4c75f0575ecfa5332a8c0a394dc9b3f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 04:40:30 +0000 Subject: [PATCH 616/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.1...v2.3.2) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 34eee991f14..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 diff --git a/go.sum b/go.sum index 6ff2145f0b8..de4e8c13c0b 100644 --- a/go.sum +++ b/go.sum @@ -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= From 366a10b3deb5624cf590b2ee8b1aed970ab90617 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:26:58 +0900 Subject: [PATCH 617/868] update containerd (2.3.2) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 14 +++++++------- Dockerfile | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 0a791265daa..2afcd149afb 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -52,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 }} @@ -81,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 @@ -98,7 +98,7 @@ 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 @@ -159,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/Dockerfile b/Dockerfile index 384b8cba0ee..244c8590452 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.1@64b425cf570b3b8dd1d4cc46da7c1fce65c6651a +ARG CONTAINERD_VERSION=v2.3.2@fff62f14765df376e5fc36f5a8f8e795b5670f61 ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From 2254f8e537c2a4ab6f4f832a2c1359f11204d40b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 16 Jun 2026 20:29:06 +0900 Subject: [PATCH 618/868] Use client.WithImageConfigLabels for image config labels Signed-off-by: Akihiro Suda --- pkg/cmd/container/create.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 264c4ecede2..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 != "" { @@ -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 From 97e5145980326488928f9add62986232cc74b20e Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sun, 14 Jun 2026 13:19:44 +0900 Subject: [PATCH 619/868] test: refactor container_run_test.go to use Tigron Signed-off-by: Park jungtae --- cmd/nerdctl/container/container_run_test.go | 850 +++++++++++--------- 1 file changed, 454 insertions(+), 396 deletions(-) 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) From 188f20f0c4de1bb6b61d52ca579f265c62c8d03b Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Fri, 19 Jun 2026 13:03:06 +0400 Subject: [PATCH 620/868] fix: honor --workdir in compose run Signed-off-by: Immanuel Tikhonov --- cmd/nerdctl/compose/compose_run_linux_test.go | 45 +++++++++++++++++++ pkg/composer/run.go | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 71efbb9d070..217c21708d0 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -326,6 +326,51 @@ services: testCase.Run(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: 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. From 814617f5aea6a9d5be9fdbf8d8b899bb7786d433 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sat, 20 Jun 2026 11:10:02 +0400 Subject: [PATCH 621/868] fix: clarify healthcheck help defaults Signed-off-by: immanuwell --- cmd/nerdctl/container/container_run.go | 6 +++--- cmd/nerdctl/container/container_run_help_test.go | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 1a7384a7101..b52994dd063 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -250,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") diff --git a/cmd/nerdctl/container/container_run_help_test.go b/cmd/nerdctl/container/container_run_help_test.go index 91021bdcc2f..9f8197200b5 100644 --- a/cmd/nerdctl/container/container_run_help_test.go +++ b/cmd/nerdctl/container/container_run_help_test.go @@ -41,4 +41,7 @@ func TestRunHelpDoesNotDuplicateDefaults(t *testing.T) { 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")) } From 1e8a519d9d4b381945e45b4abdf15ffb2596f75b Mon Sep 17 00:00:00 2001 From: Aaron Mark <64331623+amarkdotdev@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:07:37 +0000 Subject: [PATCH 622/868] feat: show RootlessKit version in nerdctl version output When running in rootless mode, nerdctl version now includes the RootlessKit version as a server component, matching the format used by docker version. The version is retrieved via the RootlessKit API socket (rootlessutil.NewRootlessKitClient + Info(ctx)) rather than shelling out to rootlesskit --version, which is more robust and avoids PATH issues. The version is only shown when rootless mode is active (i.e. when rootlessutil.IsRootless() is true). If the API socket is unavailable or the Info call fails, a warning is logged and the component is shown without a version string. Fixes https://github.com/containerd/nerdctl/issues/4936 Signed-off-by: Aaron Mark <64331623+amarkdotdev@users.noreply.github.com> --- pkg/infoutil/infoutil.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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() From 4213f61a02845581893643a85423877aa54ff9ec Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Mon, 22 Jun 2026 17:29:44 +0530 Subject: [PATCH 623/868] feat(run): allow -i and -d together when -t is set Previously `nerdctl run` rejected -i with -d unconditionally via a FIXME error. The combination is valid when -t is also given: the containerd shim holds the pty open, so the detached container keeps a usable stdin. Without -t, a detached interactive container cannot work in nerdctl's daemonless model -- there is no persistent process to hold the container's stdin open, so the process would read EOF immediately. That case now returns a clear error instead of the FIXME. Add integration tests for the accepted (-t -d -i) and rejected (-d -i) cases. Signed-off-by: Mayur Das --- ...ainer_run_detach_interactive_linux_test.go | 86 +++++++++++++++++++ pkg/cmd/container/create.go | 9 +- 2 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 cmd/nerdctl/container/container_run_detach_interactive_linux_test.go diff --git a/cmd/nerdctl/container/container_run_detach_interactive_linux_test.go b/cmd/nerdctl/container/container_run_detach_interactive_linux_test.go new file mode 100644 index 00000000000..730ae56893a --- /dev/null +++ b/cmd/nerdctl/container/container_run_detach_interactive_linux_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 container + +import ( + "errors" + "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/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// TestRunDetachInteractiveWithTTY verifies that `run -t -d -i` is accepted and +// starts a detached container that keeps running, matching Docker. The TTY's pty +// is held by the shim, so the combination is valid. +func TestRunDetachInteractiveWithTTY(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", "-t", "-d", "-i", "--name", data.Identifier(), + testutil.CommonImage, "sleep", "infinity") + } + + 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( + helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) + }, + } + } + + testCase.Run(t) +} + +// TestRunDetachInteractiveWithoutTTYFails verifies that `run -d -i` without -t is +// rejected: being daemonless, nerdctl has no process to keep stdin open after +// detaching. Docker (daemon-backed) supports it, so this is nerdctl-only. +func TestRunDetachInteractiveWithoutTTYFails(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + + 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", "-i", "--name", data.Identifier(), testutil.CommonImage, "cat") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("can only be specified together with -t")}, + } + } + + testCase.Run(t) +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index eb1de8df27d..b25add2d606 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -253,10 +253,11 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err } - if options.Interactive { - if options.Detach { - return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), errors.New("currently flag -i and -d cannot be specified together (FIXME)") - } + // -i with -d requires -t. Without a pty, nerdctl (being daemonless) has no + // process to keep the container's stdin open after it detaches, so the + // process would read EOF immediately; with -t the shim holds the pty open. + if options.Interactive && options.Detach && !options.TTY { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), errors.New("combination of flags -i and -d can only be specified together with -t") } if options.TTY { From 7d2afb4167bb7a450dae7df67ba2bfbf6bc9d649 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Mon, 22 Jun 2026 18:05:14 +0530 Subject: [PATCH 624/868] fix(mount): remove the non-Docker `rw` option from --mount Docker's `--mount` has no `rw` option (only `readonly`/`ro`), and in nerdctl `rw` was redundant (read-write is the default) while `rw=false` was silently ignored, leaving the mount writable. Remove `rw` from the `--mount` parser so `rw`/`rw=false` return an error, matching Docker. `ro`/`readonly`/`rro` are unchanged, and the `-v src:dst:rw` syntax is unaffected (different parser). Also fix the docs: `readonly`/`ro` are Docker-compatible, `rro` is nerdctl-specific (recursive read-only). Signed-off-by: Mayur Das --- docs/command-reference.md | 3 +- pkg/mountutil/mountutil_linux.go | 4 +- pkg/mountutil/mountutil_linux_test.go | 63 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 2195e6ee7bb..e667dbf0517 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -302,7 +302,8 @@ Volume flags: - Common Options: - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. - :whale: `dst`, `destination`, `target`: Mount destination spec. - - :whale: `readonly`, `ro`, `rw`, `rro`: Filesystem permissions. + - :whale: `readonly`, `ro`: mount the filesystem read-only. + - :nerd_face: `rro`: mount the filesystem recursively read-only. - Options specific to `bind`: - :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). - :whale: `bind-nonrecursive`: `true` or `false`(default). If set to true, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index d1fde8b1c2a..9793216f11a 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -332,7 +332,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e if len(parts) == 1 { switch key { - case "readonly", "ro", "rw", "rro": + case "readonly", "ro", "rro": rwOption = key continue case "bind-nonrecursive": @@ -361,7 +361,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e src = value case "target", "dst", "destination": dst = value - case "readonly", "ro", "rw", "rro": + case "readonly", "ro", "rro": trueValue, err := strconv.ParseBool(value) if err != nil { return nil, fmt.Errorf("invalid value for %s: %s", key, value) diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 74484cbbbfe..982a9b7ea09 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -352,3 +352,66 @@ func TestProcessFlagVAnonymousVolumes(t *testing.T) { }) } } + +// TestProcessFlagMountRW verifies that the non-Docker `rw` option is no longer +// accepted by --mount, while ro/readonly/rro remain valid read-only flags. +func TestProcessFlagMountRW(t *testing.T) { + // rw is no longer a valid --mount option. + rejected := []struct { + spec string + want string + }{ + {"type=bind,source=/foo,target=/bar,rw", "must be a key=value pair"}, + {"type=bind,source=/foo,target=/bar,rw=true", "unexpected key 'rw'"}, + {"type=bind,source=/foo,target=/bar,rw=false", "unexpected key 'rw'"}, + } + for _, tt := range rejected { + t.Run(tt.spec, func(t *testing.T) { + _, err := ProcessFlagMount(tt.spec, nil) + assert.ErrorContains(t, err, tt.want) + }) + } + + // ro/rro still parse into a complete read-only bind mount. + src := t.TempDir() + accepted := []struct { + spec string + wants *Processed + }{ + { + spec: "type=bind,source=" + src + ",target=/bar,ro", + wants: &Processed{ + Type: Bind, + Mount: specs.Mount{ + Type: "bind", + Source: src, + Destination: "/bar", + Options: []string{"rbind", "ro", "rprivate"}, + }, + }, + }, + { + spec: "type=bind,source=" + src + ",target=/bar,rro", + wants: &Processed{ + Type: Bind, + Mount: specs.Mount{ + Type: "bind", + Source: src, + Destination: "/bar", + Options: []string{"rbind", "ro", "rro", "rprivate"}, + }, + }, + }, + } + for _, tt := range accepted { + t.Run(tt.spec, func(t *testing.T) { + got, err := ProcessFlagMount(tt.spec, nil) + assert.NilError(t, err) + assert.Equal(t, got.Type, tt.wants.Type) + assert.Equal(t, got.Mount.Type, tt.wants.Mount.Type) + assert.Equal(t, got.Mount.Source, tt.wants.Mount.Source) + assert.Equal(t, got.Mount.Destination, tt.wants.Mount.Destination) + assert.DeepEqual(t, got.Mount.Options, tt.wants.Mount.Options) + }) + } +} From 05176dd8e622c90aadc8379adb291db180402fa2 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Mon, 22 Jun 2026 22:01:17 +0400 Subject: [PATCH 625/868] fix: skip rootless reexec for help-only command paths Signed-off-by: immanuwell --- cmd/nerdctl/main_linux.go | 3 ++ cmd/nerdctl/main_linux_test.go | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 cmd/nerdctl/main_linux_test.go diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go index 5aba7c2f480..08fd24e4773 100644 --- a/cmd/nerdctl/main_linux.go +++ b/cmd/nerdctl/main_linux.go @@ -35,6 +35,9 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { if !rootlessutil.IsRootlessParent() { return false } + if len(args) == 0 && cmd.HasSubCommands() { + return false + } if len(commands) < 2 { return true } diff --git a/cmd/nerdctl/main_linux_test.go b/cmd/nerdctl/main_linux_test.go new file mode 100644 index 00000000000..303b1e833e1 --- /dev/null +++ b/cmd/nerdctl/main_linux_test.go @@ -0,0 +1,69 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" +) + +func TestAppNeedsRootlessParentMain(t *testing.T) { + if !rootlessutil.IsRootlessParent() { + t.Skip("test requires a rootless parent context") + } + + app, err := newApp() + assert.NilError(t, err) + + tests := []struct { + name string + path []string + expected bool + }{ + { + name: "root help path does not require reexec", + path: nil, + expected: false, + }, + { + name: "management command help path does not require reexec", + path: []string{"system"}, + expected: false, + }, + { + name: "runtime command still requires reexec", + path: []string{"system", "info"}, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := app + args := []string{} + if len(tc.path) > 0 { + var findErr error + cmd, args, findErr = app.Find(tc.path) + assert.NilError(t, findErr) + } + assert.Equal(t, appNeedsRootlessParentMain(cmd, args), tc.expected) + }) + } +} From 6674b302b7c9f333ddffacb87d4c5f5b12617b4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:32:22 +0000 Subject: [PATCH 626/868] build(deps): bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.6 to 0.5.7. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/5f14fd08f7cf1cb1609c1e344975f152c7ee938d...192e21d79ab29983730a13d1382995c2307fbcaa) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.5.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index cae92db27d3..7933223fcc4 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,7 +92,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From 15f871ff9647ed2000ac91d188c040e0be54036b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:32:34 +0000 Subject: [PATCH 627/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.11.0 to 2.12.1. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.11.0...v2.12.1) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.12.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 246436fa89e..90168be5355 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.1 - github.com/compose-spec/compose-go/v2 v2.11.0 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.12.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index de4e8c13c0b..fab993af1e2 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM= -github.com/compose-spec/compose-go/v2 v2.11.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.12.1 h1:+xBZNxcgSus4atQJwXPEdhHRgCEyZmj/BuqN5m33Ou0= +github.com/compose-spec/compose-go/v2 v2.12.1/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= From 49a33d5df44bd4c65855a6fbdcdd96b353e15bcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:32:46 +0000 Subject: [PATCH 628/868] build(deps): bump github.com/pelletier/go-toml/v2 from 2.4.0 to 2.4.1 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.4.0...v2.4.1) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 246436fa89e..a2bf89144e0 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.15.1 - github.com/pelletier/go-toml/v2 v2.4.0 + github.com/pelletier/go-toml/v2 v2.4.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index de4e8c13c0b..79258630d6d 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= 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/pelletier/go-toml/v2 v2.4.1 h1:j5OMOImsH+j2k7GJ5YO+RxfWwohNiH6t5zB/+h3bagc= +github.com/pelletier/go-toml/v2 v2.4.1/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= From 6b4ad409ce3d18ff1cf8f29bba91cda6643d608d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:32:24 +0000 Subject: [PATCH 629/868] build(deps): bump actions/cache from 5.0.5 to 6.0.0 Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.0.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...2c8a9bd7457de244a408f35966fab2fb45fda9c8) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima-freebsd.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml index a7d53d64c8d..5b382025fc1 100644 --- a/.github/workflows/job-test-in-lima-freebsd.yml +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -27,7 +27,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }}-freebsd diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 3eec402053b..1df50af4921 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -41,7 +41,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} From 60c5c483d947de87e443fb5bceeb772cc80752fe Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 25 Jun 2026 03:00:56 +0900 Subject: [PATCH 630/868] update runc (1.5.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 244c8590452..828c09e7ac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.3.2@fff62f14765df376e5fc36f5a8f8e795b5670f61 -ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 +ARG RUNC_VERSION=v1.5.0@c4bb59526d0c9cf3a3a46a04d08ca031749a2119 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From 0b3829648fb86af42cb6d6602567ab78d4762119 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 25 Jun 2026 03:18:42 +0900 Subject: [PATCH 631/868] update BuildKit (0.31.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 diff --git a/Dockerfile b/Dockerfile index 828c09e7ac7..0b68758e902 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.5.0@c4bb59526d0c9cf3a3a46a04d08ca031749a2119 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.31.0@BINARY +ARG BUILDKIT_VERSION=v0.31.1@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 deleted file mode 100644 index 2ce0cd2cb67..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 +++ /dev/null @@ -1,2 +0,0 @@ -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/buildkit-v0.31.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 new file mode 100644 index 00000000000..798584ad2f5 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 @@ -0,0 +1,2 @@ +1fc78750d0c96bdc18799a3c0b551d6807bd4939e8cd79e357823e467451e16e buildkit-v0.31.1.linux-amd64.tar.gz +a4c9bef205f61c1e4253bdfd196e1db303aca25e8756105d75c6fddfbab7903f buildkit-v0.31.1.linux-arm64.tar.gz From 1a2b44acf8b79dec02b7d86cd3edf7193c61e56e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:06:33 +0000 Subject: [PATCH 632/868] build(deps): bump github.com/Microsoft/hcsshim Bumps [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) from 0.15.0-rc.1 to 0.15.0-rc.2. - [Release notes](https://github.com/Microsoft/hcsshim/releases) - [Commits](https://github.com/Microsoft/hcsshim/compare/v0.15.0-rc.1...v0.15.0-rc.2) --- updated-dependencies: - dependency-name: github.com/Microsoft/hcsshim dependency-version: 0.15.0-rc.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 6db69145c41..23c487f8f6e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.26.3 require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 - github.com/Microsoft/hcsshim v0.15.0-rc.1 + github.com/Microsoft/hcsshim v0.15.0-rc.2 github.com/compose-spec/compose-go/v2 v2.12.1 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined @@ -84,7 +84,7 @@ require ( github.com/containers/ocicrypt v1.2.1 // indirect github.com/creack/pty v1.1.24 // indirect github.com/djherbis/times v1.6.0 // indirect - github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -96,7 +96,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mattn/go-shellwords v1.0.13 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index a76b57524a2..af9a6d26d59 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= -github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= +github.com/Microsoft/hcsshim v0.15.0-rc.2 h1:/3Izdm/kGtHjqC0wdwepupLaz+U+xIjA1i03RM3v/SA= +github.com/Microsoft/hcsshim v0.15.0-rc.2/go.mod h1:HICB5JfsB/rThwBKw9LiTd40H79lXbp/hzRgLGNLJLs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -95,8 +95,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -188,8 +188,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= +github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= @@ -453,8 +453,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From 8328fb146d5c217fda3b689c2ce92ba63a3ca635 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:32:30 +0000 Subject: [PATCH 633/868] build(deps): bump actions/setup-go from 6.4.0 to 6.5.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.4.0 to 6.5.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...924ae3a1cded613372ab5595356fb5720e22ba16) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 0040f9cd89d..41fb102692c 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -53,7 +53,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index 4ba1d420c3b..c16f5ffb5f9 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -56,7 +56,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index fb4d849db3e..c74d3bab499 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: "Init: install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 09430928c1a..4d12106dab0 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -126,7 +126,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 7e444244616..0e1eaf8c835 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -64,7 +64,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1913f7f3ed3..b2bf225108b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: "Install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: "1.26" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 2c8024ef658..576fe49c2c0 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -51,7 +51,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From 15ed8f6d114ab6e52b03651c0e7bcce4fc815f40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:32:31 +0000 Subject: [PATCH 634/868] build(deps): bump github.com/pelletier/go-toml/v2 from 2.4.1 to 2.4.2 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.4.1 to 2.4.2. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.4.1...v2.4.2) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6db69145c41..9c58ee64f31 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.15.1 - github.com/pelletier/go-toml/v2 v2.4.1 + github.com/pelletier/go-toml/v2 v2.4.2 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index a76b57524a2..e6dd2c4ed6f 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= 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.1 h1:j5OMOImsH+j2k7GJ5YO+RxfWwohNiH6t5zB/+h3bagc= -github.com/pelletier/go-toml/v2 v2.4.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q= +github.com/pelletier/go-toml/v2 v2.4.2/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= From 490d28461a769c6134ff738d2e25709ca2329931 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Tue, 23 Jun 2026 10:10:18 +0530 Subject: [PATCH 635/868] test: de-parallelize TestRunQuiet so it stops GC'ing the shared image TestRunQuiet force-removes the shared CommonImage (== AlpineImage) in its setup and cleanup to exercise a fresh pull, but it ran in parallel. The containerd content store is global across namespaces, so that rmi GCs the Alpine layer blobs out from under concurrent tests mid-run, which then fail with "content digest not found" (observed in TestRunUmask). Mark TestRunQuiet NoParallel so it no longer races other tests using the shared image, instead of pulling the image in each consumer's setup. Signed-off-by: Mayur Das --- cmd/nerdctl/container/container_run_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 8ad9c6d8c21..b26db7dc2d0 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -798,6 +798,11 @@ func TestRunAttachFlag(t *testing.T) { func TestRunQuiet(t *testing.T) { testCase := nerdtest.Setup() + // This test removes the shared image to force a fresh pull, so it must not + // run alongside other tests that use it: the content store is global across + // namespaces, so the rmi would GC layers out from under a parallel run. + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", testutil.CommonImage) } From 0968a71acc636f512c2924c1168bbd16287526c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:36:55 +0000 Subject: [PATCH 636/868] build(deps): bump github.com/moby/sys/user in the moby-sys group Bumps the moby-sys group with 1 update: [github.com/moby/sys/user](https://github.com/moby/sys). Updates `github.com/moby/sys/user` from 0.4.0 to 0.4.1 - [Release notes](https://github.com/moby/sys/releases) - [Commits](https://github.com/moby/sys/compare/user/v0.4.0...user/v0.4.1) --- updated-dependencies: - dependency-name: github.com/moby/sys/user dependency-version: 0.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: moby-sys ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bfc20340074..f0675f109ab 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( 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/user v0.4.1 //gomodjail:unconfined github.com/moby/sys/userns v0.1.0 //gomodjail:unconfined github.com/moby/term v0.5.2 //gomodjail:unconfined github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 80f338a929d..fbecf7df5be 100644 --- a/go.sum +++ b/go.sum @@ -222,8 +222,8 @@ github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0 github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0= +github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= From fa7a31754891e7cabc2995817789422b48eac64d Mon Sep 17 00:00:00 2001 From: Takumi Akasaka Date: Fri, 26 Jun 2026 14:15:07 +0900 Subject: [PATCH 637/868] refactor: migrate container_run_network_windows_test.go to nerdtest.Setup Refactor the test cases in container_run_network_windows_test.go to use the Tigron framework (nerdtest.Setup). TestRunPort is left as-is because it only delegates to baseTestRunPort, shared with container_run_network_base_test.go and migrated separately. Signed-off-by: Takumi Akasaka --- .../container_run_network_windows_test.go | 208 ++++++++++-------- 1 file changed, 112 insertions(+), 96 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_windows_test.go b/cmd/nerdctl/container/container_run_network_windows_test.go index a727978eed5..848acd848fe 100644 --- a/cmd/nerdctl/container/container_run_network_windows_test.go +++ b/cmd/nerdctl/container/container_run_network_windows_test.go @@ -25,39 +25,24 @@ import ( "github.com/Microsoft/hcsshim" "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/defaults" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // TestRunInternetConnectivity tests Internet connectivity by pinging github.com. func TestRunInternetConnectivity(t *testing.T) { - base := testutil.NewBase(t) - - type testCase struct { - args []string - } - testCases := []testCase{ - { - args: []string{"--net", "nat"}, - }, - } - for _, tc := range testCases { - tc := tc // IMPORTANT - name := "default" - if len(tc.args) > 0 { - name = strings.Join(tc.args, "_") - } - t.Run(name, func(t *testing.T) { - args := []string{"run", "--rm"} - args = append(args, tc.args...) - // TODO(aznashwan): smarter way to ensure internet connectivity is working. - // ping doesn't seem to work on GitHub Actions ("Request timed out.") - args = append(args, testutil.CommonImage, "curl.exe -sSL https://github.com") - cmd := base.Cmd(args...) - cmd.AssertOutContains("") - }) - } + testCase := nerdtest.Setup() + // TODO(aznashwan): smarter way to ensure internet connectivity is working. + // ping doesn't seem to work on GitHub Actions ("Request timed out.") + testCase.Command = test.Command("run", "--rm", "--net", "nat", testutil.CommonImage, "curl.exe -sSL https://github.com") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("")) + testCase.Run(t) } func TestRunPort(t *testing.T) { @@ -88,60 +73,75 @@ func listHnsEndpointsRegex(hnsEndpointNameRegex string) ([]hcsshim.HNSEndpoint, // Asserts whether the container with the provided has any HNS endpoints with the expected // naming format (`${container_id}_${network_name}`) for all of the provided network names. // The container ID can be a regex. -func assertHnsEndpointsExistence(t *testing.T, shouldExist bool, containerIDRegex string, networkNames ...string) { +func assertHnsEndpointsExistence(helpers test.Helpers, shouldExist bool, containerIDRegex string, networkNames ...string) { + helpers.T().Helper() for _, netName := range networkNames { endpointName := fmt.Sprintf("%s_%s", containerIDRegex, netName) - - testName := fmt.Sprintf("hns_endpoint_%s_shouldExist_%t", endpointName, shouldExist) - t.Run(testName, func(t *testing.T) { - matchingEndpoints, err := listHnsEndpointsRegex(endpointName) - assert.NilError(t, err) - if shouldExist { - assert.Equal(t, len(matchingEndpoints), 1) - assert.Equal(t, matchingEndpoints[0].Name, endpointName) - } else { - assert.Equal(t, len(matchingEndpoints), 0) - } - }) + matchingEndpoints, err := listHnsEndpointsRegex(endpointName) + assert.NilError(helpers.T(), err) + if shouldExist { + assert.Equal(helpers.T(), len(matchingEndpoints), 1) + assert.Equal(helpers.T(), matchingEndpoints[0].Name, endpointName) + } else { + assert.Equal(helpers.T(), len(matchingEndpoints), 0) + } } } // Tests whether HNS endpoints are properly created and managed throughout the lifecycle of a container. func TestHnsEndpointsExistDuringContainerLifecycle(t *testing.T) { - base := testutil.NewBase(t) - - testNet, err := getTestingNetwork() - assert.NilError(t, err) - - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - cmd := base.Cmd( - "create", - "--name", tID, - "--net", testNet.Name, - testutil.CommonImage, - "bash", "-c", - // NOTE: the BusyBox image used in Windows testing's `sleep` binary - // does not support the `infinity` argument. - "tail", "-f", - ) - t.Logf("Creating HNS lifecycle test container with command: %q", strings.Join(cmd.Command, " ")) - containerID := strings.TrimSpace(cmd.Run().Stdout()) - t.Logf("HNS endpoint lifecycle test container ID: %q", containerID) - - // HNS endpoints should be allocated on container creation. - assertHnsEndpointsExistence(t, true, containerID, testNet.Name) + testCase := nerdtest.Setup() + // This test inspects host-wide HNS endpoint state on the shared default network, + // which is not safe to run in parallel with other tests touching the same network. + testCase.NoParallel = true - // Starting and stopping the container should NOT affect/change the endpoints. - base.Cmd("start", containerID).AssertOK() - assertHnsEndpointsExistence(t, true, containerID, testNet.Name) + var netName string + var containerID string - base.Cmd("stop", containerID).AssertOK() - assertHnsEndpointsExistence(t, true, containerID, testNet.Name) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testNet, err := getTestingNetwork() + assert.NilError(helpers.T(), err) + netName = testNet.Name - // Removing the container should remove the HNS endpoints. - base.Cmd("rm", containerID).AssertOK() - assertHnsEndpointsExistence(t, false, containerID, testNet.Name) + // NOTE: the BusyBox image used in Windows testing's `sleep` binary + // does not support the `infinity` argument. + createOut := helpers.Capture( + "create", + "--name", data.Identifier(), + "--net", testNet.Name, + testutil.CommonImage, + "bash", "-c", + "tail", "-f", + ) + containerID = strings.TrimSpace(createOut) + helpers.T().Log(fmt.Sprintf("HNS endpoint lifecycle test container ID: %q", containerID)) + + // HNS endpoints should be allocated on container creation. + assertHnsEndpointsExistence(helpers, true, containerID, netName) + + // Starting and stopping the container should NOT affect/change the endpoints. + helpers.Ensure("start", containerID) + assertHnsEndpointsExistence(helpers, true, containerID, netName) + + helpers.Ensure("stop", containerID) + assertHnsEndpointsExistence(helpers, true, containerID, netName) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Removing the container should remove the HNS endpoints. + return helpers.Command("rm", containerID) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assertHnsEndpointsExistence(helpers, false, containerID, netName) + }, + } + } + testCase.Run(t) } // Returns a network to be used for testing. @@ -159,30 +159,46 @@ func getTestingNetwork() (*netutil.NetworkConfig, error) { // Tests whether HNS endpoints are properly removed when running `run --rm`. func TestHnsEndpointsRemovedAfterAttachedRun(t *testing.T) { - base := testutil.NewBase(t) - - testNet, err := getTestingNetwork() - assert.NilError(t, err) - - // NOTE: because we cannot set/obtain the ID of the container to check for the exact HNS - // endpoint name, we record the number of HNS endpoints on the testing network and - // ensure it remains constant until after the test. - existingEndpoints, err := listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) - assert.NilError(t, err) - originalEndpointsCount := len(existingEndpoints) - - tID := testutil.Identifier(t) - base.Cmd( - "run", - "--name", - tID, - "--rm", - "--net", testNet.Name, - testutil.CommonImage, - "ipconfig", "/all", - ).AssertOK() - - existingEndpoints, err = listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) - assert.NilError(t, err) - assert.Equal(t, originalEndpointsCount, len(existingEndpoints), "the number of HNS endpoints should equal pre-test amount") + testCase := nerdtest.Setup() + // This test counts host-wide HNS endpoints on the shared default network before and after + // the run; concurrent tests creating/removing containers on the same network would corrupt + // the count. Container cleanup is handled by `--rm`, so no Cleanup callback is needed. + testCase.NoParallel = true + + var netName string + var originalEndpointsCount int + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testNet, err := getTestingNetwork() + assert.NilError(helpers.T(), err) + netName = testNet.Name + + // NOTE: because we cannot set/obtain the ID of the container to check for the exact HNS + // endpoint name, we record the number of HNS endpoints on the testing network and + // ensure it remains constant until after the test. + existingEndpoints, err := listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) + assert.NilError(helpers.T(), err) + originalEndpointsCount = len(existingEndpoints) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "run", + "--name", data.Identifier(), + "--rm", + "--net", netName, + testutil.CommonImage, + "ipconfig", "/all", + ) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + existingEndpoints, err := listHnsEndpointsRegex(fmt.Sprintf(".*_%s", netName)) + assert.NilError(t, err) + assert.Equal(t, originalEndpointsCount, len(existingEndpoints), "the number of HNS endpoints should equal pre-test amount") + }, + } + } + testCase.Run(t) } From 867cc57f25680afb14edb8d9de187c23f96f1a19 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Fri, 26 Jun 2026 11:37:49 +0400 Subject: [PATCH 638/868] test: remove redundant test lock pre-check Signed-off-by: immanuwell --- pkg/testutil/testutil.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index f6bf39dda7a..603566b340c 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -18,7 +18,6 @@ package testutil import ( "encoding/json" - "errors" "flag" "fmt" "io" @@ -504,14 +503,6 @@ func M(m *testing.M) { } os.Exit(func() int { - // If there is a lockfile (no err), or if we error-ed stating it (permission), another test run is currently going. - // Note that this could be racy. The .lock file COULD get acquired after this and before we hit the lock section. - // This is not a big deal then: we will just wait for the lock to free. - if _, err := os.Stat(testLockFile); err == nil || !errors.Is(err, os.ErrNotExist) { - log.L.Errorf("Another test binary is already running. If you think this is an error, manually remove %s", testLockFile) - return 1 - } - err := os.MkdirAll(filepath.Dir(testLockFile), 0o777) if err != nil { log.L.WithError(err).Errorf("failed creating testing lock directory %q", filepath.Dir(testLockFile)) From 31a40e9bc8047bdd2851c435a9fffa04e2cfd20b Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Thu, 25 Jun 2026 20:57:17 +0530 Subject: [PATCH 639/868] fix(network): match each gateway to its subnet for dual-stack network create paired a single --gateway with every subnet, so on a dual-stack network the IPv6 gateway was checked against the IPv4 subnet and creation failed with "no matching subnet". Accept --gateway more than once and match each gateway to the subnet that contains it. Signed-off-by: Mayur Das --- cmd/nerdctl/network/network_create.go | 6 +-- .../network/network_create_linux_test.go | 35 ++++++++++++++++++ docs/command-reference.md | 2 +- pkg/api/types/network_types.go | 2 +- pkg/cmd/network/create.go | 2 +- pkg/netutil/netutil.go | 6 +-- pkg/netutil/netutil_unix.go | 37 +++++++++++++++++-- pkg/netutil/netutil_windows.go | 8 +++- 8 files changed, 84 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index 720a6ff1cec..7d6c02cb123 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -47,7 +47,7 @@ func createCommand() *cobra.Command { cmd.RegisterFlagCompletionFunc("ipam-driver", completion.IPAMDrivers) cmd.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options") cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) - cmd.Flags().String("gateway", "", `Gateway for the master subnet`) + cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") cmd.Flags().String("ip-range", "", `Allocate container ip from a sub-range`) cmd.Flags().StringArray("label", nil, "Set metadata for a network") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") @@ -84,7 +84,7 @@ func createAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - gatewayStr, err := cmd.Flags().GetString("gateway") + gateways, err := cmd.Flags().GetStringArray("gateway") if err != nil { return err } @@ -114,7 +114,7 @@ func createAction(cmd *cobra.Command, args []string) error { IPAMDriver: ipamDriver, IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), Subnets: subnets, - Gateway: gatewayStr, + Gateway: gateways, IPRange: ipRangeStr, Labels: labels, IPv6: ipv6, diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index f9c35c845aa..e43bbddf983 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -109,6 +109,41 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "dual-stack with explicit gateways", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + // Before the fix the IPv6 gateway was checked against the IPv4 + // subnet and creation failed. + helpers.Ensure("network", "create", data.Identifier(), + "--ipv6", + "--subnet", "10.5.0.0/16", + "--subnet", "2001:db8:5::/64", + "--gateway", "10.5.0.1", + "--gateway", "2001:db8:5::1", + ) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + netw := nerdtest.InspectNetwork(helpers, data.Identifier()) + gateways := map[string]string{} + for _, c := range netw.IPAM.Config { + gateways[c.Subnet] = c.Gateway + } + assert.Equal(t, gateways["10.5.0.0/16"], "10.5.0.1") + assert.Equal(t, gateways["2001:db8:5::/64"], "2001:db8:5::1") + }, + } + }, + }, { Description: "internal enabled", Setup: func(data test.Data, helpers test.Helpers) { diff --git a/docs/command-reference.md b/docs/command-reference.md index e667dbf0517..55813aad960 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1265,7 +1265,7 @@ Flags: - :nerd_face: `--ipam-driver=dhcp`: DHCP IPAM driver for unix, requires root - :whale: `--ipam-opt`: Set IPAM driver specific options - :whale: `--subnet`: Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16" -- :whale: `--gateway`: Gateway for the master subnet +- :whale: `--gateway`: IPv4 or IPv6 Gateway for the master subnet - :whale: `--ip-range`: Allocate container ip from a sub-range - :whale: `--label`: Set metadata on a network - :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet. diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go index 530f66fa729..17ca78a4cf6 100644 --- a/pkg/api/types/network_types.go +++ b/pkg/api/types/network_types.go @@ -31,7 +31,7 @@ type NetworkCreateOptions struct { IPAMDriver string IPAMOptions map[string]string Subnets []string - Gateway string + Gateway []string IPRange string Labels []string IPv6 bool diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index dc62875863e..69f4b80a2c8 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -28,7 +28,7 @@ import ( func Create(options types.NetworkCreateOptions, stdout io.Writer) error { if len(options.Subnets) == 0 { - if options.Gateway != "" || options.IPRange != "" { + if len(options.Gateway) > 0 || options.IPRange != "" { return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") } options.Subnets = []string{""} diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 5f213d8692f..e95c17fbb4d 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -438,20 +438,20 @@ func (e *CNIEnv) createDefaultNetworkConfig(bridgeIP string) error { } bridgeCIDR := DefaultCIDR - bridgeGatewayIP := "" + var bridgeGateways []string if bridgeIP != "" { bIP, bCIDR, err := net.ParseCIDR(bridgeIP) if err != nil { return fmt.Errorf("invalid bridge ip %s: %w", bridgeIP, err) } - bridgeGatewayIP = bIP.String() + bridgeGateways = []string{bIP.String()} bridgeCIDR = bCIDR.String() } opts := types.NetworkCreateOptions{ Name: DefaultNetworkName, Driver: DefaultNetworkName, Subnets: []string{bridgeCIDR}, - Gateway: bridgeGatewayIP, + Gateway: bridgeGateways, IPAMDriver: "default", Labels: []string{fmt.Sprintf("%s=true", labels.NerdctlDefaultNetwork)}, } diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index 046c173d122..4a5d1c0c50f 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -215,7 +215,7 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": @@ -225,13 +225,14 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRan {Dst: "0.0.0.0/0"}, } } - ranges, findIPv4, err := e.parseIPAMRanges(subnets, gatewayStr, ipRangeStr, ipv6) + ranges, findIPv4, err := e.parseIPAMRanges(subnets, gateways, ipRangeStr, ipv6) if err != nil { return nil, err } ipamConf.Ranges = append(ipamConf.Ranges, ranges...) if !findIPv4 { - ranges, _, _ = e.parseIPAMRanges([]string{""}, gatewayStr, ipRangeStr, ipv6) + // The default IPv4 range uses a computed gateway; pass none. + ranges, _, _ = e.parseIPAMRanges([]string{""}, nil, ipRangeStr, ipv6) ipamConf.Ranges = append(ipamConf.Ranges, ranges...) } ipamConfig = ipamConf @@ -288,9 +289,21 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRan return ipam, nil } -func (e *CNIEnv) parseIPAMRanges(subnets []string, gateway, ipRange string, ipv6 bool) ([][]IPAMRange, bool, error) { +func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRange string, ipv6 bool) ([][]IPAMRange, bool, error) { + // Parse the gateways once up front; matching them to subnets below is then + // just a containment check, with no parse error mixed into the loop. + parsedGateways := make([]net.IP, len(gateways)) + for i, g := range gateways { + gw := net.ParseIP(g) + if gw == nil { + return nil, false, fmt.Errorf("failed to parse gateway %q", g) + } + parsedGateways[i] = gw + } + findIPv4 := false ranges := make([][]IPAMRange, 0, len(subnets)) + used := make([]bool, len(gateways)) for i := range subnets { subnet, err := e.parseSubnet(subnets[i]) if err != nil { @@ -303,12 +316,28 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateway, ipRange string, ipv6 if !findIPv4 && subnet.IP.To4() != nil { findIPv4 = true } + // Pair the subnet with the gateway it contains, so dual-stack matches + // the v4 gateway to the v4 subnet and v6 to v6. + gateway := "" + for j, gw := range parsedGateways { + if !used[j] && subnet.Contains(gw) { + gateway, used[j] = gateways[j], true + break + } + } ipamRange, err := parseIPAMRange(subnet, gateway, ipRange) if err != nil { return nil, findIPv4, err } ranges = append(ranges, []IPAMRange{*ipamRange}) } + // Only known after every subnet is seen: a gateway that matched none is a + // user error, same as Docker. + for j, ok := range used { + if !ok { + return nil, findIPv4, fmt.Errorf("no matching subnet for gateway %q", gateways[j]) + } + } return ranges, findIPv4, nil } diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 484b03c9b77..71d9a76ce6c 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -72,13 +72,19 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { switch driver { case "default": default: return nil, fmt.Errorf("unsupported ipam driver %q", driver) } + // Windows is single-subnet, so use at most one gateway. + gatewayStr := "" + if len(gateways) > 0 { + gatewayStr = gateways[0] + } + ipamConfig := newWindowsIPAMConfig() subnet, err := e.parseSubnet(subnets[0]) if err != nil { From 7a532e1c4b70c7cf658034d72ad7c45613f42913 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 25 Jun 2026 21:00:36 +0900 Subject: [PATCH 640/868] logging: write container output synchronously so final output is not truncated Logging tests such as TestLogsWithoutNewlineOrEOF and TestLogsAfterRestartingContainer flake under gomodjail: a container's final output, in particular a line emitted right before exit with no trailing newline, is sometimes dropped from the log. Running nerdctl run --name c alpine printf "'Hello World!\nThere is no newline'" nerdctl logs -f c would intermittently print only "'Hello World!" instead of the full output. Two independent problems, both reproduced locally under gomodjail: 1. getContainerWait hung for short-lived containers. The logging process is started by containerd while it sets up the container's IO, before the task is created, so the first con.Task() returns NotFound and the code retried forever "waiting for the task to start". For a fast container the task can instead have already exited and been removed before the logger ever sees it, so it never appeared and the logger blocked forever holding the logger lock. It now concludes the container has exited once it is missing and the container has been observed producing output. 2. The container's final chunk was lost to teardown. On exit containerd closes the stdio FIFOs and tears the logging process down almost immediately. The old path read the FIFO, copied it through an io.Pipe and a bufio splitter, and handed each line to the driver over a buffered channel; a trailing chunk with no newline was held in the splitter until EOF and then raced the teardown across several goroutines, so it was frequently lost. The logger now reads each FIFO directly and, for drivers that can write synchronously (json-file, via the new SyncDriver interface), writes each entry inline from the reading goroutine and flushes a trailing no-newline fragment as soon as it is read. Streaming drivers keep using the buffered channel so a slow driver cannot block the container. The viewer also does a final read of the JSON log file when it receives the stop signal, so entries flushed just before exit are not missed. Verified locally with the gomodjail-packed binary: 250+ iterations of the failing printf case, the restart (doubled-output) case, multi-line output and follow-on-running-container all pass with no truncation. Fixes https://github.com/containerd/nerdctl/issues/5006 Assisted-by: Claude Opus 4.8 Signed-off-by: Akihiro Suda --- pkg/logging/json_logger.go | 27 +++- pkg/logging/json_logger_test.go | 40 ++++++ pkg/logging/jsonfile/jsonfile.go | 24 ++++ pkg/logging/logging.go | 203 +++++++++++++++++++++++-------- pkg/logging/logging_test.go | 130 +++++++++++++++++++- 5 files changed, 371 insertions(+), 53 deletions(-) diff --git a/pkg/logging/json_logger.go b/pkg/logging/json_logger.go index 7e2dca196fd..02862965046 100644 --- a/pkg/logging/json_logger.go +++ b/pkg/logging/json_logger.go @@ -48,8 +48,9 @@ var JSONDriverLogOpts = []string{ } type JSONLogger struct { - Opts map[string]string - logger *logrotate.Logger + Opts map[string]string + logger *logrotate.Logger + encoder *jsonfile.SyncEncoder } func JSONFileLogOptsValidate(logOptMap map[string]string) error { @@ -119,6 +120,7 @@ func (jsonLogger *JSONLogger) PreProcess(ctx context.Context, dataStore string, // MaxBackups does not include file to write logs to l.MaxBackups = maxFile - 1 jsonLogger.logger = l + jsonLogger.encoder = jsonfile.NewSyncEncoder(l) return nil } @@ -126,6 +128,14 @@ func (jsonLogger *JSONLogger) Process(stdout <-chan string, stderr <-chan string return jsonfile.Encode(stdout, stderr, jsonLogger.logger) } +// WriteLogEntry writes a single log line synchronously, implementing SyncDriver. +// Writing inline (rather than over a channel consumed by Process) ensures a +// container's final output is durable before containerd tears the logging +// process down on exit. https://github.com/containerd/nerdctl/issues/5006 +func (jsonLogger *JSONLogger) WriteLogEntry(stream, line string) error { + return jsonLogger.encoder.Encode(stream, line) +} + func (jsonLogger *JSONLogger) PostProcess() error { return nil } @@ -182,7 +192,18 @@ func viewLogsJSONFileDirect(lvopts LogViewOptions, jsonLogFilePath string, stdou for { select { case <-stopChannel: - log.L.Debug("received stop signal while re-reading JSON logfile, returning") + log.L.Debug("received stop signal while re-reading JSON logfile, draining remaining logs and returning") + // The stop signal is only sent after WaitForLogger has confirmed that the + // logger finished writing (see pkg/cmd/container.Logs). However, the + // watcher-driven read loop may not have consumed the final entries yet: + // they may have been flushed to the file while we were blocked in + // startTail, and the stop signal wins the next select iteration before + // they are read. Do a final read so that we don't drop log content that + // was written right before the container exited. + // https://github.com/containerd/nerdctl/issues/5006 + if _, err := jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until); err != nil { + log.L.WithError(err).Debugf("error draining remaining logs from JSON logfile %q", jsonLogFilePath) + } return nil default: if stop || (limitedMode && limitedNum == 0) { diff --git a/pkg/logging/json_logger_test.go b/pkg/logging/json_logger_test.go index 7b41c10a68c..7ecf183b851 100644 --- a/pkg/logging/json_logger_test.go +++ b/pkg/logging/json_logger_test.go @@ -109,6 +109,46 @@ func TestReadRotatedJSONLog(t *testing.T) { } } +// TestReadJSONLogsDrainsOnStop verifies that when the stop signal is received +// while following a log file, any log entries that were flushed but not yet +// read are still drained and written out before returning. +// Regression test for https://github.com/containerd/nerdctl/issues/5006 +func TestReadJSONLogsDrainsOnStop(t *testing.T) { + file, err := os.CreateTemp("", "TestDrainOnStop") + if err != nil { + t.Fatalf("unable to create temp file") + } + defer os.Remove(file.Name()) + // A final entry without a trailing newline in the log text, mirroring the + // scenario in the linked issue. + file.WriteString(`{"log":"Hello World!\n","stream":"stdout","time":"2024-07-12T03:09:24.916296732Z"}` + "\n") + file.WriteString(`{"log":"There is no newline","stream":"stdout","time":"2024-07-12T03:09:24.916296732Z"}` + "\n") + + // Pre-load the stop signal so the first select iteration takes the stop + // branch while the file still has unread content. This deterministically + // reproduces the race where the stop signal wins before the final entries + // are read. + stopChan := make(chan os.Signal, 1) + stopChan <- os.Interrupt + + stdoutBuf := bytes.NewBuffer(nil) + stderrBuf := bytes.NewBuffer(nil) + lvOpts := LogViewOptions{ + LogPath: file.Name(), + Follow: true, + } + if err := viewLogsJSONFileDirect(lvOpts, file.Name(), stdoutBuf, stderrBuf, stopChan); err != nil { + t.Fatal(err.Error()) + } + if stderrBuf.Len() > 0 { + t.Fatalf("Stderr: %v", stderrBuf.String()) + } + const expected = "Hello World!\nThere is no newline" + if actual := stdoutBuf.String(); expected != actual { + t.Fatalf("Actual output does not match expected.\nActual: %q\nExpected: %q\n", actual, expected) + } +} + func TestReadJSONLogs(t *testing.T) { file, err := os.CreateTemp("", "TestFollowLogs") if err != nil { diff --git a/pkg/logging/jsonfile/jsonfile.go b/pkg/logging/jsonfile/jsonfile.go index 22afc6f3442..e2c2829b70c 100644 --- a/pkg/logging/jsonfile/jsonfile.go +++ b/pkg/logging/jsonfile/jsonfile.go @@ -43,6 +43,30 @@ func Path(dataStore, ns, id string) string { return filepath.Join(dataStore, "containers", ns, id, id+"-json.log") } +// SyncEncoder writes individual json-file log entries to a writer. Its Encode +// method is safe for concurrent use, so it can be shared between the goroutines +// reading a container's stdout and stderr. +type SyncEncoder struct { + mu sync.Mutex + enc *json.Encoder +} + +// NewSyncEncoder returns a SyncEncoder that writes to w. +func NewSyncEncoder(w io.Writer) *SyncEncoder { + return &SyncEncoder{enc: json.NewEncoder(w)} +} + +// Encode writes a single log entry for the given stream. +func (s *SyncEncoder) Encode(stream, line string) error { + s.mu.Lock() + defer s.mu.Unlock() + return s.enc.Encode(&Entry{ + Stream: stream, + Log: line, + Time: time.Now().UTC(), + }) +} + func Encode(stdout <-chan string, stderr <-chan string, writer io.Writer) error { enc := json.NewEncoder(writer) var encMu sync.Mutex diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 91a3231ee3a..a93594bd8c0 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -17,7 +17,7 @@ package logging import ( - "bufio" + "bytes" "context" "encoding/json" "errors" @@ -28,6 +28,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "time" "github.com/fsnotify/fsnotify" @@ -52,6 +53,11 @@ const ( Labels = "labels" ) +const ( + streamStdout = "stdout" + streamStderr = "stderr" +) + type Driver interface { Init(dataStore, ns, id string) error PreProcess(ctx context.Context, dataStore string, config *logging.Config) error @@ -59,6 +65,22 @@ type Driver interface { PostProcess() error } +// SyncDriver is an optional capability for a Driver whose log entries can be +// written synchronously and cheaply (e.g. to a local file). When a driver +// implements it, the logger writes each entry by calling WriteLogEntry directly +// from the goroutine that reads the container's stdio, rather than handing it to +// Process over a buffered channel. This makes a container's final output (in +// particular a trailing chunk with no newline) durable before containerd tears +// the logging process down on exit. Drivers that may block, such as +// network-backed ones, should not implement it so that the buffered channel +// keeps them from blocking the container. https://github.com/containerd/nerdctl/issues/5006 +type SyncDriver interface { + Driver + // WriteLogEntry writes a single log line for the given stream ("stdout" or + // "stderr"). It may be called concurrently for different streams. + WriteLogEntry(stream, line string) error +} + type DriverFactory func(map[string]string, string) (Driver, error) type LogOptsValidateFunc func(logOptMap map[string]string) error @@ -165,7 +187,15 @@ func WaitForLogger(dataStore, ns, id string) error { }) } -func getContainerWait(ctx context.Context, address string, config *logging.Config) (<-chan containerd.ExitStatus, error) { +// alreadyExited returns a channel that immediately reports an exit, used when +// we have determined that the container's task is already gone. +func alreadyExited() <-chan containerd.ExitStatus { + ch := make(chan containerd.ExitStatus, 1) + ch <- containerd.ExitStatus{} + return ch +} + +func getContainerWait(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) { client, err := containerd.New(strings.TrimPrefix(address, "unix://"), containerd.WithDefaultNamespace(config.Namespace)) if err != nil { return nil, err @@ -183,8 +213,19 @@ func getContainerWait(ctx context.Context, address string, config *logging.Confi return nil, err } - // If task was not found, it's possible that the container runtime is still being created. - // Retry every 100ms. + // The task was not found. containerd starts this logging process while + // setting up the container's IO, i.e. before the task is created, so a + // NotFound here usually just means the task has not been created yet: retry + // until it appears. + // + // However, for a short-lived container the task may instead have already + // exited and been removed before we ever observed it (this is more likely + // when this logger process is slow to start, e.g. under gomodjail). In that + // case the task will never appear and waiting for it would hang the logger + // forever, holding the logger lock and truncating the container's final + // output. Once we have seen the container produce output we therefore know + // it has run, so a still-missing task means it has already exited. + // https://github.com/containerd/nerdctl/issues/5006 ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() @@ -194,18 +235,20 @@ func getContainerWait(ctx context.Context, address string, config *logging.Confi return nil, errors.New("timed out waiting for container task to start") case <-ticker.C: task, err = con.Task(ctx, nil) - if err != nil { - if errdefs.IsNotFound(err) { - continue - } + if err == nil { + return task.Wait(ctx) + } + if !errdefs.IsNotFound(err) { return nil, err } - return task.Wait(ctx) + if outputSeen() { + return alreadyExited(), nil + } } } } -type ContainerWaitFunc func(ctx context.Context, address string, config *logging.Config) (<-chan containerd.ExitStatus, error) +type ContainerWaitFunc func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore, address string, getContainerWait ContainerWaitFunc, config *logging.Config) error { if err := driver.PreProcess(ctx, dataStore, config); err != nil { @@ -226,60 +269,122 @@ func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore, addres stderrR.Cancel() }() - // initialize goroutines to copy stdout and stderr streams to a closable pipe - pipeStdoutR, pipeStdoutW := io.Pipe() - pipeStderrR, pipeStderrW := io.Pipe() - copyStream := func(reader io.Reader, writer *io.PipeWriter) { - // copy using a buffer of size 32K - buf := make([]byte, 32<<10) - _, err := io.CopyBuffer(writer, reader, buf) - if err != nil { - log.G(ctx).Errorf("failed to copy stream: %s", err) - } - } - go copyStream(stdoutR, pipeStdoutW) - go copyStream(stderrR, pipeStderrW) + // copiedBytes counts how much container output has been read so far. It lets + // getContainerWait tell "the task has not been created yet" apart from "the + // task has already exited and been removed" when it sees a missing task. + var copiedBytes atomic.Int64 + outputSeen := func() bool { return copiedBytes.Load() > 0 } - var wg sync.WaitGroup - wg.Add(3) stdout := make(chan string, 10000) stderr := make(chan string, 10000) - processLogFunc := func(reader io.Reader, dataChan chan string) { - defer wg.Done() - defer close(dataChan) - r := bufio.NewReader(reader) - - var err error - for err == nil { - var s string - s, err = r.ReadString('\n') - if len(s) > 0 { - dataChan <- s + // If the driver can write synchronously, emit writes each log entry directly + // from the goroutine that reads the container's stdio. Otherwise it hands the + // entry to the driver's Process method over a buffered channel, which keeps a + // slow (e.g. network-backed) driver from blocking the container. + // + // The synchronous path matters because, when a container exits, containerd + // closes the stdio FIFOs and then tears the logging process down almost + // immediately. Handing the final chunk to another goroutine to write races + // that teardown and can lose a trailing chunk that has no newline; writing it + // inline does not. https://github.com/containerd/nerdctl/issues/5006 + syncDriver, isSync := driver.(SyncDriver) + emit := func(stream, line string) { + if isSync { + if err := syncDriver.WriteLogEntry(stream, line); err != nil { + log.G(ctx).WithError(err).Error("failed to write log entry") } + return + } + if stream == streamStdout { + stdout <- line + } else { + stderr <- line + } + } + + var wg sync.WaitGroup - if err != nil && err != io.EOF { - log.L.WithError(err).Error("failed to read log") + // processStream reads a container stdio FIFO directly and emits its output + // split into newline-terminated lines. Complete lines are emitted as they are + // read; a trailing fragment without a newline is buffered until more output + // arrives (so a long line is not split) and emitted when the stream ends. + processStream := func(stream string, reader io.Reader, dataChan chan string) { + defer wg.Done() + if !isSync { + defer close(dataChan) + } + buf := make([]byte, 32<<10) + var pending []byte + // emitLines emits each complete (newline-terminated) line, leaving any + // trailing fragment buffered in pending so that a single logical line is + // not split across log entries. + emitLines := func() { + for { + i := bytes.IndexByte(pending, '\n') + if i < 0 { + break + } + emit(stream, string(pending[:i+1])) + pending = pending[i+1:] + } + } + for { + nr, err := reader.Read(buf) + if nr > 0 { + copiedBytes.Add(int64(nr)) + pending = append(pending, buf[:nr]...) + emitLines() + // For a synchronous driver, emit a trailing fragment immediately + // instead of buffering it until the stream ends. The fragment is + // then written to the log before the container's abrupt teardown + // on exit can lose it; for a streaming (channel) driver this would + // only split lines, so it is left buffered there. + if isSync && len(pending) > 0 { + emit(stream, string(pending)) + pending = pending[:0] + } + } + if err != nil { + emitLines() + // The stream has ended: emit any final fragment that did not end + // in a newline. + if len(pending) > 0 { + emit(stream, string(pending)) + } + if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { + log.L.WithError(err).Error("failed to read log") + } + return } } } - go processLogFunc(pipeStdoutR, stdout) - go processLogFunc(pipeStderrR, stderr) - go func() { - defer wg.Done() - driver.Process(stdout, stderr) - }() + wg.Add(2) + go processStream(streamStdout, stdoutR, stdout) + go processStream(streamStderr, stderrR, stderr) + if !isSync { + wg.Add(1) + go func() { + defer wg.Done() + driver.Process(stdout, stderr) + }() + } go func() { - // close pipeStdoutW and pipeStderrW upon container exit - defer pipeStdoutW.Close() - defer pipeStderrW.Close() - - exitCh, err := getContainerWait(ctx, address, config) + // Wait for the container to exit, then cancel the readers. containerd + // keeps the stdio FIFO write ends open (so the container can be + // restarted), so the FIFOs may not reach EOF on exit; without this the + // read goroutines, and therefore the logger, could block forever. + exitCh, err := getContainerWait(ctx, address, config, outputSeen) if err != nil { + // We could not determine when the container exits. Do not cancel the + // readers: they will finish on their own when the FIFO reaches EOF. + // Cancelling here could truncate a still-running container. log.G(ctx).Errorf("failed to get container task wait channel: %v", err) return } <-exitCh + stdoutR.Cancel() + stderrR.Cancel() }() wg.Wait() return driver.PostProcess() diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go index da0d535b074..ecab183bebc 100644 --- a/pkg/logging/logging_test.go +++ b/pkg/logging/logging_test.go @@ -21,7 +21,9 @@ import ( "bytes" "context" "math/rand" + "os" "strings" + "sync" "testing" "time" @@ -58,6 +60,34 @@ func (m *MockDriver) PostProcess() error { return nil } +// SyncMockDriver implements SyncDriver, recording the entries written to it. +type SyncMockDriver struct { + mu sync.Mutex + receivedStdout []string + receivedStderr []string +} + +func (m *SyncMockDriver) Init(dataStore, ns, id string) error { return nil } +func (m *SyncMockDriver) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error { + return nil +} +func (m *SyncMockDriver) Process(stdout <-chan string, stderr <-chan string) error { + // Not used on the synchronous path (the logger calls WriteLogEntry instead), + // but must satisfy the Driver interface. + return nil +} +func (m *SyncMockDriver) PostProcess() error { return nil } +func (m *SyncMockDriver) WriteLogEntry(stream, line string) error { + m.mu.Lock() + defer m.mu.Unlock() + if stream == streamStdout { + m.receivedStdout = append(m.receivedStdout, line) + } else { + m.receivedStderr = append(m.receivedStderr, line) + } + return nil +} + func TestLoggingProcessAdapter(t *testing.T) { // Will process a normal String to stdout and a bigger one to stderr normalString := generateRandomString(1024) @@ -79,7 +109,7 @@ func TestLoggingProcessAdapter(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var getContainerWaitMock ContainerWaitFunc = func(ctx context.Context, address string, config *logging.Config) (<-chan containerd.ExitStatus, error) { + var getContainerWaitMock ContainerWaitFunc = func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) { exitChan := make(chan containerd.ExitStatus, 1) time.Sleep(50 * time.Millisecond) exitChan <- containerd.ExitStatus{} @@ -112,6 +142,104 @@ func TestLoggingProcessAdapter(t *testing.T) { } } +// TestLoggingProcessAdapterTrailingChunk verifies that the logger forwards all +// of the container's output, including a final chunk that has no trailing +// newline, rather than holding that chunk back until something closes the +// stream. The container's stdio FIFOs are modelled with os.Pipe; closing the +// write end models the container exiting and containerd closing the FIFO. +// Regression test for https://github.com/containerd/nerdctl/issues/5006 +func TestLoggingProcessAdapterTrailingChunk(t *testing.T) { + const expected = "'Hello World!\nThere is no newline'" + + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer stdoutR.Close() + stderrR, stderrW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer stderrR.Close() + + driver := &MockDriver{} + config := &logging.Config{ + Stdout: stdoutR, + Stderr: stderrR, + } + + // Write the container's output, including a trailing chunk without a newline, + // then close the write ends to model the container exiting. + if _, err := stdoutW.WriteString(expected); err != nil { + t.Fatal(err) + } + stdoutW.Close() + stderrW.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // getContainerWait never reports an exit here: completion is driven by the + // FIFOs reaching EOF, as it usually is in practice. + var getContainerWaitMock ContainerWaitFunc = func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) { + return make(chan containerd.ExitStatus), nil + } + + if err := loggingProcessAdapter(ctx, driver, "testDataStore", "", getContainerWaitMock, config); err != nil { + t.Fatal(err) + } + + if actual := strings.Join(driver.receivedStdout, ""); actual != expected { + t.Fatalf("stdout is %q, expected %q", actual, expected) + } +} + +// TestLoggingProcessAdapterSyncTrailingChunk verifies the same trailing-chunk +// behaviour for a driver that writes synchronously (SyncDriver), which is the +// path that protects the final chunk from the container's abrupt teardown. +// Regression test for https://github.com/containerd/nerdctl/issues/5006 +func TestLoggingProcessAdapterSyncTrailingChunk(t *testing.T) { + const expected = "'Hello World!\nThere is no newline'" + + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer stdoutR.Close() + stderrR, stderrW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer stderrR.Close() + + driver := &SyncMockDriver{} + config := &logging.Config{ + Stdout: stdoutR, + Stderr: stderrR, + } + + if _, err := stdoutW.WriteString(expected); err != nil { + t.Fatal(err) + } + stdoutW.Close() + stderrW.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var getContainerWaitMock ContainerWaitFunc = func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) { + return make(chan containerd.ExitStatus), nil + } + + if err := loggingProcessAdapter(ctx, driver, "testDataStore", "", getContainerWaitMock, config); err != nil { + t.Fatal(err) + } + + if actual := strings.Join(driver.receivedStdout, ""); actual != expected { + t.Fatalf("stdout is %q, expected %q", actual, expected) + } +} + // generateRandomString creates a random string of the given size. func generateRandomString(size int) string { characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" From 5fce8b48a06aad18944eb69494d710b9939acf5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:11:46 +0000 Subject: [PATCH 641/868] build(deps): bump actions/cache from 6.0.0 to 6.1.0 Bumps [actions/cache](https://github.com/actions/cache) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/2c8a9bd7457de244a408f35966fab2fb45fda9c8...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/job-test-in-lima-freebsd.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml index 5b382025fc1..f25478b4c58 100644 --- a/.github/workflows/job-test-in-lima-freebsd.yml +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -27,7 +27,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }}-freebsd diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 1df50af4921..8e8ce68adc8 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -41,7 +41,7 @@ jobs: id: lima-actions-setup - name: "Init: Cache" - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} From aee2b7374bf56c0d05486c051716f17bd00a7e2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:12:42 +0000 Subject: [PATCH 642/868] build(deps): bump actions/attest-build-provenance from 4.1.0 to 4.1.1 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32...0f67c3f4856b2e3261c31976d6725780e5e4c373) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2bf225108b..d92a6a5d104 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From bf17357d0e8d9994a487cad9cf24cf40090848f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:53:36 +0000 Subject: [PATCH 643/868] build(deps): bump github.com/docker/cli Bumps the docker group with 1 update in the / directory: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.6.0+incompatible to 29.6.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.6.0...v29.6.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.6.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f0675f109ab..3bdb8a97191 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.6.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.6.1+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 diff --git a/go.sum b/go.sum index fbecf7df5be..39c179940a8 100644 --- a/go.sum +++ b/go.sum @@ -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.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc= -github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= +github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= From 97ad2533222ea72f6640a41293f3693a50233f3d Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 28 Jun 2026 08:20:45 +0400 Subject: [PATCH 644/868] fix: return inspect format errors Signed-off-by: immanuwell --- cmd/nerdctl/container/container_inspect.go | 10 +------ cmd/nerdctl/image/image_inspect.go | 10 +------ cmd/nerdctl/inspect/inspect.go | 8 +---- pkg/cmd/network/inspect.go | 5 +--- pkg/formatter/common.go | 9 ++++++ pkg/formatter/formatter_test.go | 35 ++++++++++++++++++++++ 6 files changed, 48 insertions(+), 29 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect.go b/cmd/nerdctl/container/container_inspect.go index 78560b63c0e..99c113fda88 100644 --- a/cmd/nerdctl/container/container_inspect.go +++ b/cmd/nerdctl/container/container_inspect.go @@ -21,8 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" @@ -108,13 +106,7 @@ func inspectAction(cmd *cobra.Command, args []string) error { return err } - // Display - if len(entries) > 0 { - if formatErr := formatter.FormatSlice(opt.Format, opt.Stdout, entries); formatErr != nil { - log.G(ctx).Error(formatErr) - } - } - return err + return formatter.FormatInspectSlice(opt.Format, opt.Stdout, entries) } func containerInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/image/image_inspect.go b/cmd/nerdctl/image/image_inspect.go index 5dd7238a151..023f23f9650 100644 --- a/cmd/nerdctl/image/image_inspect.go +++ b/cmd/nerdctl/image/image_inspect.go @@ -21,8 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" @@ -110,13 +108,7 @@ func imageInspectAction(cmd *cobra.Command, args []string) error { return err } - // Display - if len(entries) > 0 { - if formatErr := formatter.FormatSlice(options.Format, options.Stdout, entries); formatErr != nil { - log.G(ctx).Error(formatErr) - } - } - return err + return formatter.FormatInspectSlice(options.Format, options.Stdout, entries) } func imageInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/inspect/inspect.go b/cmd/nerdctl/inspect/inspect.go index 0473f1bddc3..8bb09a53681 100644 --- a/cmd/nerdctl/inspect/inspect.go +++ b/cmd/nerdctl/inspect/inspect.go @@ -22,8 +22,6 @@ import ( "github.com/spf13/cobra" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" containercmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" @@ -176,11 +174,7 @@ func inspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("%d errors: %v", len(errs), errs) } - if formatErr := formatter.FormatSlice(format, cmd.OutOrStdout(), entries); formatErr != nil { - log.G(ctx).Error(formatErr) - } - - return nil + return formatter.FormatInspectSlice(format, cmd.OutOrStdout(), entries) } func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/pkg/cmd/network/inspect.go b/pkg/cmd/network/inspect.go index 6bacb75e445..236df20765c 100644 --- a/pkg/cmd/network/inspect.go +++ b/pkg/cmd/network/inspect.go @@ -98,10 +98,7 @@ func Inspect(ctx context.Context, client *containerd.Client, options types.Netwo } if len(result) > 0 { - if formatErr := formatter.FormatSlice(options.Format, options.Stdout, result); formatErr != nil { - log.G(ctx).Error(formatErr) - } - err = nil + err = formatter.FormatInspectSlice(options.Format, options.Stdout, result) } else { err = errors.New("unable to find any network matching the provided request") } diff --git a/pkg/formatter/common.go b/pkg/formatter/common.go index 18cd6e6ca45..e418dd7d8ef 100644 --- a/pkg/formatter/common.go +++ b/pkg/formatter/common.go @@ -78,6 +78,15 @@ func FormatSlice(format string, writer io.Writer, x []interface{}) error { return nil } +// FormatInspectSlice formats inspect results and propagates template errors back +// to the caller so CLI commands can fail with a non-zero exit code. +func FormatInspectSlice(format string, writer io.Writer, x []interface{}) error { + if len(x) == 0 { + return nil + } + return FormatSlice(format, writer, x) +} + func tryRawFormat(b *bytes.Buffer, f interface{}, tmpl *template.Template) error { m, err := json.MarshalIndent(f, "", " ") if err != nil { diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index dda59e80fe0..9243a293c48 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -17,6 +17,7 @@ package formatter import ( + "bytes" "testing" "time" @@ -237,3 +238,37 @@ func TestEllipsis(t *testing.T) { }) } } + +func TestFormatInspectSlice(t *testing.T) { + t.Parallel() + + t.Run("empty slice is ignored", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + err := FormatInspectSlice("{{.ID}}", &buf, nil) + assert.NilError(t, err) + assert.Equal(t, "", buf.String()) + }) + + t.Run("malformed template returns error", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + err := FormatInspectSlice("{{bad", &buf, []interface{}{ + map[string]string{"ID": "abc"}, + }) + assert.ErrorContains(t, err, "template") + }) + + t.Run("default format still works", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + err := FormatInspectSlice("", &buf, []interface{}{ + map[string]string{"ID": "abc"}, + }) + assert.NilError(t, err) + assert.Assert(t, buf.Len() > 0) + }) +} From 9f7761b03dbb28c2c5a7e629f3aa3fe4f899db70 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Sat, 27 Jun 2026 21:25:53 +0530 Subject: [PATCH 645/868] fix(network): match each ip-range to its subnet for dual-stack network create applied a single --ip-range to every subnet, so on a dual-stack network the IPv4 range was checked against the IPv6 subnet and creation failed with "no matching subnet". Accept --ip-range more than once and match each range to the subnet that contains it, the same way --gateway is handled. Signed-off-by: Mayur Das --- cmd/nerdctl/network/network_create.go | 6 +- .../network/network_create_linux_test.go | 35 +++++++++ pkg/api/types/network_types.go | 2 +- pkg/cmd/network/create.go | 2 +- pkg/netutil/netutil_unix.go | 72 ++++++++++++++----- pkg/netutil/netutil_unix_test.go | 54 ++++++++++++++ pkg/netutil/netutil_windows.go | 8 ++- 7 files changed, 156 insertions(+), 23 deletions(-) diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index 7d6c02cb123..f8b5dc85822 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -48,7 +48,7 @@ func createCommand() *cobra.Command { cmd.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options") cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") - cmd.Flags().String("ip-range", "", `Allocate container ip from a sub-range`) + cmd.Flags().StringArray("ip-range", nil, `Allocate container ip from a sub-range`) cmd.Flags().StringArray("label", nil, "Set metadata for a network") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") cmd.Flags().Bool("internal", false, "Restrict external access to the network") @@ -88,7 +88,7 @@ func createAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - ipRangeStr, err := cmd.Flags().GetString("ip-range") + ipRanges, err := cmd.Flags().GetStringArray("ip-range") if err != nil { return err } @@ -115,7 +115,7 @@ func createAction(cmd *cobra.Command, args []string) error { IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), Subnets: subnets, Gateway: gateways, - IPRange: ipRangeStr, + IPRange: ipRanges, Labels: labels, IPv6: ipv6, Internal: internal, diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index e43bbddf983..76992c85c47 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -144,6 +144,41 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "dual-stack with explicit ip-ranges", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + // Before the fix the IPv4 ip-range was checked against the IPv6 + // subnet and creation failed. + helpers.Ensure("network", "create", data.Identifier(), + "--ipv6", + "--subnet", "10.6.0.0/16", + "--subnet", "2001:db8:6::/64", + "--ip-range", "10.6.1.0/24", + "--ip-range", "2001:db8:6::/80", + ) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + netw := nerdtest.InspectNetwork(helpers, data.Identifier()) + ranges := map[string]string{} + for _, c := range netw.IPAM.Config { + ranges[c.Subnet] = c.IPRange + } + assert.Equal(t, ranges["10.6.0.0/16"], "10.6.1.0/24") + assert.Equal(t, ranges["2001:db8:6::/64"], "2001:db8:6::/80") + }, + } + }, + }, { Description: "internal enabled", Setup: func(data test.Data, helpers test.Helpers) { diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go index 17ca78a4cf6..a2df90ecf84 100644 --- a/pkg/api/types/network_types.go +++ b/pkg/api/types/network_types.go @@ -32,7 +32,7 @@ type NetworkCreateOptions struct { IPAMOptions map[string]string Subnets []string Gateway []string - IPRange string + IPRange []string Labels []string IPv6 bool Internal bool diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index 69f4b80a2c8..89c48cc0db9 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -28,7 +28,7 @@ import ( func Create(options types.NetworkCreateOptions, stdout io.Writer) error { if len(options.Subnets) == 0 { - if len(options.Gateway) > 0 || options.IPRange != "" { + if len(options.Gateway) > 0 || len(options.IPRange) > 0 { return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") } options.Subnets = []string{""} diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index 4a5d1c0c50f..a3cd38c6f93 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -215,7 +215,7 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": @@ -225,14 +225,15 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string {Dst: "0.0.0.0/0"}, } } - ranges, findIPv4, err := e.parseIPAMRanges(subnets, gateways, ipRangeStr, ipv6) + ranges, findIPv4, err := e.parseIPAMRanges(subnets, gateways, ipRanges, ipv6) if err != nil { return nil, err } ipamConf.Ranges = append(ipamConf.Ranges, ranges...) if !findIPv4 { - // The default IPv4 range uses a computed gateway; pass none. - ranges, _, _ = e.parseIPAMRanges([]string{""}, nil, ipRangeStr, ipv6) + // The default IPv4 range uses a computed gateway and no ip-range; + // any user-supplied gateway or ip-range belongs to an explicit subnet. + ranges, _, _ = e.parseIPAMRanges([]string{""}, nil, nil, ipv6) ipamConf.Ranges = append(ipamConf.Ranges, ranges...) } ipamConfig = ipamConf @@ -289,7 +290,25 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string return ipam, nil } -func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRange string, ipv6 bool) ([][]IPAMRange, bool, error) { +func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRanges []string, ipv6 bool) ([][]IPAMRange, bool, error) { + // Resolve every requested subnet first; parseSubnet also rejects overlaps + // with existing networks. The pairing below then works purely on the parsed + // CIDRs, so it can be unit-tested without probing the host's networks. + parsedSubnets := make([]*net.IPNet, len(subnets)) + for i := range subnets { + subnet, err := e.parseSubnet(subnets[i]) + if err != nil { + return nil, false, err + } + parsedSubnets[i] = subnet + } + return pairIPAMRanges(parsedSubnets, gateways, ipRanges, ipv6) +} + +// pairIPAMRanges matches each gateway and ip-range to the subnet that contains +// it and builds the per-subnet IPAM ranges. It is split out from subnet +// resolution so the matching can be tested without touching live networks. +func pairIPAMRanges(subnets []*net.IPNet, gateways []string, ipRanges []string, ipv6 bool) ([][]IPAMRange, bool, error) { // Parse the gateways once up front; matching them to subnets below is then // just a containment check, with no parse error mixed into the loop. parsedGateways := make([]net.IP, len(gateways)) @@ -300,15 +319,22 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRange st } parsedGateways[i] = gw } + // Parse the ip-ranges the same way, keyed by network so each can be matched + // to the subnet that contains it. + parsedRanges := make([]*net.IPNet, len(ipRanges)) + for i, r := range ipRanges { + _, ipNet, err := net.ParseCIDR(r) + if err != nil { + return nil, false, fmt.Errorf("failed to parse ip-range %q", r) + } + parsedRanges[i] = ipNet + } findIPv4 := false ranges := make([][]IPAMRange, 0, len(subnets)) - used := make([]bool, len(gateways)) - for i := range subnets { - subnet, err := e.parseSubnet(subnets[i]) - if err != nil { - return nil, findIPv4, err - } + usedGateways := make([]bool, len(gateways)) + usedRanges := make([]bool, len(ipRanges)) + for _, subnet := range subnets { // if ipv6 flag is not set, subnets of ipv6 should be excluded if !ipv6 && subnet.IP.To4() == nil { continue @@ -320,8 +346,17 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRange st // the v4 gateway to the v4 subnet and v6 to v6. gateway := "" for j, gw := range parsedGateways { - if !used[j] && subnet.Contains(gw) { - gateway, used[j] = gateways[j], true + if !usedGateways[j] && subnet.Contains(gw) { + gateway, usedGateways[j] = gateways[j], true + break + } + } + // Pair the subnet with the ip-range it contains, the same way, so a + // dual-stack network does not check the v4 range against the v6 subnet. + ipRange := "" + for j, r := range parsedRanges { + if !usedRanges[j] && subnet.Contains(r.IP) { + ipRange, usedRanges[j] = ipRanges[j], true break } } @@ -331,13 +366,18 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRange st } ranges = append(ranges, []IPAMRange{*ipamRange}) } - // Only known after every subnet is seen: a gateway that matched none is a - // user error, same as Docker. - for j, ok := range used { + // Only known after every subnet is seen: a gateway or ip-range that matched + // none is a user error, same as Docker. + for j, ok := range usedGateways { if !ok { return nil, findIPv4, fmt.Errorf("no matching subnet for gateway %q", gateways[j]) } } + for j, ok := range usedRanges { + if !ok { + return nil, findIPv4, fmt.Errorf("no matching subnet for ip-range %q", ipRanges[j]) + } + } return ranges, findIPv4, nil } diff --git a/pkg/netutil/netutil_unix_test.go b/pkg/netutil/netutil_unix_test.go index 5a2d66d4451..142a3e36b71 100644 --- a/pkg/netutil/netutil_unix_test.go +++ b/pkg/netutil/netutil_unix_test.go @@ -19,6 +19,7 @@ package netutil import ( + "net" "testing" "github.com/Masterminds/semver/v3" @@ -69,3 +70,56 @@ func TestGuessFirewallPluginVersion(t *testing.T) { } } } + +// TestPairIPAMRangesIPRange covers matching repeatable --ip-range values to the +// subnet that contains each, and the errors for an unmatched or malformed range. +func TestPairIPAMRangesIPRange(t *testing.T) { + t.Parallel() + // parse turns the CIDR strings into the already-resolved subnets that + // pairIPAMRanges takes, keeping each subtest readable. + parse := func(t *testing.T, cidrs ...string) []*net.IPNet { + t.Helper() + subnets := make([]*net.IPNet, len(cidrs)) + for i, c := range cidrs { + _, n, err := net.ParseCIDR(c) + assert.NilError(t, err) + subnets[i] = n + } + return subnets + } + + t.Run("each ip-range pairs with its subnet regardless of order", func(t *testing.T) { + subnets := parse(t, "10.6.0.0/16", "2001:db8:6::/64") + // Given v6-first to prove the pairing is by containment, not by index. + ipRanges := []string{"2001:db8:6::/80", "10.6.1.0/24"} + ranges, findIPv4, err := pairIPAMRanges(subnets, nil, ipRanges, true) + assert.NilError(t, err) + assert.Equal(t, true, findIPv4) + got := map[string]string{} + for _, r := range ranges { + got[r[0].Subnet] = r[0].IPRange + } + assert.Equal(t, "10.6.1.0/24", got["10.6.0.0/16"]) + assert.Equal(t, "2001:db8:6::/80", got["2001:db8:6::/64"]) + }) + + t.Run("an ip-range matching no subnet errors", func(t *testing.T) { + _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"192.168.1.0/24"}, false) + assert.ErrorContains(t, err, `no matching subnet for ip-range "192.168.1.0/24"`) + }) + + t.Run("an IPv4 ip-range with only an IPv6 subnet errors", func(t *testing.T) { + _, _, err := pairIPAMRanges(parse(t, "2001:db8:6::/64"), nil, []string{"10.6.1.0/24"}, true) + assert.ErrorContains(t, err, `no matching subnet for ip-range "10.6.1.0/24"`) + }) + + t.Run("a second ip-range claiming the same subnet errors", func(t *testing.T) { + _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"10.6.1.0/24", "10.6.2.0/24"}, false) + assert.ErrorContains(t, err, `no matching subnet for ip-range "10.6.2.0/24"`) + }) + + t.Run("a malformed ip-range errors", func(t *testing.T) { + _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"bogus"}, false) + assert.ErrorContains(t, err, `failed to parse ip-range "bogus"`) + }) +} diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 71d9a76ce6c..5d204c4161d 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -72,18 +72,22 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRangeStr string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { switch driver { case "default": default: return nil, fmt.Errorf("unsupported ipam driver %q", driver) } - // Windows is single-subnet, so use at most one gateway. + // Windows is single-subnet, so use at most one gateway and one ip-range. gatewayStr := "" if len(gateways) > 0 { gatewayStr = gateways[0] } + ipRangeStr := "" + if len(ipRanges) > 0 { + ipRangeStr = ipRanges[0] + } ipamConfig := newWindowsIPAMConfig() subnet, err := e.parseSubnet(subnets[0]) From 9701e4388fd609496fb8d7187bfd374c584ce493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:32:45 +0000 Subject: [PATCH 646/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.12.1 to 2.13.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.12.1...v2.13.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3bdb8a97191..40e4740cc47 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.2 - github.com/compose-spec/compose-go/v2 v2.12.1 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.13.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 39c179940a8..ece66433486 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.12.1 h1:+xBZNxcgSus4atQJwXPEdhHRgCEyZmj/BuqN5m33Ou0= -github.com/compose-spec/compose-go/v2 v2.12.1/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE= +github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= From 80d71866b397a17e481a0df7b12ed06004eb8335 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:48:48 +0000 Subject: [PATCH 647/868] build(deps): bump github.com/Microsoft/hcsshim Bumps [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) from 0.15.0-rc.2 to 0.15.0-rc.3. - [Release notes](https://github.com/Microsoft/hcsshim/releases) - [Commits](https://github.com/Microsoft/hcsshim/compare/v0.15.0-rc.2...v0.15.0-rc.3) --- updated-dependencies: - dependency-name: github.com/Microsoft/hcsshim dependency-version: 0.15.0-rc.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 40e4740cc47..0ce1252fda1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.26.3 require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 - github.com/Microsoft/hcsshim v0.15.0-rc.2 + github.com/Microsoft/hcsshim v0.15.0-rc.3 github.com/compose-spec/compose-go/v2 v2.13.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined diff --git a/go.sum b/go.sum index ece66433486..a339395ac38 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/Microsoft/hcsshim v0.15.0-rc.2 h1:/3Izdm/kGtHjqC0wdwepupLaz+U+xIjA1i03RM3v/SA= -github.com/Microsoft/hcsshim v0.15.0-rc.2/go.mod h1:HICB5JfsB/rThwBKw9LiTd40H79lXbp/hzRgLGNLJLs= +github.com/Microsoft/hcsshim v0.15.0-rc.3 h1:ZTNzOp0QwJ1EiL3zopSOawIG0j7zAvzJx0rBmcR6HJ0= +github.com/Microsoft/hcsshim v0.15.0-rc.3/go.mod h1:VhDiwXgb8cEJxO9H57YL4NNIYqvZKpqvSDcimLyo7m8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= From 80c53a04823049ed7a7efe8908da2f54a94dad6a Mon Sep 17 00:00:00 2001 From: immanuwell Date: Tue, 30 Jun 2026 11:47:18 +0400 Subject: [PATCH 648/868] fix: hide internal alias flags from help output Signed-off-by: immanuwell --- cmd/nerdctl/compose/compose_help_test.go | 62 +++++++++++++++++++++ cmd/nerdctl/helpers/cobra.go | 39 +++++++++++++ cmd/nerdctl/main_test.go | 70 ++++++++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 cmd/nerdctl/compose/compose_help_test.go diff --git a/cmd/nerdctl/compose/compose_help_test.go b/cmd/nerdctl/compose/compose_help_test.go new file mode 100644 index 00000000000..23481d28c58 --- /dev/null +++ b/cmd/nerdctl/compose/compose_help_test.go @@ -0,0 +1,62 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "bytes" + "strings" + "testing" +) + +func TestComposeHelpHidesAliasImplementationFlags(t *testing.T) { + cmd := Command() + + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"--help"}) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + out := stdout.String() + if strings.Contains(out, "-f, --f") { + t.Fatalf("help output unexpectedly contains alias implementation flag\n%s", out) + } + expected := "--file stringArray Specify an alternate compose file (aliases: -f)" + if !strings.Contains(out, expected) { + t.Fatalf("help output missing %q\n%s", expected, out) + } +} + +func TestComposeHiddenFileAliasStillParses(t *testing.T) { + cmd := Command() + + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"-f", "compose.yaml", "--help"}) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + if got := cmd.Flag("file").Value.String(); got != "[compose.yaml]" { + t.Fatalf("file flag = %q, want %q", got, "[compose.yaml]") + } +} diff --git a/cmd/nerdctl/helpers/cobra.go b/cmd/nerdctl/helpers/cobra.go index 8ee5baeb260..c0194fd885e 100644 --- a/cmd/nerdctl/helpers/cobra.go +++ b/cmd/nerdctl/helpers/cobra.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" "github.com/spf13/cobra" @@ -29,6 +30,28 @@ import ( "github.com/containerd/log" ) +func formatAliasLabels(aliasGroups ...[]string) []string { + var labels []string + for _, aliases := range aliasGroups { + for _, alias := range aliases { + prefix := "--" + if len(alias) == 1 { + prefix = "-" + } + labels = append(labels, prefix+alias) + } + } + return labels +} + +func appendAliasesUsage(usage string, aliasGroups ...[]string) string { + labels := formatAliasLabels(aliasGroups...) + if len(labels) == 0 { + return usage + } + return fmt.Sprintf("%s (aliases: %s)", usage, strings.Join(labels, ", ")) +} + // UnknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail // https://github.com/containerd/nerdctl/issues/487 // @@ -74,6 +97,7 @@ func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value stri if envV, ok := os.LookupEnv(env); ok { value = envV } + usage = appendAliasesUsage(usage, aliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(string) flags := cmd.Flags() @@ -85,6 +109,7 @@ func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value stri } else { flags.StringVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) } } @@ -100,6 +125,7 @@ func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, en } value = int(v) } + usage = appendAliasesUsage(usage, aliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(int) flags := cmd.Flags() @@ -111,6 +137,7 @@ func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, en } else { flags.IntVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) } } @@ -126,6 +153,7 @@ func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value ti log.L.WithError(err).Warnf("Invalid duration value for `%s`", env) } } + usage = appendAliasesUsage(usage, aliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(time.Duration) flags := cmd.Flags() @@ -137,6 +165,7 @@ func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value ti } else { flags.DurationVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) } } @@ -176,6 +205,7 @@ func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonP if envV, ok := os.LookupEnv(env); ok { value = []string{envV} } + usage = appendAliasesUsage(usage, aliases, nonPersistentAliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new([]string) flags := cmd.Flags() @@ -186,6 +216,7 @@ func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonP } else { flags.StringArrayVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) } persistentFlags := cmd.PersistentFlags() @@ -197,6 +228,7 @@ func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonP } else { persistentFlags.StringArrayVar(p, a, value, aliasesUsage) } + persistentFlags.MarkHidden(a) } } @@ -209,6 +241,7 @@ func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAlia if envV, ok := os.LookupEnv(env); ok { value = envV } + usage = appendAliasesUsage(usage, aliases, localAliases, persistentAliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(string) @@ -222,6 +255,7 @@ func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAlia } else { flags.StringVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) // non-persistent flags are not added to the InheritedFlags, so we should add them manually f := flags.Lookup(a) aliasToBeInherited.AddFlag(f) @@ -236,6 +270,7 @@ func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAlia } else { localFlags.StringVar(p, a, value, aliasesUsage) } + localFlags.MarkHidden(a) } // persistentFlags cannot redefine alias already used in subcommands @@ -248,6 +283,7 @@ func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAlia } else { persistentFlags.StringVar(p, a, value, aliasesUsage) } + persistentFlags.MarkHidden(a) } } @@ -264,6 +300,7 @@ func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersiste log.L.WithError(err).Warnf("Invalid boolean value for `%s`", env) } } + usage = appendAliasesUsage(usage, aliases, nonPersistentAliases) aliasesUsage := fmt.Sprintf("Alias of --%s", name) p := new(bool) flags := cmd.Flags() @@ -274,6 +311,7 @@ func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersiste } else { flags.BoolVar(p, a, value, aliasesUsage) } + flags.MarkHidden(a) } persistentFlags := cmd.PersistentFlags() @@ -285,6 +323,7 @@ func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersiste } else { persistentFlags.BoolVar(p, a, value, aliasesUsage) } + persistentFlags.MarkHidden(a) } } diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index bcd84434556..71bc19d1db5 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -17,7 +17,9 @@ package main import ( + "bytes" "errors" + "strings" "testing" "github.com/containerd/containerd/v2/defaults" @@ -130,3 +132,71 @@ version = 2`), testCase.Run(t) } + +func TestRootHelpHidesAliasImplementationFlags(t *testing.T) { + app, err := newApp() + if err != nil { + t.Fatal(err) + } + + var stdout bytes.Buffer + app.SetOut(&stdout) + app.SetErr(&stdout) + app.SetArgs([]string{"--help"}) + + if err := app.Execute(); err != nil { + t.Fatal(err) + } + + out := stdout.String() + for _, unexpected := range []string{ + "-a, --a", + "-H, --H", + "-n, --n", + } { + if strings.Contains(out, unexpected) { + t.Fatalf("help output unexpectedly contains %q\n%s", unexpected, out) + } + } + for _, expected := range []string{ + "--address string containerd address, optionally with \"unix://\" prefix [$CONTAINERD_ADDRESS] (aliases: -a, -H, --host)", + "--namespace string containerd namespace, such as \"moby\" for Docker, \"k8s.io\" for Kubernetes [$CONTAINERD_NAMESPACE] (aliases: -n)", + } { + if !strings.Contains(out, expected) { + t.Fatalf("help output missing %q\n%s", expected, out) + } + } +} + +func TestRootHiddenAliasesStillParse(t *testing.T) { + app, err := newApp() + if err != nil { + t.Fatal(err) + } + + var stdout bytes.Buffer + app.SetOut(&stdout) + app.SetErr(&stdout) + app.SetArgs([]string{ + "-a", "unix:///tmp/a.sock", + "-H", "unix:///tmp/h.sock", + "--host", "unix:///tmp/host.sock", + "-n", "testns", + "--storage-driver", "native", + "--help", + }) + + if err := app.Execute(); err != nil { + t.Fatal(err) + } + + if got := app.Flag("address").Value.String(); got != "unix:///tmp/host.sock" { + t.Fatalf("address flag = %q, want %q", got, "unix:///tmp/host.sock") + } + if got := app.Flag("namespace").Value.String(); got != "testns" { + t.Fatalf("namespace flag = %q, want %q", got, "testns") + } + if got := app.Flag("snapshotter").Value.String(); got != "native" { + t.Fatalf("snapshotter flag = %q, want %q", got, "native") + } +} From a84b909d6e799b352d79c4c34956f3d59a2f1195 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 30 Jun 2026 02:14:45 +0900 Subject: [PATCH 649/868] CI: update Docker (29.6.1) Assisted-by: Claude Opus 4.8 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- cmd/nerdctl/builder/builder_build_test.go | 11 +++++-- .../container_run_mount_linux_test.go | 4 +-- cmd/nerdctl/image/image_inspect_test.go | 5 ++- cmd/nerdctl/image/image_list_test.go | 9 +++++- .../system/system_events_linux_test.go | 17 ++++++++-- docs/command-reference.md | 3 +- pkg/mountutil/mountutil_linux.go | 22 +++++++++++++ pkg/mountutil/mountutil_linux_test.go | 32 +++++++++++++++++++ 9 files changed, 95 insertions(+), 10 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 2afcd149afb..9fda60e7c26 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -155,7 +155,7 @@ jobs: no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.1 - docker-version: 5:28.0.4-1~ubuntu.24.04~noble + docker-version: 5:29.6.1-1~ubuntu.24.04~noble # 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 diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index a8d9b9fb163..06676463e7b 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -666,8 +666,15 @@ CMD ["echo", "nerdctl-build-test-string"] // XXX FIXME helpers.Capture("build", data.Temp().Path()) }, - Command: test.Command("images"), - Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("")), + Command: test.Command("images", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // TODO: follow Docker v29 behavior (change to ) https://github.com/containerd/nerdctl/issues/5027 + noTag := "" + if nerdtest.IsDocker() { + noTag = "" + } + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(noTag))(data, helpers) + }, } testCase.Run(t) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index 083ddb47adf..359e8234f75 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -443,7 +443,7 @@ func TestRunMountBindMode(t *testing.T) { base.Cmd("run", "--rm", - "--mount", fmt.Sprintf("type=bind,bind-nonrecursive,src=%s,target=/mnt1", tmpDir1), + "--mount", fmt.Sprintf("type=bind,bind-recursive=disabled,src=%s,target=/mnt1", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { @@ -459,7 +459,7 @@ func TestRunMountBindMode(t *testing.T) { base.Cmd("run", "--rm", - "--mount", fmt.Sprintf("type=bind,bind-nonrecursive=false,src=%s,target=/mnt1", tmpDir1), + "--mount", fmt.Sprintf("type=bind,bind-recursive=enabled,src=%s,target=/mnt1", tmpDir1), testutil.AlpineImage, "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", ).AssertOutWithFunc(func(stdout string) error { diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 68124c53d34..4298102d394 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -68,7 +68,10 @@ func TestImageInspectSimpleCases(t *testing.T) { }, { Description: "Config.Image field is set", - Command: test.Command("image", "inspect", testutil.CommonImage), + // Config.Image is no longer populated since Docker v28.2 + // https://github.com/moby/moby/pull/48457 + Require: require.Not(nerdtest.Docker), + Command: test.Command("image", "inspect", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 228b3a08d1a..c204fb95852 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -315,7 +315,14 @@ CMD ["echo", "nerdctl-build-notag-string"] { Description: "dangling", Command: test.Command("images", "--filter", "dangling=true"), - Expected: test.Expects(0, nil, expect.Contains("")), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // TODO: follow Docker v29 behavior (change to ) https://github.com/containerd/nerdctl/issues/5027 + dangling := "" + if nerdtest.IsDocker() { + dangling = "" + } + return test.Expects(0, nil, expect.Contains(dangling))(data, helpers) + }, }, { Description: "not dangling", diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index 431abdafb0b..d63a450057a 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -28,6 +28,19 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) +// startEventOutput returns the substring expected in the JSON output of a +// container "start" event. Docker v29 dropped the legacy top-level "status" +// field from the events API, exposing only "Action", whereas nerdctl still +// emits a "Status" field. +// https://github.com/moby/moby/pull/50832 +// https://github.com/containerd/nerdctl/issues/5028 +func startEventOutput() string { + if nerdtest.IsDocker() { + return "\"Action\":\"start\"" + } + return "tatus\":\"start\"" +} + func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { helpers.Ensure("pull", testutil.CommonImage) cmd := helpers.Command("events", "--filter", data.Labels().Get("filter"), "--format", "json") @@ -68,7 +81,7 @@ func TestEventFilters(t *testing.T) { }, Data: test.WithLabels(map[string]string{ "filter": "event=start", - "output": "tatus\":\"start\"", + "output": startEventOutput(), }), }, { @@ -97,7 +110,7 @@ func TestEventFilters(t *testing.T) { }, Data: test.WithLabels(map[string]string{ "filter": "status=start", - "output": "tatus\":\"start\"", + "output": startEventOutput(), }), }, { diff --git a/docs/command-reference.md b/docs/command-reference.md index 55813aad960..3eb172bae10 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -306,7 +306,8 @@ Volume flags: - :nerd_face: `rro`: mount the filesystem recursively read-only. - Options specific to `bind`: - :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). - - :whale: `bind-nonrecursive`: `true` or `false`(default). If set to true, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. + - :whale: `bind-recursive`: `enabled`(default) or `disabled`. If set to `disabled`, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. + - :whale: `bind-nonrecursive`: `true` or `false`(default). Deprecated alias for `bind-recursive=disabled` / `bind-recursive=enabled`. If set to true, submounts are not recursively bind-mounted. - unimplemented options: `consistency` - Options specific to `tmpfs`: - :whale: `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default. diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 9793216f11a..b40cf9750ec 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -336,6 +336,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e rwOption = key continue case "bind-nonrecursive": + // Removed in Docker v29, in favor of `bind-recursive=disabled` https://github.com/docker/cli/pull/6241 bindNonRecursive = true continue } @@ -374,10 +375,31 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e // parseVolumeOptions will do that. bindPropagation = value case "bind-nonrecursive": + // Removed in Docker v29, in favor of `bind-recursive=disabled` https://github.com/docker/cli/pull/6241 bindNonRecursive, err = strconv.ParseBool(value) if err != nil { return nil, fmt.Errorf("invalid value for %s: %s", key, value) } + case "bind-recursive": + // bind-recursive is the Docker option that supersedes bind-nonrecursive. + valS := value + // Allow boolean as an alias to "enabled" or "disabled" + if b, err := strconv.ParseBool(valS); err == nil { + if b { + valS = "enabled" + } else { + valS = "disabled" + } + } + switch valS { + case "enabled": + bindNonRecursive = false + case "disabled": + bindNonRecursive = true + default: + // TODO: support "writable", "readonly" + return nil, fmt.Errorf("invalid value for %s: %s (must be \"enabled\" or \"disabled\")", key, value) + } case "tmpfs-size": tmpfsSize, err = units.RAMInBytes(value) if err != nil { diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 982a9b7ea09..f4209bc5aae 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -18,6 +18,7 @@ package mountutil import ( "context" + "slices" "strings" "testing" @@ -415,3 +416,34 @@ func TestProcessFlagMountRW(t *testing.T) { }) } } + +// TestProcessFlagMountBindRecursive verifies that the Docker `bind-recursive` +// option (which supersedes the deprecated `bind-nonrecursive`) is honored and +// maps to non-recursive (bind) or recursive (rbind) mounts. +func TestProcessFlagMountBindRecursive(t *testing.T) { + src := t.TempDir() + + accepted := []struct { + spec string + wantBindOpt string + }{ + {"type=bind,source=" + src + ",target=/bar,bind-recursive=disabled", "bind"}, + {"type=bind,source=" + src + ",target=/bar,bind-recursive=enabled", "rbind"}, + {"type=bind,source=" + src + ",target=/bar,bind-recursive=false", "bind"}, + {"type=bind,source=" + src + ",target=/bar,bind-recursive=1", "rbind"}, + // The deprecated bind-nonrecursive option keeps working. + {"type=bind,source=" + src + ",target=/bar,bind-nonrecursive", "bind"}, + {"type=bind,source=" + src + ",target=/bar,bind-nonrecursive=false", "rbind"}, + } + for _, tt := range accepted { + t.Run(tt.spec, func(t *testing.T) { + got, err := ProcessFlagMount(tt.spec, nil) + assert.NilError(t, err) + assert.Assert(t, slices.Contains(got.Mount.Options, tt.wantBindOpt), + "expected option %q in %v", tt.wantBindOpt, got.Mount.Options) + }) + } + + _, err := ProcessFlagMount("type=bind,source="+src+",target=/bar,bind-recursive=bogus", nil) + assert.ErrorContains(t, err, "invalid value for bind-recursive") +} From 48867a80e55a1edc3582d6a945186a3585b3bc1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:32:44 +0000 Subject: [PATCH 650/868] build(deps): bump github.com/klauspost/compress from 1.18.6 to 1.18.7 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.6 to 1.18.7. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.6...v1.18.7) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0ce1252fda1..dd7ce278d53 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.1 - github.com/klauspost/compress v1.18.6 + github.com/klauspost/compress v1.18.7 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined github.com/moby/moby/client v0.5.0 github.com/moby/moby/v2 v2.0.0-beta.18 diff --git a/go.sum b/go.sum index a339395ac38..a92a46371b9 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ 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/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 421dfdeab60b1dc5a179115920cd7604c81773ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:32:12 +0000 Subject: [PATCH 651/868] build(deps): bump docker/build-push-action from 7.2.0 to 7.3.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.2.0 to 7.3.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/f9f3042f7e2789586610d6e8b85c8f03e5195baf...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 8e554fd72d8..b03695fa95c 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -63,7 +63,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64 From 687347bcbb191e02a2fe963ec84a1921935da541 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:32:17 +0000 Subject: [PATCH 652/868] build(deps): bump docker/setup-qemu-action from 4.1.0 to 4.2.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/06116385d9baf250c9f4dcb4858b16962ea869c3...96fe6ef7f33517b61c61be40b68a1882f3264fb8) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 8e554fd72d8..0d684476579 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -37,7 +37,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d92a6a5d104..02113c68336 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: "Install go" uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: From 7c60de36c2651e0c0c2e42d4a2a8e2b569bddd26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:32:37 +0000 Subject: [PATCH 653/868] build(deps): bump github.com/klauspost/compress from 1.18.7 to 1.19.0 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.7 to 1.19.0. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.18.7...v1.19.0) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dd7ce278d53..a403f282139 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.1 - github.com/klauspost/compress v1.18.7 + github.com/klauspost/compress v1.19.0 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined github.com/moby/moby/client v0.5.0 github.com/moby/moby/v2 v2.0.0-beta.18 diff --git a/go.sum b/go.sum index a92a46371b9..29df109e3e2 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ 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/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 287372c2645f1257a0051ce90323f68fd6b30e28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:32:29 +0000 Subject: [PATCH 654/868] build(deps): bump docker/metadata-action from 6.1.0 to 6.2.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9...dc802804100637a589fabce1cb79ff13a1411302) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 8e554fd72d8..7f4532a4908 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -56,7 +56,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} From 12e609da5339a6e4c4173849203719ac9cce5d61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:32:31 +0000 Subject: [PATCH 655/868] build(deps): bump docker/login-action from 4.2.0 to 4.3.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...c99871dec2022cc055c062a10cc1a1310835ceb4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 8e554fd72d8..2cadb2b72fa 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -46,7 +46,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 07127a853c7d2e9dfb9500e8ec202fc8c3ea0cff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:32:36 +0000 Subject: [PATCH 656/868] build(deps): bump docker/setup-buildx-action from 4.1.0 to 4.2.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 8e554fd72d8..2d1274a365b 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -40,7 +40,7 @@ jobs: uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action From 0683124129fa39185750f7962718ed11149fd802 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 22:32:20 +0000 Subject: [PATCH 657/868] build(deps): bump docker/login-action from 4.3.0 to 4.4.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/c99871dec2022cc055c062a10cc1a1310835ceb4...af1e73f918a031802d376d3c8bbc3fe56130a9b0) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 914dfd660db..c8e897784cf 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -46,7 +46,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From f64005926175f7497364084292f04792842c2361 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 02:13:12 +0900 Subject: [PATCH 658/868] rootless: fix containerd startup when the host has run a rootful containerd v2 Rootful containerd creates /run/nri on the host (NRI is enabled by default since containerd v2.0). RootlessKit's copy-up of /run turns that directory into a symlink pointing back at the root-owned original, inside which the rootless containerd cannot bind its own NRI socket: failed to create socket "/var/run/nri/nri.sock": listen unix /var/run/nri/nri.sock: bind: permission denied Remove the symlink in the child namespace, like the existing /run/containerd one, so that a fresh, writable directory gets created on the copy-up tmpfs instead. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- extras/rootless/containerd-rootless.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh index b992b5744be..bfa7acc4578 100755 --- a/extras/rootless/containerd-rootless.sh +++ b/extras/rootless/containerd-rootless.sh @@ -161,7 +161,7 @@ else # Remove the *symlinks* for the existing files in the parent namespace if any, # so that we can create our own files in our mount namespace. # The actual files in the parent namespace are *not removed* by this rm command. - rm -f /run/containerd /run/xtables.lock \ + rm -f /run/containerd /run/nri /run/xtables.lock \ /var/lib/containerd /var/lib/cni /etc/containerd # Bind-mount /etc/ssl. From 18e6645b27059ce0a27058e87a701dc151802cce Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 06:30:50 +0900 Subject: [PATCH 659/868] test-integration.sh: do not print the output of the non-failing tests gotestsum was invoked with the "testname" format, which it silently upgrades to "github-actions" when GITHUB_ACTIONS=true (with no option to disable the upgrade), printing the output of every test: the logs were too large to be rendered by the GitHub Actions web UI. Use the "pkgname-and-test-fails" format by default: it prints a single line per package, plus the output of the failing tests (which is also repeated in the "=== Failed" summary at the end of the run), and is not subjected to the "github-actions" upgrade. Set GOTESTSUM_FORMAT to override the format. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- hack/test-integration.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hack/test-integration.sh b/hack/test-integration.sh index cdbeb61957f..470ac567ef5 100755 --- a/hack/test-integration.sh +++ b/hack/test-integration.sh @@ -30,8 +30,16 @@ readonly timeout="30m" readonly retries="2" readonly needsudo="${WITH_SUDO:-}" +# Do not print the output of the non-failing tests: the full logs are too large to be +# rendered by the GitHub Actions web UI. The "pkgname-and-test-fails" format prints a +# single line per package, plus the output of the failing tests (which is repeated in +# the "=== Failed" summary at the end of the run). +# Note that the "testname" format must be avoided in the CI: gotestsum silently upgrades +# it to "github-actions" when GITHUB_ACTIONS=true, printing the output of every test. +# Set GOTESTSUM_FORMAT to override the format. +# # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization -args=(--format=testname --jsonfile /tmp/test-integration.log --packages="$root"/../cmd/nerdctl/...) +args=(--format="${GOTESTSUM_FORMAT:-pkgname-and-test-fails}" --jsonfile /tmp/test-integration.log --packages="$root"/../cmd/nerdctl/...) # FIXME: not working on windows. Need to change approach: move away from --post-run-command and # just process the log file. This might also allow multi-steps/multi-target results aggregation. [ "$(uname -s)" != "Linux" ] || args+=(--post-run-command "$root"/github/gotestsum-reporter.sh) From 7655a251ae8cf013b6761568f2507728a8f9bb52 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 05:27:19 +0900 Subject: [PATCH 660/868] test: fix the SELinux tests TestRunSelinux, TestRunSelinuxWithSecurityOpt, and TestRunSelinuxWithVolumeLabel had never actually run in the CI, as the containerized test environment did not enable SELinux. Now that the integration tests run directly on Enterprise Linux Lima guests, they run for the first time, revealing that: - the image argument was missing from the `nerdctl run` command lines ("sleep" was interpreted as the image name, failing to pull) - the volume directory was created at "/", which is not writable in rootless mode: create it under the test temporary directory instead - a volume relabeled with :Z gets container_file_t, which does not contain the expected "container_t" substring; `ls -Z` also needs -d to print the label of the (empty) directory itself Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- .../container/container_run_security_linux_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index 3f0a6e0c891..55a1492e0ae 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -250,7 +250,7 @@ func TestRunSelinuxWithSecurityOpt(t *testing.T) { { Description: "test run with selinux-enabled", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, "sleep", "infinity") + return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", testContainer) @@ -283,7 +283,7 @@ func TestRunSelinux(t *testing.T) { { Description: "test run with selinux-enabled", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, "sleep", "infinity") + return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", testContainer) @@ -317,21 +317,23 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { { Description: "test run with selinux-enabled", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("/%s:/%s:Z", testContainer, testContainer), "--name", testContainer, "sleep", "infinity") + // The volume directory must live somewhere writable by the (possibly + // rootless) user running the tests: nerdctl creates it on `run`. + hostDir := data.Temp().Path("volume") + return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("%s:/mnt:Z", hostDir), "--name", testContainer, testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", testContainer) - os.RemoveAll(fmt.Sprintf("/%s", testContainer)) }, 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) { - cmd := exec.Command("ls", "-Z", fmt.Sprintf("/%s", testContainer)) + cmd := exec.Command("ls", "-dZ", data.Temp().Path("volume")) lsStdout, err := cmd.CombinedOutput() assert.NilError(t, err) - assert.Equal(t, strings.Contains(string(lsStdout), "container_t"), true) + assert.Equal(t, strings.Contains(string(lsStdout), "container_file_t"), true) }, ), } From d8b7940dfad3ff3a87f493f6cef25fb349791f84 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 17:05:52 +0900 Subject: [PATCH 661/868] test: TestBuildFromStdin: do not assume direct export to the image store The test asserted that the image tag appears on stderr, which only happens when BuildKit exports the build result directly to the containerd image store ("naming to docker.io/library/..."). When nerdctl determines that the image is not sharable between the BuildKit worker and the client (eg: on EL 8 rootless, where the BuildKit worker uses the fuse-overlayfs snapshotter while the client uses the default overlayfs), it falls back to loading a tarball, and the tag is printed on stdout ("Loaded image: ...") instead. Assert the actual outcome instead: the image built from the Dockerfile fed on stdin exists and runs its CMD. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- cmd/nerdctl/builder/builder_build_test.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 06676463e7b..84a0d17f860 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -17,7 +17,6 @@ package builder import ( - "errors" "fmt" "os" "path/filepath" @@ -247,19 +246,23 @@ CMD ["echo", "nerdctl-build-test-stdin"]`, testutil.CommonImage) testCase := &test.Case{ Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "-", ".") + cmd.Feed(strings.NewReader(dockerfile)) + cmd.Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, + // Run the image to prove that the build consumed the Dockerfile fed on stdin. + // Note: do not assert on the tag appearing in the build output: it is only + // printed on stderr ("naming to ...") when BuildKit exports directly to the + // containerd image store, not when nerdctl falls back to loading a tarball + // (eg: when the BuildKit worker snapshotter does not match the client one). Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "-", ".") - cmd.Feed(strings.NewReader(dockerfile)) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Errors: []error{errors.New(data.Identifier())}, - } + return helpers.Command("run", "--rm", data.Identifier()) }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-stdin\n")), } testCase.Run(t) From e59ffa650de48f6676432c29b401251879f2f97a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 01:15:56 +0900 Subject: [PATCH 662/868] test: adapt to running directly on hosts TestHostNetworkDnsPreserved: GitHub Actions hosts run systemd-resolved, so /etc/resolv.conf only contains the 127.0.0.53 stub. For host-network containers, nerdctl deliberately provides the resolv.conf that systemd-resolved generates with the actual upstream nameservers (see pkg/resolvconf.Path()), while Docker keeps the stub. Capture the nameservers accordingly in the test setup. This could not happen while the tests ran inside a Docker container with a Docker-generated resolv.conf. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- .../container_run_network_linux_test.go | 29 ++++++++++++++----- cmd/nerdctl/network/network_inspect_test.go | 4 ++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 13d94a2cab0..25c62db3733 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -964,14 +964,27 @@ func TestHostNetworkDnsPreserved(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { // In some rootless CI job, slirp provides 10.0.2.3 as DNS server. // We cannot simply parse host /etc/resolv.conf here. - helpers.Command("run", "--rm", - "-v", "/etc/resolv.conf:/mnt/resolv.conf:ro", - testutil.AlpineImage, - "grep", "-E", "^nameserver\\s+", "/mnt/resolv.conf").Run(&test.Expected{ - Output: func(stdout string, t tig.T) { - data.Labels().Set("nameservers", stdout) - }, - }) + captureNameservers := func(resolvConfPath string) string { + var nameservers string + helpers.Command("run", "--rm", + "-v", resolvConfPath+":/mnt/resolv.conf:ro", + testutil.AlpineImage, + "grep", "-E", "^nameserver\\s+", "/mnt/resolv.conf").Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + nameservers = stdout + }, + }) + return nameservers + } + nameservers := captureNameservers("/etc/resolv.conf") + // Mirror pkg/resolvconf.Path(): when 127.0.0.53 is the only nameserver, the host + // runs systemd-resolved, and nerdctl uses the resolv.conf that systemd-resolved + // generates with the actual upstream nameservers. + // Docker, on the other hand, keeps the stub for host-network containers. + if !nerdtest.IsDocker() && strings.TrimSpace(nameservers) == "nameserver 127.0.0.53" { + nameservers = captureNameservers("/run/systemd/resolve/resolv.conf") + } + data.Labels().Set("nameservers", nameservers) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 84e5e6f9dcb..8811bec39bf 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -500,7 +500,9 @@ func TestNetworkInspectDualStack(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", "--ipv6", - "--subnet", "10.1.0.0/24", + // Not 10.1.0.0/24: the eth0 of the GitHub Actions runners lives in + // 10.1.0.0/20, which the subnet overlap check rejects + "--subnet", "10.24.0.0/24", "--subnet", "fd00::/64", data.Identifier("test-dual-stack")) From f21b32bb49bc013ce34117caddf35a09b3f0d748 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 2 Jul 2026 22:22:17 +0900 Subject: [PATCH 663/868] CI: run test-integration[-rootless] directly on hosts and Lima guests The integration tests are no longer wrapped inside a Docker container. Docker is still used to build the test dependencies (the new `out-test-integration-artifacts` Dockerfile stage), which are then installed on the host with hack/provisioning/linux/test-integration-env.sh, and the tests now run with `go test` via hack/test-integration.sh: - job-test-in-container.yml is removed; its matrix is migrated to job-test-in-host.yml - job-test-in-lima.yml no longer nests Docker inside the guest VM (issue 3858); the artifacts are built on the host and installed in the guest, which is started in plain mode - the test-integration* Dockerfile stages are removed, along with the unused build-minimal stage (the demo stage is retained) - hack/test-integration-rootless.sh (moved from Dockerfile.d) runs the tests inside the systemd user session of the unprivileged user, which is available without a login, as the provisioning script enables lingering for that user Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- .github/workflows/job-test-in-container.yml | 210 -------------- .github/workflows/job-test-in-host.yml | 236 +++++++++------ .github/workflows/job-test-in-lima.yml | 101 ++++--- .github/workflows/workflow-flaky.yml | 1 + .github/workflows/workflow-test.yml | 23 +- Dockerfile | 125 ++------ ...containerd.service.d_port-slirp4netns.conf | 3 - ...-integration-buildkit-nerdctl-test.service | 2 +- .../test-integration-ipfs-offline.service | 2 +- Dockerfile.d/test-integration-rootless.sh | 76 ----- .../test-integration-soci-snapshotter.service | 2 +- docs/testing/README.md | 20 +- hack/build-integration-canary.sh | 2 +- hack/provisioning/README.md | 3 +- .../linux/test-integration-env.sh | 271 ++++++++++++++++++ hack/test-integration-rootless.sh | 135 +++++++++ 16 files changed, 668 insertions(+), 544 deletions(-) delete mode 100644 .github/workflows/job-test-in-container.yml delete mode 100644 Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf delete mode 100755 Dockerfile.d/test-integration-rootless.sh create mode 100755 hack/provisioning/linux/test-integration-env.sh create mode 100755 hack/test-integration-rootless.sh diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml deleted file mode 100644 index 76789ef09c7..00000000000 --- a/.github/workflows/job-test-in-container.yml +++ /dev/null @@ -1,210 +0,0 @@ -# This job runs integration tests inside a container, for all supported variants (ipv6, canary, etc) -# Note that it is linux and nerdctl (+/- gomodjail) only. -name: job-test-in-container - -on: - workflow_call: - inputs: - timeout: - required: true - type: number - runner: - required: true - type: string - canary: - required: false - default: false - type: boolean - target: - required: false - default: '' - type: string - binary: - required: false - default: nerdctl - type: string - containerd-version: - required: false - default: '' - type: string - rootlesskit-version: - required: false - default: '' - type: string - ipv6: - required: false - default: false - type: boolean - skip-flaky: - required: false - default: false - type: boolean - -env: - GOTOOLCHAIN: local - -jobs: - test: - name: | - ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} - ${{ inputs.target }} - ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} - ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} - ${{ inputs.ipv6 && ' (ipv6)' || '' }} - ${{ inputs.canary && ' (canary)' || '' }} - ${{ inputs.containerd-version && format(' (ctd: {0})', inputs.containerd-version) || '' }} - ${{ inputs.rootlesskit-version && format(' (rlk: {0})', inputs.rootlesskit-version) || '' }} - timeout-minutes: ${{ inputs.timeout }} - runs-on: ${{ inputs.runner }} - defaults: - run: - shell: bash - - env: - # https://github.com/containerd/nerdctl/issues/622 - # The only case when rootlesskit-version is force-specified is when we downgrade explicitly to v1 - WORKAROUND_ISSUE_622: ${{ inputs.rootlesskit-version }} - - steps: - - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 1 - persist-credentials: false - - - name: "Init: expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 - - - name: "Init: install br-netfilter" - run: | - # This ensures that bridged traffic goes through netfilter - sudo modprobe br-netfilter - - name: "Init: register QEMU (tonistiigi/binfmt)" - run: | - # `--install all` will only install emulation for architectures that cannot be natively executed - # Since some arm64 platforms do provide native fallback execution for 32 bits, - # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. - # To avoid that, we explicitly list the architectures we do want emulation for. - docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 - docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - if: ${{ inputs.canary }} - name: "Init (canary): prepare updated test image" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - . ./hack/build-integration-canary.sh - canary::build::integration - - if: ${{ ! inputs.canary }} - name: "Init: prepare test image" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - INPUTS_CONTAINERD_VERSION: ${{ inputs.containerd-version }} - INPUTS_TARGET: ${{ inputs.target }} - INPUTS_ROOTLESSKIT_VERSION: ${{ inputs.rootlesskit-version }} - run: | - buildargs=() - # If the runner is old, use old ubuntu inside the container as well - [ "${{ contains(inputs.runner, '22.04') }}" != "true" ] || buildargs=(--build-arg UBUNTU_VERSION=22.04) - # Honor if we want old containerd - [ "${INPUTS_CONTAINERD_VERSION}" == "" ] || buildargs+=(--build-arg CONTAINERD_VERSION=${INPUTS_CONTAINERD_VERSION}) - # Honor custom targets and if we want old rootlesskit - target=test-integration - if [ "${INPUTS_TARGET}" != "rootful" ]; then - target+=-${INPUTS_TARGET} - if [ "${INPUTS_ROOTLESSKIT_VERSION}" != "" ]; then - buildargs+=(--build-arg ROOTLESSKIT_VERSION=${INPUTS_ROOTLESSKIT_VERSION}) - fi - fi - # Cache is sharded per-architecture - arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} - docker buildx create --name with-gha --use - docker buildx build \ - --secret id=github_token,env=GITHUB_TOKEN \ - --output=type=docker \ - --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ - -t "$target" --target "$target" \ - "${buildargs[@]}" \ - . - # Rootful needs to disable snap - - if: ${{ inputs.target == 'rootful' }} - name: "Init: remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" - run: | - sudo systemctl disable --now snapd.service snapd.socket - sudo apt-get purge -qq snapd - sudo losetup -Dv - sudo losetup -lv - # Rootless on modern ubuntu wants apparmor - - if: ${{ inputs.target != 'rootful' && ! contains(inputs.runner, '22.04') }} - name: "Init: prepare apparmor for rootless + ubuntu 24+" - run: | - cat <, - include - /usr/local/bin/rootlesskit flags=(unconfined) { - userns, - # Site-specific additions and overrides. See local/README for details. - include if exists - } - EOT - sudo systemctl restart apparmor.service - # ipv6 wants... ipv6 - - if: ${{ inputs.ipv6 }} - name: "Init: ipv6" - run: | - # Enable ipv4 and ipv6 forwarding - sudo sysctl -w net.ipv6.conf.all.forwarding=1 - sudo sysctl -w net.ipv4.ip_forward=1 - # Enable IPv6 for Docker, and configure docker to use containerd for gha - sudo mkdir -p /etc/docker - echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "ip6tables": true}' | sudo tee /etc/docker/daemon.json - - name: "Init: enable Docker experimental features" - run: | - sudo mkdir -p /etc/docker - if [ -f /etc/docker/daemon.json ]; then - tmpfile="$(sudo mktemp)" - sudo jq '.experimental = true' /etc/docker/daemon.json | sudo tee "$tmpfile" >/dev/null - sudo mv "$tmpfile" /etc/docker/daemon.json - else - echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json >/dev/null - fi - sudo systemctl restart docker - - name: "Run: integration tests" - run: | - . ./hack/github/action-helpers.sh - github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" - - # IPV6 note: nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config. - # Therefore, it's hard to debug why the IPv6 tests fail in such an isolation layer. - # On the other side, using the host network is easier at configuration. - # Besides, each job is running on a different instance, which means using host network here - # is safe and has no side effects on others. - [ "${INPUTS_TARGET}" == "rootful" ] \ - && args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \ - || args=(test-integration-${INPUTS_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh) - if [ "${{ inputs.ipv6 }}" == true ]; then - docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${INPUTS_BINARY} - else - docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${INPUTS_BINARY} - fi - env: - INPUTS_TARGET: ${{ inputs.target }} - INPUTS_BINARY: ${{ inputs.binary }} - # FIXME: this NEEDS to go away - - name: "Run: integration tests (flaky)" - if: ${{ !fromJSON(inputs.skip-flaky) }} - run: | - . ./hack/github/action-helpers.sh - github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" - - [ "${INPUTS_TARGET}" == "rootful" ] \ - && args=(test-integration ./hack/test-integration.sh) \ - || args=(test-integration-${INPUTS_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh) - if [ "${{ inputs.ipv6 }}" == true ]; then - docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${INPUTS_BINARY} - else - docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${INPUTS_BINARY} - fi - env: - INPUTS_TARGET: ${{ inputs.target }} - INPUTS_BINARY: ${{ inputs.binary }} diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 4d12106dab0..bd4e29e352b 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -1,5 +1,10 @@ -# This currently test docker and nerdctl on windows (w/o canary) -# Structure is in to allow testing nerdctl on linux as well, though more work is required to make it functional. +# This job runs integration tests directly on the host, with `go test`. +# It covers: +# - windows (w/o canary) +# - docker on linux +# - nerdctl on linux, for all supported variants (rootful, rootless, ipv6, canary, etc.) +# On linux, Docker is only used to build the test dependencies (see the +# `out-test-integration-artifacts` Dockerfile stage), and is disabled before the tests start. name: job-test-in-host on: @@ -19,39 +24,50 @@ on: required: false default: false type: boolean + # Leave empty for windows and docker. Set to rootful, rootless, or + # rootless-port-slirp4netns to test nerdctl on a linux host. + target: + required: false + default: '' + type: string binary: required: false default: nerdctl type: string + containerd-version: + required: false + default: '' + type: string + rootlesskit-version: + required: false + default: '' + type: string + ipv6: + required: false + default: false + type: boolean + skip-flaky: + required: false + default: false + type: boolean go-version: required: true type: string docker-version: - required: true + required: false + default: '' type: string windows-containerd-version: - required: true + required: false + default: '' type: string windows-containerd-sha: - required: true - type: string - linux-containerd-version: - required: true - type: string - linux-containerd-sha: - required: true - type: string - linux-containerd-service-sha: - required: true + required: false + default: '' type: string windows-cni-version: - required: true - type: string - linux-cni-version: - required: true - type: string - linux-cni-sha: - required: true + required: false + default: '' type: string env: @@ -61,10 +77,14 @@ jobs: test: name: | ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} + ${{ inputs.target }} ${{ contains(inputs.runner, 'ubuntu') && ' linux' || ' windows' }} ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} + ${{ inputs.ipv6 && ' (ipv6)' || '' }} ${{ inputs.canary && ' (canary)' || '' }} + ${{ inputs.containerd-version && format(' (ctd: {0})', inputs.containerd-version) || '' }} + ${{ inputs.rootlesskit-version && format(' (rlk: {0})', inputs.rootlesskit-version) || '' }} timeout-minutes: ${{ inputs.timeout }} runs-on: "${{ inputs.runner }}" defaults: @@ -74,12 +94,13 @@ jobs: env: SHOULD_RUN: "yes" GO_VERSION: ${{ inputs.go-version }} - # Both Docker and nerdctl on linux need rootful right now - WITH_SUDO: ${{ contains(inputs.runner, 'ubuntu') }} + # Docker on linux needs rootful. So does nerdctl, unless the target is rootless. + WITH_SUDO: ${{ contains(inputs.runner, 'ubuntu') && !startsWith(inputs.target, 'rootless') }} WINDOWS_CONTAINERD_VERSION: ${{ inputs.windows-containerd-version }} WINDOWS_CONTAINERD_SHA: ${{ inputs.windows-containerd-sha }} - LINUX_CONTAINERD_VERSION: ${{ inputs.linux-containerd-version }} - LINUX_CONTAINERD_SHA: ${{ inputs.linux-containerd-sha }} + # https://github.com/containerd/nerdctl/issues/622 + # The only case when rootlesskit-version is force-specified is when we downgrade explicitly to v1 + WORKAROUND_ISSUE_622: ${{ inputs.rootlesskit-version }} steps: - name: "Init: checkout" @@ -107,18 +128,22 @@ jobs: if [[ "${INPUTS_RUNNER}" == *windows* ]]; then containerd_version="$WINDOWS_CONTAINERD_VERSION" - else - containerd_version="$LINUX_CONTAINERD_VERSION" - fi - [ "${latest_containerd:1}" == "$containerd_version" ] || { - if [[ "${INPUTS_RUNNER}" == *windows* ]]; then + [ "${latest_containerd:1}" == "$containerd_version" ] || { printf "WINDOWS_CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" printf "WINDOWS_CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" - else - printf "LINUX_CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" - printf "LINUX_CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" - fi - } + } + else + # On linux, containerd is built from source, as part of the test artifacts + containerd_version="$(grep -m1 '^ARG CONTAINERD_VERSION=' Dockerfile | cut -d= -f2)" + containerd_version="${containerd_version%%@*}" + containerd_version="${containerd_version:1}" + [ "${latest_containerd:1}" == "$containerd_version" ] || \ + printf "CANARY_CONTAINERD_VERSION=%s\n" "$latest_containerd" >> "$GITHUB_ENV" + # The golang docker image tag lags behind the golang releases + latest_go_hub="$(. ./hack/build-integration-canary.sh; canary::golang::hublatest)" + [ "$latest_go_hub" == "" ] || \ + printf "CANARY_GO_VERSION=%s\n" "$latest_go_hub" >> "$GITHUB_ENV" + fi if [ "$latest_go" == "" ] && [ "${latest_containerd:1}" == "$containerd_version" ]; then echo "::warning title=No canary::There is currently no canary versions to test. Steps will not run."; printf "SHOULD_RUN=no\n" >> "$GITHUB_ENV" @@ -134,52 +159,35 @@ jobs: # XXX RUNNER_OS and generally env is too unreliable # - if: ${{ env.RUNNER_OS == 'Linux' }} - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} - name: "Init (linux): prepare host" + name: "Init (linux): register QEMU (tonistiigi/binfmt)" run: | - if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then - echo "::group:: configure cdi and experimental for docker" - sudo mkdir -p /etc/docker - sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json - echo "::endgroup::" - echo "::group:: downgrade docker to the specific version we want to test (${INPUTS_DOCKER_VERSION})" - sudo apt-get update -qq - sudo apt-get install -qq ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo cp ./hack/provisioning/gpg/docker /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ - | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update -qq - sudo apt-get install -qq --allow-downgrades docker-ce=${INPUTS_DOCKER_VERSION} docker-ce-cli=${INPUTS_DOCKER_VERSION} - sudo systemctl restart docker - echo "::endgroup::" - else - # FIXME: this is missing runc (see top level workflow note about the state of this) - echo "::group:: install dependencies" - sudo ./hack/provisioning/linux/containerd.sh uninstall - ./hack/provisioning/linux/containerd.sh rootful "$LINUX_CONTAINERD_VERSION" "amd64" "$LINUX_CONTAINERD_SHA" "${INPUTS_LINUX_CONTAINERD_SERVICE_SHA}" - sudo ./hack/provisioning/linux/cni.sh uninstall - ./hack/provisioning/linux/cni.sh install "${INPUTS_LINUX_CNI_VERSION}" "amd64" "${INPUTS_LINUX_CNI_SHA}" - echo "::endgroup::" - - echo "::group:: build nerctl" - go install ./cmd/nerdctl - echo "$HOME/go/bin" >> "$GITHUB_PATH" - # Since tests are going to run root, we need nerdctl to be in a PATH that will survive `sudo` - sudo cp "$(which nerdctl)" /usr/local/bin - echo "::endgroup::" - fi - - # Register QEMU (tonistiigi/binfmt) # `--install all` will only install emulation for architectures that cannot be natively executed # Since some arm64 platforms do provide native fallback execution for 32 bits, # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`. # To avoid that, we explicitly list the architectures we do want emulation for. - echo "::group:: install binfmt" docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/amd64 docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm/v7 + + - if: ${{ contains(inputs.runner, 'ubuntu') && inputs.target == '' && env.SHOULD_RUN == 'yes' }} + name: "Init (linux docker): prepare host" + run: | + echo "::group:: configure cdi and experimental for docker" + sudo mkdir -p /etc/docker + sudo jq -n '.features.cdi = true | .experimental = true' | sudo tee /etc/docker/daemon.json + echo "::endgroup::" + echo "::group:: downgrade docker to the specific version we want to test (${INPUTS_DOCKER_VERSION})" + sudo apt-get update -qq + sudo apt-get install -qq ca-certificates curl + sudo install -m 0755 -d /etc/apt/keyrings + sudo cp ./hack/provisioning/gpg/docker /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt-get update -qq + sudo apt-get install -qq --allow-downgrades docker-ce=${INPUTS_DOCKER_VERSION} docker-ce-cli=${INPUTS_DOCKER_VERSION} + sudo systemctl restart docker echo "::endgroup::" # FIXME: remove expect when we are done removing unbuffer from tests @@ -192,9 +200,6 @@ jobs: sudo modprobe br-netfilter env: INPUTS_DOCKER_VERSION: ${{ inputs.docker-version }} - INPUTS_LINUX_CONTAINERD_SERVICE_SHA: ${{ inputs.linux-containerd-service-sha }} - INPUTS_LINUX_CNI_VERSION: ${{ inputs.linux-cni-version }} - INPUTS_LINUX_CNI_SHA: ${{ inputs.linux-cni-sha }} - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} name: "Init (windows): prepare host" @@ -220,15 +225,65 @@ jobs: choco install jq + # Rootful needs to disable snap + - if: ${{ inputs.target == 'rootful' && env.SHOULD_RUN == 'yes' }} + name: "Init (rootful): remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" + run: | + sudo systemctl disable --now snapd.service snapd.socket + sudo apt-get purge -qq snapd + sudo losetup -Dv + sudo losetup -lv + + # ipv6 wants... ipv6 + - if: ${{ inputs.ipv6 && env.SHOULD_RUN == 'yes' }} + name: "Init (ipv6): enable ipv4 and ipv6 forwarding" + run: | + sudo sysctl -w net.ipv6.conf.all.forwarding=1 + sudo sysctl -w net.ipv4.ip_forward=1 + + - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} + name: "Init (linux): Set up Docker Buildx" + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides + # the GitHub runtime token and cache url to BuildKit by itself. + # The cache is sharded per-architecture; empty build-args lines are ignored, and + # the canary containerd version (if any) takes precedence over the input. + - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} + name: "Init (linux): build test artifacts" + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + # push is false by default; stated explicitly for zizmor's cache-poisoning audit + push: false + target: out-test-integration-artifacts + outputs: type=local,dest=/tmp/nerdctl-test-artifacts + cache-from: type=gha,scope=test-integration-dependencies-${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} + build-args: | + ${{ (env.CANARY_CONTAINERD_VERSION && format('CONTAINERD_VERSION={0}', env.CANARY_CONTAINERD_VERSION)) || (inputs.containerd-version && format('CONTAINERD_VERSION={0}', inputs.containerd-version)) || '' }} + ${{ inputs.rootlesskit-version && format('ROOTLESSKIT_VERSION={0}', inputs.rootlesskit-version) || '' }} + ${{ env.CANARY_GO_VERSION && format('GO_VERSION={0}', env.CANARY_GO_VERSION) || '' }} + + # Note that Docker cannot be used anymore past this point. + - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} + name: "Init (linux): provision the host" + env: + INPUTS_TARGET: ${{ inputs.target }} + run: | + sudo env GITHUB_ACTIONS="$GITHUB_ACTIONS" ./hack/provisioning/linux/test-integration-env.sh install /tmp/nerdctl-test-artifacts "${INPUTS_TARGET}" + - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install dev tools" run: | echo "::group:: make install-dev-tools" make install-dev-tools + [ "$(uname -s)" != "Linux" ] || echo "$HOME/go/bin" >> "$GITHUB_PATH" echo "::endgroup::" - # ipv6 is tested only on linux - - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} + # ipv6 is tested only on linux. For nerdctl, this is done through the ipv6 input instead. + - if: ${{ contains(inputs.runner, 'ubuntu') && inputs.target == '' && env.SHOULD_RUN == 'yes' }} name: "Run (linux): integration tests (IPv6)" run: | . ./hack/github/action-helpers.sh @@ -244,17 +299,36 @@ jobs: . ./hack/github/action-helpers.sh github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" - ./hack/test-integration.sh -test.target=${INPUTS_BINARY} -test.only-flaky=false + args=(-test.target=${INPUTS_BINARY} -test.only-flaky=false) + [ "${INPUTS_IPV6}" != "true" ] || args+=(-test.only-ipv6) + [ "${INPUTS_TARGET}" != "rootful" ] || args+=(-test.allow-modify-users=true) + if [[ "${INPUTS_TARGET}" == rootless* ]]; then + ./hack/test-integration-rootless.sh ./hack/test-integration.sh "${args[@]}" + else + ./hack/test-integration.sh "${args[@]}" + fi env: INPUTS_BINARY: ${{ inputs.binary }} + INPUTS_TARGET: ${{ inputs.target }} + INPUTS_IPV6: ${{ inputs.ipv6 }} + CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER: ${{ inputs.target == 'rootless-port-slirp4netns' && 'slirp4netns' || '' }} # FIXME: this must go - - if: ${{ env.SHOULD_RUN == 'yes' }} + - if: ${{ env.SHOULD_RUN == 'yes' && !fromJSON(inputs.skip-flaky) }} name: "Run: integration tests (flaky)" run: | . ./hack/github/action-helpers.sh github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" - ./hack/test-integration.sh -test.target=${INPUTS_BINARY} -test.only-flaky=true + args=(-test.target=${INPUTS_BINARY} -test.only-flaky=true) + [ "${INPUTS_IPV6}" != "true" ] || args+=(-test.only-ipv6) + if [[ "${INPUTS_TARGET}" == rootless* ]]; then + ./hack/test-integration-rootless.sh ./hack/test-integration.sh "${args[@]}" + else + ./hack/test-integration.sh "${args[@]}" + fi env: INPUTS_BINARY: ${{ inputs.binary }} + INPUTS_TARGET: ${{ inputs.target }} + INPUTS_IPV6: ${{ inputs.ipv6 }} + CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER: ${{ inputs.target == 'rootless-port-slirp4netns' && 'slirp4netns' || '' }} diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 8e8ce68adc8..50ad41903fe 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -1,4 +1,6 @@ # Currently, Lima job test only for EL, though in the future it could be used to also test FreeBSD or other linux-es +# The test artifacts are built on the host with Docker, then installed inside the guest VM, +# where the integration tests run directly with `go test` (no Docker inside the VM). name: job-test-in-lima on: @@ -16,6 +18,9 @@ on: guest: required: true type: string + go-version: + required: true + type: string skip-flaky: required: false default: false @@ -29,6 +34,7 @@ jobs: env: TARGET: ${{ inputs.target }} GUEST: ${{ inputs.guest }} + GO_VERSION: ${{ inputs.go-version }} steps: - name: "Init: checkout" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -49,84 +55,75 @@ jobs: - name: "Init: start the guest VM" run: | set -eux - # containerd=none is set because the built-in containerd support conflicts with Docker + # --plain disables the mounts, the port forwards, containerd, and the Lima boot + # scripts: just a plain VM with ssh. The provisioning and test scripts recreate + # the environment that the boot scripts would have set up (kernel modules, + # sysctls, CONTAINERD_SNAPSHOTTER, ...). limactl start \ --name=default \ + --plain \ --cpus=4 \ --memory=12 \ - --containerd=none \ - --set '.mounts=null | .portForwards=[{"guestSocket":"/var/run/docker.sock","hostSocket":"{{.Dir}}/sock/docker.sock"}]' \ template://${GUEST} - # FIXME: the tests should be directly executed in the VM without nesting Docker inside it - # https://github.com/containerd/nerdctl/issues/3858 - - name: "Init: install dockerd in the guest VM" + - name: "Init: Set up Docker Buildx" + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides the + # GitHub runtime token and cache url to BuildKit by itself. + - name: "Init: build test artifacts (on the host, with Docker)" + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + # push is false by default; stated explicitly for zizmor's cache-poisoning audit + push: false + target: out-test-integration-artifacts + outputs: type=local,dest=/tmp/nerdctl-test-artifacts + cache-from: type=gha,scope=test-integration-dependencies-amd64 + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} + + - name: "Init: copy source and artifacts into the guest VM" run: | set -eux - lima sudo mkdir -p /etc/systemd/system/docker.socket.d - cat <<-EOF | lima sudo tee /etc/systemd/system/docker.socket.d/override.conf - [Socket] - SocketUser=$(whoami) - EOF - lima sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo - lima sudo dnf -q -y install docker-ce --nobest - lima sudo systemctl enable --now docker + limactl copy -r "$PWD" default:nerdctl + limactl copy -r /tmp/nerdctl-test-artifacts default:nerdctl-test-artifacts + # Run all the subsequent lima commands from the copied source + echo "LIMA_WORKDIR=$(lima pwd)/nerdctl" >>"$GITHUB_ENV" - - name: "Init: configure the host to use dockerd in the guest VM" + - name: "Init: install go in the guest VM" run: | set -eux - sudo systemctl disable --now docker.service docker.socket - export DOCKER_HOST="unix://$(limactl ls --format '{{.Dir}}/sock/docker.sock' default)" - echo "DOCKER_HOST=${DOCKER_HOST}" >>$GITHUB_ENV - docker info - docker version + lima bash -c 'command -v tar' || lima sudo dnf install -q -y tar + gover="$(curl -fsSL --proto '=https' --tlsv1.2 'https://go.dev/dl/?mode=json&include=all' | jq -r --arg prefix "go${GO_VERSION}." '[.[].version | select(startswith($prefix))] | first')" + [ "$gover" != "null" ] + curl -fsSL --proto '=https' --tlsv1.2 "https://go.dev/dl/${gover}.linux-amd64.tar.gz" | lima sudo tar -xzf- -C /usr/local - - name: "Init: install br-netfilter in the guest VM" + - name: "Init: provision the guest VM" run: | - lima sudo modprobe br-netfilter - - - name: "Init: expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 + set -eux + lima sudo env GITHUB_ACTIONS=true ./hack/provisioning/linux/test-integration-env.sh install ../nerdctl-test-artifacts "$TARGET" - - name: "Init: prepare integration tests" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Init: install dev tools in the guest VM" run: | set -eux + lima bash -c 'PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" make install-dev-tools' - sudo losetup -Dv - sudo losetup -lv - - [ "$TARGET" = "rootless" ] && TARGET=test-integration-rootless || TARGET=test-integration - docker buildx create --name with-gha --use - docker buildx build \ - --secret id=github_token,env=GITHUB_TOKEN \ - --output=type=docker \ - --cache-from type=gha,scope=test-integration-dependencies-amd64 \ - -t test-integration --target "${TARGET}" \ - . - - - name: "Run integration tests" - # Presumably, something is broken with the way docker exposes /dev to the container, as it appears to only - # randomly work. Mounting /dev does workaround the issue. - # This might be due to the old kernel shipped with Alma (4.18), or something else between centos/docker. + - name: "Run: integration tests" run: | set -eux if [ "$TARGET" = "rootless" ]; then - echo "rootless" - docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false + lima bash -c 'export GITHUB_ACTIONS=true PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" && ./hack/test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false' else - echo "rootful" - docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false + lima bash -c 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" WITH_SUDO=true && ./hack/test-integration.sh -test.only-flaky=false' fi + - name: "Run: integration tests (flaky)" if: ${{ !fromJSON(inputs.skip-flaky) }} run: | set -eux if [ "$TARGET" = "rootless" ]; then - echo "rootless" - docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true + lima bash -c 'export GITHUB_ACTIONS=true PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" && ./hack/test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true' else - echo "rootful" - docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true + lima bash -c 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" WITH_SUDO=true && ./hack/test-integration.sh -test.only-flaky=true' fi diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 5d507c9d281..2bbf64fe5b1 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -32,6 +32,7 @@ jobs: runner: ubuntu-24.04 guest: ${{ matrix.guest }} target: ${{ matrix.target }} + go-version: 1.26 skip-flaky: true # skip the most flaky ones for now test-integration-freebsd: diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 9fda60e7c26..22f45575dc8 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -58,9 +58,9 @@ jobs: containerd-version: ${{ matrix.containerd-version }} timeout: 20 - test-integration-container: - name: "in-container${{ inputs.hack }}" - uses: ./.github/workflows/job-test-in-container.yml + test-integration-host-linux: + name: "in-host${{ inputs.hack }}" + uses: ./.github/workflows/job-test-in-host.yml needs: build-dependencies strategy: fail-fast: false @@ -120,6 +120,7 @@ jobs: ipv6: ${{ matrix.ipv6 && true || false }} canary: ${{ matrix.canary && true || false }} skip-flaky: ${{ matrix.skip-flaky && true || false }} + go-version: 1.26 test-integration-host: name: "in-host${{ inputs.hack }}" @@ -136,14 +137,6 @@ jobs: # Test docker on linux - runner: ubuntu-24.04 binary: docker - - # FIXME: running nerdctl on the host is work in progress - # (we miss runc to be installed on the host - and obviously other deps) - # Plan is to pause this for now and first consolidate dependencies management (wrt Dockerfile vs. host-testing CI) - # before we can really start testing linux nerdctl on the host. - # - runner: ubuntu-24.04 - # - runner: ubuntu-24.04 - # canary: true with: timeout: 45 runner: ${{ matrix.runner }} @@ -161,11 +154,3 @@ jobs: # https://github.com/containerd/containerd/issues/13254 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: 75625e6f6595bb95f3fb9c8123a60534af4a8d9b52d7617065967bcefe71a17a - linux-containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 - linux-cni-version: v1.9.1 - linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index 0b68758e902..acbc86dfc28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,6 @@ ARG GOMODJAIL_VERSION=v0.3.2@c145bb1e36fe0939c5fa0467f2477878dea8e3d9 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.3 ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 ARG KUBO_VERSION=v0.42.0 @@ -133,10 +132,6 @@ RUN xx-go --wrap && \ make build && \ xx-verify --static cmd/ipfs/ipfs && cp -a cmd/ipfs/ipfs /out/${TARGETARCH} -FROM build-base AS build-minimal -RUN BINDIR=/out/bin make binaries install -# We do not set CMD to `go test` here, because it requires systemd - FROM build-base AS build-dependencies ARG TARGETARCH ENV GOARCH=${TARGETARCH} @@ -270,6 +265,36 @@ RUN (cd /out && find ! -type d | sort | xargs sha256sum > /tmp/SHA256SUMS ) && \ FROM scratch AS out-full COPY --from=build-full /out / +# build-test-integration-artifacts assembles, on top of the full distribution, the additional +# binaries that are only needed to run the integration test suite (cosign, soci, ipfs, nydus). +# It is meant to be exported with `--output=type=local` and installed under /usr/local on a +# (CI) host or VM, so that the integration tests can run directly on the host with `go test`. +FROM build-full AS build-test-integration-artifacts +ARG TARGETARCH +# copy cosign binary for integration test +COPY --from=ghcr.io/sigstore/cosign/cosign:v3.0.5@sha256:be924970ba7438c22e18067dec5637946d6566eac711f5bedd1584e7137008fb /ko-app/cosign /out/bin/cosign +# installing soci for integration test +# (the static build, so that it also runs on EL hosts, whose glibc is older than what +# the default build requires) +ARG SOCI_SNAPSHOTTER_VERSION +RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}-static.tar.gz" && \ + curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ + tar -C /out/bin -xvf "${fname}" soci soci-snapshotter-grpc && \ + rm -f "${fname}" +# enable offline ipfs for integration test +COPY --from=build-kubo /out/${TARGETARCH:-amd64}/* /out/bin/ +# install nydus components +ARG NYDUS_VERSION +RUN curl -o nydus-static.tgz -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \ + tar xzf nydus-static.tgz && \ + mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /out/bin/ && \ + rm -rf nydus-static.tgz nydus-static +# tests need a tini-custom binary +RUN cp /out/bin/tini /out/bin/tini-custom + +FROM scratch AS out-test-integration-artifacts +COPY --from=build-test-integration-artifacts /out / + FROM ubuntu:${UBUNTU_VERSION} AS base # fuse3 is required by stargz snapshotter RUN apt-get update -qq && apt-get install -qq -y --no-install-recommends \ @@ -295,94 +320,4 @@ VOLUME /var/lib/nerdctl ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["bash", "--login", "-i"] -FROM base AS test-integration -ARG DEBIAN_FRONTEND=noninteractive -# `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing -# `jq` is required to generate test summaries -RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ - software-properties-common \ - gnupg \ - gpg-agent \ - ca-certificates && \ - add-apt-repository ppa:criu/ppa && \ - apt-get update -qq && apt-get install -qq --no-install-recommends \ - expect \ - jq \ - git \ - make \ - criu -# We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" -COPY --from=build-base /usr/local/go /usr/local/go -ARG TARGETARCH -ENV PATH=/usr/local/go/bin:$PATH -ARG GOTESTSUM_VERSION -RUN GOBIN=/usr/local/bin go install gotest.tools/gotestsum@${GOTESTSUM_VERSION} -COPY . /go/src/github.com/containerd/nerdctl -WORKDIR /go/src/github.com/containerd/nerdctl -VOLUME /tmp -ENV CGO_ENABLED=0 -# copy cosign binary for integration test -COPY --from=ghcr.io/sigstore/cosign/cosign:v3.0.5@sha256:be924970ba7438c22e18067dec5637946d6566eac711f5bedd1584e7137008fb /ko-app/cosign /usr/local/bin/cosign -# installing soci for integration test -ARG SOCI_SNAPSHOTTER_VERSION -RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ - curl -o "${fname}" -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \ - tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc && \ - mkdir -p /etc/soci-snapshotter-grpc && \ - touch /etc/soci-snapshotter-grpc/config.toml && \ - echo "\n[pull_modes]\n [pull_modes.soci_v1]\n enable = true" >> /etc/soci-snapshotter-grpc/config.toml -# enable offline ipfs for integration test -COPY --from=build-kubo /out/${TARGETARCH:-amd64}/* /usr/local/bin/ -COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml -COPY ./Dockerfile.d/test-integration-ipfs-offline.service /usr/local/lib/systemd/system/ -COPY ./Dockerfile.d/test-integration-buildkit-nerdctl-test.service /usr/local/lib/systemd/system/ -COPY ./Dockerfile.d/test-integration-soci-snapshotter.service /usr/local/lib/systemd/system/ -RUN cp /usr/local/bin/tini /usr/local/bin/tini-custom -# using test integration containerd config -COPY ./Dockerfile.d/test-integration-etc_containerd_config.toml /etc/containerd/config.toml -# install ipfs service. avoid using 5001(api)/8080(gateway) which are reserved by tests. -RUN systemctl enable test-integration-ipfs-offline test-integration-buildkit-nerdctl-test test-integration-soci-snapshotter && \ - ipfs init && \ - ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888" && \ - ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889" -# install nydus components -ARG NYDUS_VERSION -RUN curl -o nydus-static.tgz -fsSL --retry 5 --retry-delay 5 --retry-max-time 120 --connect-timeout 20 --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \ - tar xzf nydus-static.tgz && \ - mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \ - rm nydus-static.tgz -CMD ["./hack/test-integration.sh"] - -FROM test-integration AS test-integration-rootless -# Install SSH for creating systemd user session. -# (`sudo` does not work for this purpose, -# OTOH `machinectl shell` can create the session but does not propagate exit code) -RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ - uidmap \ - openssh-server \ - openssh-client -# Install slirp4netns only if rootlesskit is prior to v3.0 -RUN if ! rootlesskit --help | grep -q gvisor-tap-vsock; then apt-get install -qq --no-install-recommends slirp4netns; fi -# TODO: update containerized-systemd to enable sshd by default, or allow `systemctl wants ssh` here -RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N '' && \ - useradd -m -s /bin/bash rootless && \ - mkdir -p -m 0700 /home/rootless/.ssh && \ - cp -a /root/.ssh/id_rsa.pub /home/rootless/.ssh/authorized_keys && \ - mkdir -p /home/rootless/.local/share && \ - chown -R rootless:rootless /home/rootless -COPY ./Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf /etc/systemd/system/user@.service.d/delegate.conf -# ipfs daemon for rootless containerd will be enabled in /test-integration-rootless.sh -RUN systemctl disable test-integration-ipfs-offline -VOLUME /home/rootless/.local/share -COPY ./Dockerfile.d/test-integration-rootless.sh / -RUN chmod a+rx /test-integration-rootless.sh -CMD ["/test-integration-rootless.sh", "./hack/test-integration.sh"] - -# test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns -FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns -RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ - slirp4netns -COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf -RUN chown -R rootless:rootless /home/rootless/.config - FROM base AS demo diff --git a/Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf b/Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf deleted file mode 100644 index e4c40b7eb24..00000000000 --- a/Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf +++ /dev/null @@ -1,3 +0,0 @@ -[Service] -# Change the port driver from "builtin" to "slirp4netns". Only used in CI. -Environment="CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" diff --git a/Dockerfile.d/test-integration-buildkit-nerdctl-test.service b/Dockerfile.d/test-integration-buildkit-nerdctl-test.service index 23d0ffc81c5..a9d6ec4dbe8 100644 --- a/Dockerfile.d/test-integration-buildkit-nerdctl-test.service +++ b/Dockerfile.d/test-integration-buildkit-nerdctl-test.service @@ -38,4 +38,4 @@ TasksMax=infinity OOMScoreAdjust=-999 [Install] -WantedBy=docker-entrypoint.target +WantedBy=multi-user.target diff --git a/Dockerfile.d/test-integration-ipfs-offline.service b/Dockerfile.d/test-integration-ipfs-offline.service index af0662250c5..b6b27d19f26 100644 --- a/Dockerfile.d/test-integration-ipfs-offline.service +++ b/Dockerfile.d/test-integration-ipfs-offline.service @@ -6,4 +6,4 @@ ExecStart=ipfs daemon --init --offline Environment=IPFS_PATH="%h/.ipfs" [Install] -WantedBy=docker-entrypoint.target +WantedBy=multi-user.target diff --git a/Dockerfile.d/test-integration-rootless.sh b/Dockerfile.d/test-integration-rootless.sh deleted file mode 100755 index 63f383462cc..00000000000 --- a/Dockerfile.d/test-integration-rootless.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -# Copyright The containerd Authors. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eux -o pipefail -if [[ "$(id -u)" = "0" ]]; then - # Ensure securityfs is mounted for apparmor to work - if ! mountpoint -q /sys/kernel/security; then - mount -tsecurityfs securityfs /sys/kernel/security - fi - if [ -e /sys/kernel/security/apparmor/profiles ]; then - # Load the "nerdctl-default" profile for TestRunApparmor - nerdctl apparmor load - fi - - : "${WORKAROUND_ISSUE_622:=}" - if [[ "$WORKAROUND_ISSUE_622" != "" ]]; then - touch /workaround-issue-622 - fi - - # Switch to the rootless user via SSH - systemctl start ssh - exec ssh -o StrictHostKeyChecking=no rootless@localhost "$0" "$@" -else - containerd-rootless-setuptool.sh install - if grep -q "options use-vc" /etc/resolv.conf; then - containerd-rootless-setuptool.sh nsenter -- sh -euc 'echo "options use-vc" >>/etc/resolv.conf' - fi - - if [[ -e /workaround-issue-622 ]]; then - echo "WORKAROUND_ISSUE_622: Not enabling BuildKit (https://github.com/containerd/nerdctl/issues/622)" >&2 - else - CONTAINERD_NAMESPACE="nerdctl-test" containerd-rootless-setuptool.sh install-buildkit-containerd - fi - containerd-rootless-setuptool.sh install-stargz - if [ ! -f "/home/rootless/.config/containerd/config.toml" ] ; then - echo "version = 2" > /home/rootless/.config/containerd/config.toml - fi - cat <>/home/rootless/.config/containerd/config.toml -[proxy_plugins] - [proxy_plugins."stargz"] - type = "snapshot" - address = "/run/user/$(id -u)/containerd-stargz-grpc/containerd-stargz-grpc.sock" - [proxy_plugins.stargz.exports] - root = "/home/rootless/.local/share/containerd-stargz-grpc/" - enable_remote_snapshot_annotations = "true" -[[plugins."io.containerd.transfer.v1.local".unpack_config]] - platform = "linux" - snapshotter = "overlayfs" -[[plugins."io.containerd.transfer.v1.local".unpack_config]] - platform = "linux" - snapshotter = "stargz" -EOF - systemctl --user restart containerd.service - containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing - echo "ipfs = true" >>/home/rootless/.config/containerd-stargz-grpc/config.toml - systemctl --user restart stargz-snapshotter.service - export IPFS_PATH="/home/rootless/.local/share/ipfs" - containerd-rootless-setuptool.sh install-bypass4netnsd - # Once ssh-ed, we lost the Dockerfile working dir, so, get back in the nerdctl checkout - cd /go/src/github.com/containerd/nerdctl - # We also lose the PATH (and SendEnv=PATH would require sshd config changes) - exec env PATH="/usr/local/go/bin:$PATH" "$@" -fi diff --git a/Dockerfile.d/test-integration-soci-snapshotter.service b/Dockerfile.d/test-integration-soci-snapshotter.service index 5964702ac6a..d4465e3d2f0 100644 --- a/Dockerfile.d/test-integration-soci-snapshotter.service +++ b/Dockerfile.d/test-integration-soci-snapshotter.service @@ -12,4 +12,4 @@ Restart=always RestartSec=5 [Install] -WantedBy=docker-entrypoint.target +WantedBy=multi-user.target diff --git a/docs/testing/README.md b/docs/testing/README.md index 0ab8fcd7647..82a2b35e86f 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -76,11 +76,25 @@ Note that this is different from the `--parallel` flag, which controls the amoun parallelization that a single go test binary will use when faced with tests that do explicitly allow it (with a call to `t.Parallel()`). -### Or test in a container +### Or provision a test environment with Docker-built artifacts + +Docker can be used to build all the dependencies needed to run the integration tests +(containerd, runc, CNI plugins, BuildKit, snapshotters, etc.), which can then be installed +on the host (this is what the CI does). These scripts substantially and irreversibly modify +the host, so they refuse to run unless `GITHUB_ACTIONS=true` is set - only do this on a +disposable machine: + +```bash +docker buildx build --target out-test-integration-artifacts --output type=local,dest=/tmp/nerdctl-test-artifacts . +sudo env GITHUB_ACTIONS=true ./hack/provisioning/linux/test-integration-env.sh install /tmp/nerdctl-test-artifacts rootful +./hack/test-integration.sh -test.target=nerdctl -test.only-flaky=false +``` + +For rootless (`rootless`, or `rootless-port-slirp4netns`): ```bash -docker build -t test-integration --target test-integration . -docker run -t --rm --privileged test-integration +sudo env GITHUB_ACTIONS=true ./hack/provisioning/linux/test-integration-env.sh install /tmp/nerdctl-test-artifacts rootless +GITHUB_ACTIONS=true ./hack/test-integration-rootless.sh ./hack/test-integration.sh -test.target=nerdctl -test.only-flaky=false ``` ### Principles diff --git a/hack/build-integration-canary.sh b/hack/build-integration-canary.sh index 7f55cc6e868..052140fafb7 100755 --- a/hack/build-integration-canary.sh +++ b/hack/build-integration-canary.sh @@ -214,7 +214,7 @@ assets::get(){ ###################### canary::build::integration(){ - docker_args=(docker build -t test-integration --target test-integration) + docker_args=(docker build -t test-integration-artifacts --target build-test-integration-artifacts) for dep in "${dependencies[@]}"; do local bl="" diff --git a/hack/provisioning/README.md b/hack/provisioning/README.md index 314ffc9fed4..03640804bc2 100644 --- a/hack/provisioning/README.md +++ b/hack/provisioning/README.md @@ -10,6 +10,7 @@ Use provided installation scripts instead (see user documentation). ## Contents - `/version` allows retrieving latest (or experimental) versions of certain products (golang, containerd, etc) -- `/linux` allows updating in-place containerd, cni (future: buildkit) +- `/linux` allows updating in-place containerd, cni (future: buildkit), and provisioning a full +integration testing environment (`test-integration-env.sh`) from Docker-built artifacts - `/windows` allows install WinCNI, containerd - `/kube` allows spinning-up a Kind cluster \ No newline at end of file diff --git a/hack/provisioning/linux/test-integration-env.sh b/hack/provisioning/linux/test-integration-env.sh new file mode 100755 index 00000000000..530ffd5e827 --- /dev/null +++ b/hack/provisioning/linux/test-integration-env.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# test-integration-env.sh provisions a linux host (a GitHub Actions runner, or a Lima guest) +# so that the integration test suite can run directly on it with `go test` +# (through hack/test-integration.sh), without being wrapped inside a Docker container. +# +# It expects a directory containing the artifacts exported from the +# `out-test-integration-artifacts` Dockerfile stage - Docker is only used to *build* them: +# docker buildx build --target out-test-integration-artifacts --output type=local,dest=DIR . +# +# Usage (as root, through sudo from the unprivileged user meant to run the tests): +# sudo ./hack/provisioning/linux/test-integration-env.sh install DIR [rootful|rootless|rootless-port-slirp4netns] +# +# Supported distributions: Ubuntu (GitHub Actions runners), and Enterprise Linux (Lima guests). + +set -o errexit -o errtrace -o functrace -o nounset -o pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" +readonly root +# lib.sh requires shasum at source time, which EL does not ship by default +if ! command -v shasum >/dev/null && command -v dnf >/dev/null; then + dnf install -q -y perl-Digest-SHA +fi +# shellcheck source=/dev/null +. "$root/../../scripts/lib.sh" + +readonly repo_root="$root/../../.." + +host::packages(){ + if command -v apt-get >/dev/null; then + # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing + # `jq` is required to generate test summaries + apt-get update -qq >/dev/null + add-apt-repository -y ppa:criu/ppa >/dev/null + apt-get install -qq --no-install-recommends \ + apparmor \ + criu \ + dbus-user-session \ + expect \ + fuse3 \ + git \ + jq \ + make \ + openssh-server \ + uidmap >/dev/null + elif command -v dnf >/dev/null; then + # `container-selinux` provides the contexts (container_t, ...) applied by the SELinux + # tests - the labels are applied even in permissive mode + dnf install -q -y \ + container-selinux \ + criu \ + expect \ + fuse3 \ + git \ + iptables \ + jq \ + make \ + openssh-server \ + shadow-utils \ + tar + else + log::error "Unsupported distribution (neither apt-get nor dnf found)" + return 1 + fi +} + +host::slirp4netns(){ + if command -v apt-get >/dev/null; then + apt-get install -qq --no-install-recommends slirp4netns >/dev/null + else + dnf install -q -y slirp4netns + fi +} + +host::artifacts(){ + local artifacts="$1" + + # The distribution-shipped containerd and Docker (if any) conflict with the containerd + # under test. Note that Docker cannot be used anymore past this point. + systemctl disable --now docker.socket 2>/dev/null || true + systemctl disable --now docker.service 2>/dev/null || true + systemctl disable --now containerd.service 2>/dev/null || true + + # /usr/local/lib/systemd/system is part of the default systemd unit search path, + # so, the containerd, buildkit and stargz-snapshotter units are usable right away. + # --no-overwrite-dir keeps the metadata of pre-existing directories (notably the + # permissions of /usr/local itself - the buildx local exporter creates the artifacts + # directory with mode 0700). + # --no-same-owner makes the files owned by root, rather than by the user that ran buildx. + (cd "$artifacts" && tar -cf- .) | tar -C /usr/local -xf- --no-same-owner --no-overwrite-dir +} + +host::configuration(){ + # Test-specific containerd, buildkit, stargz and soci configurations + mkdir -p /etc/containerd /etc/buildkit /etc/containerd-stargz-grpc /etc/soci-snapshotter-grpc + cp "$repo_root/Dockerfile.d/test-integration-etc_containerd_config.toml" /etc/containerd/config.toml + cp "$repo_root/Dockerfile.d/etc_buildkit_buildkitd.toml" /etc/buildkit/buildkitd.toml + cp "$repo_root/Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml" /etc/containerd-stargz-grpc/config.toml + printf '\n[pull_modes]\n [pull_modes.soci_v1]\n enable = true\n' > /etc/soci-snapshotter-grpc/config.toml + mkdir -p /etc/cni && chmod 0755 /etc/cni + + # Test-specific systemd units (started explicitly by host::services) + cp "$repo_root"/Dockerfile.d/test-integration-*.service /etc/systemd/system/ + + # On EL, be permissive: the test environment is not currently designed to run enforcing + # (the containerized test environment used to run privileged) + if command -v setenforce >/dev/null; then + setenforce 0 || true + [ ! -e /etc/selinux/config ] || sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config + fi + + # Tests are run by an unprivileged user, wrapping privileged invocations with + # `sudo`: the binaries under test must be resolvable then. + printf 'Defaults secure_path="/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"\n' \ + > /etc/sudoers.d/99-test-integration + chmod 0440 /etc/sudoers.d/99-test-integration + # Note: only validate our own drop-in - other, pre-existing files might not be valid + # (eg: /etc/sudoers.d/runner on GitHub Actions runners has "bad" permissions) + visudo -c -f /etc/sudoers.d/99-test-integration >/dev/null + + # Preload the kernel modules needed by the tests: br-netfilter ensures that bridged + # traffic goes through netfilter, and rootless containerd cannot trigger module + # auto-loading from inside a user namespace. The list mirrors Lima's 00-modprobe.sh + # boot script, which does not run for guests started in plain mode. + local module + for module in \ + br_netfilter \ + fuse \ + tun tap \ + bridge veth \ + ip_tables ip6_tables iptable_nat ip6table_nat iptable_filter ip6table_filter \ + nf_tables \ + x_tables xt_MASQUERADE xt_addrtype xt_comment xt_conntrack xt_mark xt_multiport xt_nat xt_tcpudp \ + overlay; do + modprobe "$module" || log::warning "Failed to load the $module module (negligible if it is built-in the kernel)" + done + + # Ensure securityfs is mounted, so that AppArmor detection works + mountpoint -q /sys/kernel/security || mount -t securityfs securityfs /sys/kernel/security + # Load the "nerdctl-default" AppArmor profile for TestRunApparmor: in rootless mode, + # the tests cannot load it themselves + if [ -e /sys/kernel/security/apparmor/profiles ]; then + /usr/local/bin/nerdctl apparmor load + fi +} + +host::services(){ + local target="$1" + local unit + local units=( + test-integration-soci-snapshotter.service + containerd.service + buildkit.service + stargz-snapshotter.service + test-integration-buildkit-nerdctl-test.service + ) + + if [ "$target" == "rootful" ]; then + # Offline ipfs daemon for testing. Avoid using 5001(api)/8080(gateway) which are + # reserved by tests. In rootless mode, this is handled by containerd-rootless-setuptool.sh. + if [ ! -e /root/.ipfs/config ]; then + env IPFS_PATH=/root/.ipfs /usr/local/bin/ipfs init >/dev/null + fi + env IPFS_PATH=/root/.ipfs /usr/local/bin/ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888" + env IPFS_PATH=/root/.ipfs /usr/local/bin/ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889" + units+=(test-integration-ipfs-offline.service) + fi + + systemctl daemon-reload + for unit in "${units[@]}"; do + systemctl restart "$unit" + done +} + +host::rootless(){ + local target="$1" + # The unprivileged user that is going to run the tests + local user="${SUDO_USER:-}" + + if [ "$user" == "" ] || [ "$user" == "root" ]; then + log::error "The $target target requires this script to be run with sudo, from the unprivileged user meant to run the tests" + return 1 + fi + + # Rootless containerd needs subordinate uids/gids for the testing user + grep -q "^$user:" /etc/subuid 2>/dev/null || echo "$user:100000:65536" >> /etc/subuid + grep -q "^$user:" /etc/subgid 2>/dev/null || echo "$user:100000:65536" >> /etc/subgid + + # Since Ubuntu 23.10+, apparmor restricts unprivileged user namespaces creation + if [ -e /etc/apparmor.d/abi/4.0 ]; then + cat < "/etc/apparmor.d/usr.local.bin.rootlesskit" +abi , +include +/usr/local/bin/rootlesskit flags=(unconfined) { + userns, + # Site-specific additions and overrides. See local/README for details. + include if exists +} +EOT + systemctl restart apparmor.service + fi + + # cgroup v2 delegation, for resource limits to work in rootless mode + mkdir -p /etc/systemd/system/user@.service.d + cp "$repo_root/Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf" /etc/systemd/system/user@.service.d/delegate.conf + systemctl daemon-reload + + # Keep the systemd user session (and thus the rootless daemons installed by + # containerd-rootless-setuptool.sh) alive in-between ssh sessions + loginctl enable-linger "$user" + + # Some tests publish ports 80 and 443, which the rootlesskit port driver has to bind + # as the unprivileged user. The containerized test environment used to get this for + # free: Docker sets this sysctl to 0 inside containers. + sysctl -w net.ipv4.ip_unprivileged_port_start=0 >/dev/null + + # Allow unprivileged ICMP Echo sockets (EL disables them by default; this mirrors + # Lima's 20-rootless-base.sh boot script, which does not run for guests started in + # plain mode) + sysctl -w "net.ipv4.ping_group_range=0 2147483647" >/dev/null + + # Without slirp4netns installed, rootlesskit v3.0+ falls back to its experimental + # gvisor-tap-vsock network driver: always provide the stable slirp4netns driver. + # slirp4netns is also required by the slirp4netns port driver, and by rootlesskit prior to v3.0. + host::slirp4netns +} + +provision::test-integration-env::install(){ + local artifacts="${1:-}" + local target="${2:-rootful}" + + [ "$(id -u)" == 0 ] || { + log::error "You need to be root" + return 1 + } + + # This script substantially and irreversibly modifies the host it runs on: + # it is only safe to run on a disposable CI machine + [ "${GITHUB_ACTIONS:-}" == "true" ] || { + log::error "Refusing to run outside of GitHub Actions (export GITHUB_ACTIONS=true to force)" + return 1 + } + + if [ "$artifacts" == "" ] || [ ! -d "$artifacts" ]; then + log::error "You need to point at a directory containing the test artifacts (built with: docker buildx build --target out-test-integration-artifacts --output type=local,dest=DIR .)" + return 1 + fi + + host::packages + host::artifacts "$artifacts" + host::configuration + [ "$target" == "rootful" ] || host::rootless "$target" + host::services "$target" +} + +com="$1" +shift +provision::test-integration-env::"$com" "$@" diff --git a/hack/test-integration-rootless.sh b/hack/test-integration-rootless.sh new file mode 100755 index 00000000000..3bf6bd4d981 --- /dev/null +++ b/hack/test-integration-rootless.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# test-integration-rootless.sh runs COMMAND in rootless mode, inside the systemd user +# session of the current, unprivileged user. +# +# It must be started as an unprivileged user with passwordless sudo, on a host +# provisioned with hack/provisioning/linux/test-integration-env.sh (which, among other +# things, enables lingering for the user, so that the systemd user session is available +# even when this script does not run from a logind session, eg: on a GitHub Actions +# runner). +# +# Usage: test-integration-rootless.sh COMMAND [ARGS...] +set -eux -o pipefail + +[ "$(id -u)" != "0" ] || { + echo "This script must be started as an unprivileged user with passwordless sudo" >&2 + exit 1 +} + +# This script substantially and irreversibly modifies the host (and the current user +# account): it is only safe to run on a disposable CI machine +[ "${GITHUB_ACTIONS:-}" == "true" ] || { + echo "Refusing to run outside of GitHub Actions (export GITHUB_ACTIONS=true to force)" >&2 + exit 1 +} + +# systemctl --user (and the dbus clients) locate the systemd user session through +# XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS. They are inherited when the script runs +# from a logind session (eg: an ssh session into a Lima guest), but not necessarily +# otherwise (eg: a GitHub Actions runner job): if unset, default them to the standard +# locations of the user session, which exists in any case, as the provisioning script +# enabled lingering. +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" +export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUNTIME_DIR}/bus}" + +# The environment below mirrors Lima's 20-rootless-base.sh boot script, which does not +# run in the CI guest VMs (they are started in plain mode). +# Make sure iptables and mount.fuse3 are resolvable (on EL, non-login shells do not get +# the sbin directories in their PATH). +export PATH="$PATH:/usr/sbin:/sbin" +# fuse-overlayfs is the most stable snapshotter for rootless, on kernel < 5.13 (eg: EL 8) +# https://rootlesscontaine.rs/how-it-works/overlayfs/ +if [ -z "${CONTAINERD_SNAPSHOTTER:-}" ]; then + kernel="$(uname -r)" + kernel="${kernel%%-*}" + if [ "$(printf '%s\n' "$kernel" "5.13" | sort -V | head -n1)" != "5.13" ]; then + export CONTAINERD_SNAPSHOTTER="fuse-overlayfs" + fi +fi + +export IPFS_PATH="$HOME/.local/share/ipfs" + +# If anything fails below, the systemd user journal usually knows why +trap 'sudo journalctl --no-pager --lines=200 _UID="$(id -u)" >&2 || true' ERR + +# This script gets invoked repeatedly (eg: non-flaky, then flaky test runs). +# The installation below is not idempotent (specifically, the containerd configuration +# must not be appended twice), so, only perform it once. +if [ ! -e "$HOME/.config/nerdctl-test-setup-done" ]; then + # The rootlesskit port driver is configured through the environment of the (generated) + # containerd systemd user unit, so, it has to be baked into a unit drop-in. + if [ "${CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER:-builtin}" != "builtin" ]; then + mkdir -p "$HOME/.config/systemd/user/containerd.service.d" + cat <<-EOF >"$HOME/.config/systemd/user/containerd.service.d/port-driver.conf" + [Service] + Environment="CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=${CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER}" + EOF + systemctl --user daemon-reload + fi + + containerd-rootless-setuptool.sh install + if grep -q "options use-vc" /etc/resolv.conf; then + containerd-rootless-setuptool.sh nsenter -- sh -euc 'echo "options use-vc" >>/etc/resolv.conf' + fi + + if [ "${WORKAROUND_ISSUE_622:-}" != "" ]; then + echo "WORKAROUND_ISSUE_622: Not enabling BuildKit (https://github.com/containerd/nerdctl/issues/622)" >&2 + else + CONTAINERD_NAMESPACE="nerdctl-test" containerd-rootless-setuptool.sh install-buildkit-containerd + fi + containerd-rootless-setuptool.sh install-stargz + # The fuse-overlayfs snapshotter is required on hosts that cannot mount overlayfs + # in a user namespace (eg: EL 8) + containerd-rootless-setuptool.sh install-fuse-overlayfs + if [ ! -f "$HOME/.config/containerd/config.toml" ]; then + mkdir -p "$HOME/.config/containerd" + echo "version = 2" >"$HOME/.config/containerd/config.toml" + fi + cat <>"$HOME/.config/containerd/config.toml" +[proxy_plugins] + [proxy_plugins."stargz"] + type = "snapshot" + address = "/run/user/$(id -u)/containerd-stargz-grpc/containerd-stargz-grpc.sock" + [proxy_plugins.stargz.exports] + root = "$HOME/.local/share/containerd-stargz-grpc/" + enable_remote_snapshot_annotations = "true" + [proxy_plugins."fuse-overlayfs"] + type = "snapshot" + address = "/run/user/$(id -u)/containerd-fuse-overlayfs.sock" + [proxy_plugins."fuse-overlayfs".exports] + root = "$HOME/.local/share/containerd-fuse-overlayfs/" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "overlayfs" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "stargz" +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux" + snapshotter = "fuse-overlayfs" +EOF + systemctl --user restart containerd.service + containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing + echo "ipfs = true" >>"$HOME/.config/containerd-stargz-grpc/config.toml" + systemctl --user restart stargz-snapshotter.service + containerd-rootless-setuptool.sh install-bypass4netnsd + + touch "$HOME/.config/nerdctl-test-setup-done" +fi + +exec "$@" From 0d818115a0c5ee4a4b334b4bc685648cc4406065 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 23:52:56 +0900 Subject: [PATCH 664/868] Dockerfile: run the entrypoint command only after the daemons are ready The command passed to `docker run` is executed by the generated docker-entrypoint.service, which had no ordering dependency on the containerd, buildkit, and stargz-snapshotter units, so `docker run -t --rm --privileged ghcr.io/containerd/nerdctl nerdctl run ...` was racy: it failed with "cannot access containerd socket" whenever the command won the race against containerd. Add the ordering through a unit drop-in. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index acbc86dfc28..9e9c49c0f03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -308,6 +308,12 @@ COPY --from=build-full /docker-entrypoint.sh /docker-entrypoint.sh COPY --from=out-full / /usr/local/ RUN perl -pi -e 's/multi-user.target/docker-entrypoint.target/g' /usr/local/lib/systemd/system/*.service && \ systemctl enable containerd buildkit stargz-snapshotter && \ + mkdir -p /etc/systemd/system/docker-entrypoint.service.d && \ + { echo "# docker-entrypoint.service runs the command passed to \`docker run\`: delay it"; \ + echo "# until the daemons are ready, so that \`docker run ... nerdctl run ...\` works"; \ + echo "[Unit]"; \ + echo "After=containerd.service buildkit.service stargz-snapshotter.service"; \ + } >/etc/systemd/system/docker-entrypoint.service.d/10-after-daemons.conf && \ mkdir -p /etc/bash_completion.d && \ nerdctl completion bash >/etc/bash_completion.d/nerdctl && \ mkdir -p -m 0755 /etc/cni From e08b9b81cf6d1603023b4ed1464afb76c13a3e19 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 4 Jul 2026 22:47:08 +0900 Subject: [PATCH 665/868] CI: smoke test the image built by the image workflow Build the image for the host platform, load it into Docker, and check that `nerdctl run` works inside it, before the multi-platform image is built and (except on pull requests) published to ghcr.io. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- .github/workflows/ghcr-image-build-and-publish.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 914dfd660db..383dd33cade 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -60,6 +60,20 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # Build the image for the host platform and load it into Docker, to smoke test + # it before the multi-platform image is built and published + - name: Build Docker image for the smoke test + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + load: true + tags: nerdctl-smoke-test + secrets: | + github_token=${{ secrets.GITHUB_TOKEN }} + + - name: Smoke test + run: docker run -t --rm --privileged nerdctl-smoke-test nerdctl run --rm hello-world + # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image From 48f77dd40e891fde9f4885f0d397838cd2f4b805 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 5 Jul 2026 04:34:33 +0900 Subject: [PATCH 666/868] run, exec: fix `-i` hanging when stdin reaches EOF during task creation `nerdctl (run|exec) -i` (without `-t`) propagates the EOF of its own stdin to the container by calling CloseIO: the shim deliberately keeps its own write end of the stdin FIFO open (so that a detached client does not propagate EOF), and only CloseIO makes the shim close it. The io copy goroutines are started by the cio.Creator, inside container.NewTask (respectively task.Exec), before the task (process) is registered in containerd: - in taskutil.NewTask, when the stdin was short enough to reach EOF during the creation, the container.Task API lookup in the closer failed with "no such task", and the error was discarded at the debug level; - in container.Exec, the closer was only installed after task.Exec returned, and a closer firing before that was a silent no-op. In both cases the CloseIO was lost forever: the container never received EOF on its stdin, and eg: `nerdctl run --rm -i IMAGE cat` hung until killed. Make the closer block on a channel that receives the task (process) handle when the creation returns, so that the CloseIO is always delivered. The race is easily reproducible under load; in the CI it is the cause of the TestRunStdin flakiness, which became a consistent failure on the slow almalinux-8 guests after the tests moved out of Docker. Fix issue 5029 Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- pkg/cmd/container/exec.go | 20 ++++++++++++++++---- pkg/taskutil/taskutil.go | 26 ++++++++++++++++++++------ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/container/exec.go b/pkg/cmd/container/exec.go index c874a4d1087..1dcf965eaad 100644 --- a/pkg/cmd/container/exec.go +++ b/pkg/cmd/container/exec.go @@ -73,8 +73,21 @@ func execActionWithContainer(ctx context.Context, client *containerd.Client, con var ( ioCreator cio.Creator in io.Reader - stdinC = &taskutil.StdinCloser{ + // The io copy goroutines are started by the cio.Creator, inside task.Exec + // below: on a short enough stdin, they can reach EOF before the process + // handle is available. Block until it is: losing the CloseIO would leave + // the write end of the stdin FIFO open inside the shim, and the exec'ed + // process would never receive EOF on its stdin. + processC = make(chan containerd.Process, 1) + stdinC = &taskutil.StdinCloser{ Stdin: os.Stdin, + Closer: func() { + if p, ok := <-processC; ok { + if err := p.CloseIO(ctx, containerd.WithStdinCloser); err != nil { + log.G(ctx).WithError(err).Warn("failed to close the process stdin") + } + } + }, } ) @@ -90,11 +103,10 @@ func execActionWithContainer(ctx context.Context, client *containerd.Client, con execID := "exec-" + idgen.GenerateID() process, err := task.Exec(ctx, execID, pspec, ioCreator) if err != nil { + close(processC) return err } - stdinC.Closer = func() { - process.CloseIO(ctx, containerd.WithStdinCloser) - } + processC <- process // if detach, we should not call this defer if !options.Detach { defer process.Delete(ctx) diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index 633c188cf73..fbf6c586c8a 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -68,6 +68,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container var ( checkpoint *types.Descriptor t containerd.Task + stdinTask chan containerd.Task err error ) @@ -206,17 +207,24 @@ func NewTask(ctx context.Context, client *containerd.Client, container container } else if sv.LessThan(semver.MustParse("1.6.0-0")) { log.G(ctx).Warnf("`nerdctl (run|exec) -i` without `-t` expects containerd 1.6 or later, got containerd %v", sv) } - var stdinC io.ReadCloser = &StdinCloser{ + // The io copy goroutines are started by the cio.Creator, inside + // container.NewTask below: on a short enough stdin, they can reach EOF + // before the task is registered in containerd, so the task cannot be + // looked up here through the API. Block until the task handle returned + // by container.NewTask is available instead: losing the CloseIO would + // leave the write end of the stdin FIFO open inside the shim, and the + // container would never receive EOF on its stdin. + stdinTask = make(chan containerd.Task, 1) + in = &StdinCloser{ Stdin: os.Stdin, Closer: func() { - if t, err := container.Task(ctx, nil); err != nil { - log.G(ctx).WithError(err).Debugf("failed to get task for StdinCloser") - } else { - t.CloseIO(ctx, containerd.WithStdinCloser) + if t, ok := <-stdinTask; ok { + if err := t.CloseIO(ctx, containerd.WithStdinCloser); err != nil { + log.G(ctx).WithError(err).Warn("failed to close the task stdin") + } } }, } - in = stdinC } ioCreator = cioutil.NewContainerIO(opts.Namespace, opts.LogURI, false, in, os.Stdout, os.Stderr) } @@ -230,8 +238,14 @@ func NewTask(ctx context.Context, client *containerd.Client, container container t, err = container.NewTask(ctx, ioCreator, taskOpts...) if err != nil { + if stdinTask != nil { + close(stdinTask) + } return nil, err } + if stdinTask != nil { + stdinTask <- t + } return t, nil } From 93e64ea726d89f20a7130e7efa371f3ded139e4c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 5 Jul 2026 18:44:53 +0000 Subject: [PATCH 667/868] test: extend EnsureContainerExited deadline to fix flaky restart tests TestRunRestartWithOnFailure runs a container with --restart=on-failure:2 and then uses nerdtest.EnsureContainerExited to wait for the container to reach the "exited" state. However, a stopped container whose restart labels still satisfy restart.Reconcile() is reported by nerdctl as "restarting", not "exited" (see dockercompat.statusFromNative). For on-failure:2, this means the container only shows "exited" after the containerd restart monitor has performed both restarts and bumped the restart count label to 2. Since the monitor only reconciles every 10 seconds by default, convergence takes ~20+ seconds in the worst case - right at (or beyond) the ~20 seconds budget of EnsureContainerExited (20 retries x 1s sleep), especially on slow CI runners. This is the source of the observed "container ... still not exited after 20 retries" flake. Replace the retry counter with a 60-second deadline (keeping the 1-second poll interval), matching the wait budget already used by TestRunRestartWithUnlessStopped. This also covers TestUpdateRestartPolicy, which calls the same helper with an on-failure:2 policy. Passing runs are not slowed down, as the loop still exits on the first observation of the exited state. Fixes #5030 Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- pkg/testutil/nerdtest/utilities.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index a012aed201f..37775cf1e68 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -117,6 +117,14 @@ func InspectImage(helpers test.Helpers, name string) dockercompat.Image { const ( maxRetry = 20 sleep = time.Second + // exitedDeadline is the maximum duration EnsureContainerExited waits for a + // container to reach the "exited" (or "dead") state. + // Note that this must accommodate containers using a restart policy + // (eg: `--restart=on-failure:2`): such containers are reported as + // "restarting" (not "exited") until the restart monitor exhausts the retry + // count, and the monitor only reconciles every 10 seconds by default + // (see https://github.com/containerd/nerdctl/issues/5030). + exitedDeadline = 60 * time.Second ) func EnsureContainerStarted(helpers test.Helpers, con string) { @@ -154,7 +162,8 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { func EnsureContainerExited(helpers test.Helpers, con string, exitCode int) { helpers.T().Helper() exited := false - for i := 0; i < maxRetry && !exited; i++ { + deadline := time.Now().Add(exitedDeadline) + for time.Now().Before(deadline) && !exited { helpers.Command("container", "inspect", con). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, @@ -189,7 +198,7 @@ func EnsureContainerExited(helpers test.Helpers, con string, exitCode int) { helpers.T().Log(ins) helpers.T().Log(lgs) helpers.T().Log(ps) - helpers.T().Log(fmt.Sprintf("container %s still not exited after %d retries", con, maxRetry)) + helpers.T().Log(fmt.Sprintf("container %s still not exited after %s", con, exitedDeadline)) helpers.T().FailNow() } } From abcb8f44db69d2aad689f1e8d30e2faf7c19e1a7 Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Sun, 5 Jul 2026 21:47:12 +0100 Subject: [PATCH 668/868] test: refactor container_run_mount_linux_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container_run_mount_linux_test.go | 1295 ++++++++++------- 1 file changed, 794 insertions(+), 501 deletions(-) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index 359e8234f75..c94dd004a4b 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -19,293 +19,498 @@ package container import ( "fmt" "os" - "path/filepath" "strings" "testing" mobymount "github.com/moby/sys/mount" "gotest.tools/v3/assert" - "github.com/containerd/containerd/v2/core/mount" "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/mountutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunVolume(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - rwDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - roDir, err := os.MkdirTemp(t.TempDir(), "ro") - if err != nil { - t.Fatal(err) - } - rwVolName := tID + "-rw" - roVolName := tID + "-ro" - for _, v := range []string{rwVolName, roVolName} { - defer base.Cmd("volume", "rm", "-f", v).Run() - base.Cmd("volume", "create", v).AssertOK() - } - - containerName := tID - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", - "-d", - "--name", containerName, - "-v", fmt.Sprintf("%s:/mnt1", rwDir), - "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), - "-v", fmt.Sprintf("%s:/mnt3", rwVolName), - "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), - testutil.AlpineImage, - "top", - ).AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail() - base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:/mnt1", rwDir), - "-v", fmt.Sprintf("%s:/mnt3", rwVolName), - testutil.AlpineImage, - "cat", "/mnt1/file1", "/mnt3/file3", - ).AssertOutExactly("str1str3") - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:/mnt3/mnt1", rwDir), - "-v", fmt.Sprintf("%s:/mnt3", rwVolName), - testutil.AlpineImage, - "cat", "/mnt3/mnt1/file1", "/mnt3/file3", - ).AssertOutExactly("str1str3") + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rw") + roDir := data.Temp().Dir("ro") + rwVolName := data.Identifier("rw") + roVolName := data.Identifier("ro") + + helpers.Ensure("volume", "create", rwVolName) + helpers.Ensure("volume", "create", roVolName) + + helpers.Ensure("run", + "-d", + "--name", data.Identifier(), + "-v", fmt.Sprintf("%s:/mnt1", rwDir), + "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), + "-v", fmt.Sprintf("%s:/mnt3", rwVolName), + "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), + testutil.AlpineImage, + "top", + ) + + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + // Verify rw mounts are writable + helpers.Ensure("exec", data.Identifier(), "sh", "-exc", "echo -n str1 > /mnt1/file1") + helpers.Ensure("exec", data.Identifier(), "sh", "-exc", "echo -n str3 > /mnt3/file3") + // Verify ro mounts are NOT writable + helpers.Fail("exec", data.Identifier(), "sh", "-exc", "echo -n str2 > /mnt2/file2") + helpers.Fail("exec", data.Identifier(), "sh", "-exc", "echo -n str4 > /mnt4/file4") + + helpers.Ensure("rm", "-f", data.Identifier()) + + data.Labels().Set("rwDir", rwDir) + data.Labels().Set("rwVolName", rwVolName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "data persists across container removal", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt1", data.Labels().Get("rwDir")), + "-v", fmt.Sprintf("%s:/mnt3", data.Labels().Get("rwVolName")), + testutil.AlpineImage, + "cat", "/mnt1/file1", "/mnt3/file3", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("str1str3")), + }, + { + Description: "nested mount ordering", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt3/mnt1", data.Labels().Get("rwDir")), + "-v", fmt.Sprintf("%s:/mnt3", data.Labels().Get("rwVolName")), + testutil.AlpineImage, + "cat", "/mnt3/mnt1/file1", "/mnt3/file3", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("str1str3")), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("rw")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("ro")) + } + + testCase.Run(t) } func TestRunAnonymousVolume(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage).AssertOK() - base.Cmd("run", "--rm", "-v", "TestVolume2:/foo", testutil.AlpineImage).AssertOK() - base.Cmd("run", "--rm", "-v", "TestVolume", testutil.AlpineImage).AssertOK() - - // Destination must be an absolute path not named volume - base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.AlpineImage).AssertFail() + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "anonymous volume with absolute path", + Command: test.Command("run", "--rm", "-v", "/foo", testutil.AlpineImage), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "named volume with absolute path", + Command: test.Command("run", "--rm", "-v", "TestVolume2:/foo", testutil.AlpineImage), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "volume name only", + Command: test.Command("run", "--rm", "-v", "TestVolume", testutil.AlpineImage), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "destination must be absolute path not named volume", + Command: test.Command("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.AlpineImage), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + + testCase.Run(t) } func TestRunVolumeRelativePath(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Dir = t.TempDir() - base.Cmd("run", "--rm", "-v", "./foo:/mnt/foo", testutil.AlpineImage).AssertOK() - base.Cmd("run", "--rm", "-v", "./foo", testutil.AlpineImage).AssertOK() - - // Destination must be an absolute path not a relative path - base.Cmd("run", "--rm", "-v", "./foo:./foo", testutil.AlpineImage).AssertFail() + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "relative source with absolute destination", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./foo:/mnt/foo", testutil.AlpineImage) + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "relative source only", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./foo", testutil.AlpineImage) + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "destination must be absolute not relative", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./foo:./foo", testutil.AlpineImage) + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + + testCase.Run(t) } func TestRunAnonymousVolumeWithTypeMountFlag(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "--mount", "type=volume,dst=/foo", testutil.AlpineImage, - "mountpoint", "-q", "/foo").AssertOK() + testCase := nerdtest.Setup() + + testCase.Command = test.Command("run", "--rm", "--mount", "type=volume,dst=/foo", testutil.AlpineImage, + "mountpoint", "-q", "/foo") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestRunAnonymousVolumeWithBuild(t *testing.T) { - t.Parallel() - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s VOLUME /foo `, testutil.AlpineImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("img"), data.Temp().Path()) + } - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage, - "mountpoint", "-q", "/foo").AssertOK() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "-v", "/foo", testutil.AlpineImage, + "mountpoint", "-q", "/foo") + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("img")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } + + testCase.Run(t) } func TestRunCopyingUpInitialContentsOnVolume(t *testing.T) { - t.Parallel() - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - volName := testutil.Identifier(t) + "-vol" - defer base.Cmd("volume", "rm", volName).Run() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s RUN mkdir -p /mnt && echo hi > /mnt/initial_file CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + data.Temp().Save(dockerfile, "Dockerfile") + imgName := data.Identifier("img") + helpers.Ensure("build", "-t", imgName, data.Temp().Path()) - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() + volName := data.Identifier("vol") + helpers.Ensure("volume", "create", volName) - //AnonymousVolume - base.Cmd("run", "--rm", imageName).AssertOutExactly("hi\n") - base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly("hi\n") + data.Labels().Set("img", imgName) + data.Labels().Set("vol", volName) + } - //NamedVolume should be automatically created - base.Cmd("run", "-v", volName+":/mnt", "--rm", imageName).AssertOutExactly("hi\n") + testCase.SubTests = []*test.Case{ + { + Description: "without volume flag", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + { + Description: "with anonymous volume", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", "/mnt", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + { + Description: "with named volume", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", data.Labels().Get("vol")+":/mnt", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", data.Labels().Get("vol")) + helpers.Anyhow("rmi", data.Labels().Get("img")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } + + testCase.Run(t) } func TestRunCopyingUpInitialContentsOnDockerfileVolume(t *testing.T) { - t.Parallel() - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - volName := testutil.Identifier(t) + "-vol" - defer base.Cmd("volume", "rm", volName).Run() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s RUN mkdir -p /mnt && echo hi > /mnt/initial_file VOLUME /mnt CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + data.Temp().Save(dockerfile, "Dockerfile") + imgName := data.Identifier("img") + helpers.Ensure("build", "-t", imgName, data.Temp().Path()) + + volName := data.Identifier("vol") + helpers.Ensure("volume", "create", volName) - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - //AnonymousVolume - base.Cmd("run", "--rm", imageName).AssertOutExactly("hi\n") - base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly("hi\n") + data.Labels().Set("img", imgName) + data.Labels().Set("vol", volName) + } - //NamedVolume - base.Cmd("volume", "create", volName).AssertOK() - base.Cmd("run", "-v", volName+":/mnt", "--rm", imageName).AssertOutExactly("hi\n") + testCase.SubTests = []*test.Case{ + { + Description: "anonymous volume from Dockerfile VOLUME", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + { + Description: "anonymous volume with -v flag", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", "/mnt", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + { + Description: "named volume copies initial contents", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", data.Labels().Get("vol")+":/mnt", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), + }, + { + Description: "bind mount does not copy initial contents", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", fmt.Sprintf("%s:/mnt", data.Temp().Dir("bindmnt")), "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } - //mount bind - tmpDir, err := os.MkdirTemp(t.TempDir(), "hostDir") - assert.NilError(t, err) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", data.Labels().Get("vol")) + helpers.Anyhow("rmi", data.Labels().Get("img")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } - base.Cmd("run", "-v", fmt.Sprintf("%s:/mnt", tmpDir), "--rm", imageName).AssertFail() + testCase.Run(t) } func TestRunCopyingUpInitialContentsOnVolumeShouldRetainSymlink(t *testing.T) { - t.Parallel() - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + testCase.NoParallel = true + + const expected = "../../../../../../../../../../../../../../../../../../etc/passwd\n" + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s RUN ln -s ../../../../../../../../../../../../../../../../../../etc/passwd /mnt/passwd VOLUME /mnt CMD ["readlink", "/mnt/passwd"] `, testutil.AlpineImage) - const expected = "../../../../../../../../../../../../../../../../../../etc/passwd\n" - buildCtx := helpers.CreateBuildContext(t, dockerfile) + data.Temp().Save(dockerfile, "Dockerfile") + imgName := data.Identifier("img") + helpers.Ensure("build", "-t", imgName, data.Temp().Path()) + + data.Labels().Set("img", imgName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "without explicit volume flag", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(expected)), + }, + { + Description: "with anonymous volume flag", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-v", "/mnt", "--rm", data.Labels().Get("img")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(expected)), + }, + } - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Labels().Get("img")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } - base.Cmd("run", "--rm", imageName).AssertOutExactly(expected) - base.Cmd("run", "-v", "/mnt", "--rm", imageName).AssertOutExactly(expected) + testCase.Run(t) } func TestRunCopyingUpInitialContentsShouldNotResetTheCopiedContents(t *testing.T) { - t.Parallel() - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - imageName := tID + "-img" - volumeName := tID + "-vol" - containerName := tID - defer func() { - base.Cmd("rm", "-f", containerName).Run() - base.Cmd("volume", "rm", volumeName).Run() - base.Cmd("rmi", imageName).Run() - }() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s RUN echo -n "rev0" > /mnt/file `, testutil.AlpineImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("img"), data.Temp().Path()) + + helpers.Ensure("volume", "create", data.Identifier("vol")) + + // First run: verify initial content is copied, then modify it + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "-v", data.Identifier("vol")+":/mnt", + data.Identifier("img"), "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + rev0 := helpers.Capture("exec", data.Identifier(), "cat", "/mnt/file") + assert.Equal(helpers.T(), rev0, "rev0") + + helpers.Ensure("exec", data.Identifier(), "sh", "-euc", `echo -n "rev1" >/mnt/file`) + helpers.Ensure("rm", "-f", data.Identifier()) + + // Second run: volume content should be "rev1", not reset to "rev0" + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "-v", data.Identifier("vol")+":/mnt", + data.Identifier("img"), "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier(), "cat", "/mnt/file") + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("rev1")) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", data.Identifier("vol")) + helpers.Anyhow("rmi", data.Identifier("img")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() + testCase.Run(t) +} - base.Cmd("volume", "create", volumeName) - runContainer := func() { - base.Cmd("run", "-d", "--name", containerName, "-v", volumeName+":/mnt", imageName, "sleep", nerdtest.Infinity).AssertOK() +func expectMountOptions(allow, deny []string) test.Comparator { + return func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) == 1, "expected 1 line, got %d: %q", len(lines), stdout) + for _, s := range allow { + assert.Assert(t, strings.Contains(stdout, s), "expected stdout to contain %q, got %q", s, stdout) + } + for _, s := range deny { + assert.Assert(t, !strings.Contains(stdout, s), "expected stdout not to contain %q, got %q", s, stdout) + } } - runContainer() - base.EnsureContainerStarted(containerName) - base.Cmd("exec", containerName, "cat", "/mnt/file").AssertOutExactly("rev0") - base.Cmd("exec", containerName, "sh", "-euc", "echo -n \"rev1\" >/mnt/file").AssertOK() - base.Cmd("rm", "-f", containerName).AssertOK() - runContainer() - base.EnsureContainerStarted(containerName) - base.Cmd("exec", containerName, "cat", "/mnt/file").AssertOutExactly("rev1") } func TestRunTmpfs(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - f := func(allow, deny []string) func(stdout string) error { - return func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 lines, got %q", stdout) - } - for _, s := range allow { - if !strings.Contains(stdout, s) { - return fmt.Errorf("expected stdout to contain %q, got %q", s, stdout) + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "tmpfs default options", + Command: test.Command("run", "--rm", "--tmpfs", "/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectMountOptions([]string{"rw", "nosuid", "nodev", "noexec"}, nil), } - } - for _, s := range deny { - if strings.Contains(stdout, s) { - return fmt.Errorf("expected stdout not to contain %q, got %q", s, stdout) + }, + }, + { + Description: "tmpfs with size and exec", + Command: test.Command("run", "--rm", "--tmpfs", "/tmp:size=64m,exec", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectMountOptions([]string{"rw", "nosuid", "nodev", "size=65536k"}, []string{"noexec"}), } - } - return nil - } + }, + }, + { + // https://github.com/containerd/nerdctl/issues/594 + Description: "tmpfs on /dev/shm with rw exec and size", + Command: test.Command("run", "--rm", "--tmpfs", "/dev/shm:rw,exec,size=1g", testutil.AlpineImage, "grep", "/dev/shm", "/proc/mounts"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectMountOptions([]string{"rw", "nosuid", "nodev", "size=1048576k"}, []string{"noexec"}), + } + }, + }, } - base.Cmd("run", "--rm", "--tmpfs", "/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "noexec"}, nil)) - base.Cmd("run", "--rm", "--tmpfs", "/tmp:size=64m,exec", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"}, []string{"noexec"})) - // for https://github.com/containerd/nerdctl/issues/594 - base.Cmd("run", "--rm", "--tmpfs", "/dev/shm:rw,exec,size=1g", testutil.AlpineImage, "grep", "/dev/shm", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=1048576k"}, []string{"noexec"})) + + testCase.Run(t) } func TestRunBindMountTmpfs(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - f := func(allow []string) func(stdout string) error { - return func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 lines, got %q", stdout) - } - for _, s := range allow { - if !strings.Contains(stdout, s) { - return fmt.Errorf("expected stdout to contain %q, got %q", s, stdout) + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "mount type tmpfs default", + Command: test.Command("run", "--rm", "--mount", "type=tmpfs,target=/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectMountOptions([]string{"rw", "nosuid", "nodev", "noexec"}, nil), } - } - return nil - } + }, + }, + { + Description: "mount type tmpfs with size", + Command: test.Command("run", "--rm", "--mount", "type=tmpfs,target=/tmp,tmpfs-size=64m", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectMountOptions([]string{"rw", "nosuid", "nodev", "size=65536k"}, nil), + } + }, + }, } - base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "noexec"})) - base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp,tmpfs-size=64m", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"})) + + testCase.Run(t) } func mountExistsWithOpt(mountPoint, mountOpt string) test.Comparator { @@ -409,359 +614,447 @@ func TestRunBindMountBind(t *testing.T) { testCase.Run(t) } -func TestRunMountBindMode(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("must be superuser to use mount") +func expectFindmntLines(expectedLines int, expectedPrefix string) test.Comparator { + return func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) == expectedLines, "expected %d line(s), got %d: %q", expectedLines, len(lines), stdout) + assert.Assert(t, strings.HasPrefix(lines[0], expectedPrefix), "expected mount %s, got %q", expectedPrefix, lines[0]) } - t.Parallel() - base := testutil.NewBase(t) +} - tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir1) - tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") - if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { - t.Fatal(err) - } +func TestRunMountBindMode(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Rootful + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tmpDir1 := data.Temp().Dir("rw") + tmpDir1Mnt := data.Temp().Dir("rw", "mnt") + tmpDir2 := data.Temp().Dir("ro") + + err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro") + assert.NilError(helpers.T(), err, "failed to mount") - tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") - if err != nil { - t.Fatal(err) + data.Labels().Set("tmpDir1", tmpDir1) + data.Labels().Set("tmpDir1Mnt", tmpDir1Mnt) } - defer os.RemoveAll(tmpDir2) - if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { - t.Fatal(err) + testCase.SubTests = []*test.Case{ + { + Description: "bind-recursive disabled hides submounts", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "--mount", fmt.Sprintf("type=bind,bind-recursive=disabled,src=%s,target=/mnt1", data.Labels().Get("tmpDir1")), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectFindmntLines(1, "/mnt1"), + } + }, + }, + { + Description: "bind-recursive enabled shows submounts", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "--mount", fmt.Sprintf("type=bind,bind-recursive=enabled,src=%s,target=/mnt1", data.Labels().Get("tmpDir1")), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectFindmntLines(2, "/mnt1"), + } + }, + }, } - defer func() { - if err := mobymount.Unmount(tmpDir1Mnt); err != nil { - t.Fatal(err) - } - }() - - base.Cmd("run", - "--rm", - "--mount", fmt.Sprintf("type=bind,bind-recursive=disabled,src=%s,target=/mnt1", tmpDir1), - testutil.AlpineImage, - "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", - ).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %q", stdout) - } - if !strings.HasPrefix(lines[0], "/mnt1") { - return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) - } - return nil - }) - - base.Cmd("run", - "--rm", - "--mount", fmt.Sprintf("type=bind,bind-recursive=enabled,src=%s,target=/mnt1", tmpDir1), - testutil.AlpineImage, - "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", - ).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 2 { - return fmt.Errorf("expected 2 line, got %q", stdout) - } - if !strings.HasPrefix(lines[0], "/mnt1") { - return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + mntPath := data.Labels().Get("tmpDir1Mnt") + if mntPath != "" { + _ = mobymount.Unmount(mntPath) } - return nil - }) + } + + testCase.Run(t) } func TestRunVolumeBindMode(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("must be superuser to use mount") - } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + testCase.Require = require.All(nerdtest.Rootful, require.Not(nerdtest.Docker)) - tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir1) - tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") - if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { - t.Fatal(err) - } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tmpDir1 := data.Temp().Dir("rw") + tmpDir1Mnt := data.Temp().Dir("rw", "mnt") + tmpDir2 := data.Temp().Dir("ro") + + err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro") + assert.NilError(helpers.T(), err, "failed to mount") - tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") - if err != nil { - t.Fatal(err) + data.Labels().Set("tmpDir1", tmpDir1) + data.Labels().Set("tmpDir1Mnt", tmpDir1Mnt) } - defer os.RemoveAll(tmpDir2) - if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { - t.Fatal(err) + testCase.SubTests = []*test.Case{ + { + Description: "bind mode hides submounts", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt1:bind", data.Labels().Get("tmpDir1")), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectFindmntLines(1, "/mnt1"), + } + }, + }, + { + Description: "rbind mode shows submounts", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt1:rbind", data.Labels().Get("tmpDir1")), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expectFindmntLines(2, "/mnt1"), + } + }, + }, } - defer func() { - if err := mobymount.Unmount(tmpDir1Mnt); err != nil { - t.Fatal(err) - } - }() - - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:/mnt1:bind", tmpDir1), - testutil.AlpineImage, - "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", - ).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %q", stdout) - } - if !strings.HasPrefix(lines[0], "/mnt1") { - return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) - } - return nil - }) - - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:/mnt1:rbind", tmpDir1), - testutil.AlpineImage, - "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", - ).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 2 { - return fmt.Errorf("expected 2 line, got %q", stdout) - } - if !strings.HasPrefix(lines[0], "/mnt1") { - return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + mntPath := data.Labels().Get("tmpDir1Mnt") + if mntPath != "" { + _ = mobymount.Unmount(mntPath) } - return nil - }) + } + + testCase.Run(t) } func TestRunBindMountPropagation(t *testing.T) { - t.Skip("This test is currently broken. See https://github.com/containerd/nerdctl/issues/3404") - - tID := testutil.Identifier(t) - - if !isRootfsShareableMount() { - t.Skipf("rootfs doesn't support shared mount, skip test %s", tID) + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.T().Skip("This test is currently broken. See https://github.com/containerd/nerdctl/issues/3404") } - t.Parallel() - base := testutil.NewBase(t) - - testCases := []struct { - propagation string - assertFunc func(containerName, containerNameReplica string) - }{ + testCase.SubTests = []*test.Case{ { - propagation: "rshared", - assertFunc: func(containerName, containerNameReplica string) { - // replica can get sub-mounts from original - base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") - - // and sub-mounts from replica will be propagated to the original too - base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica") + Description: "rshared propagation", + Setup: func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rshared") + data.Labels().Set("rshared-rwDir", rwDir) + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rshared"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rshared-replica"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), + testutil.AlpineImage, "top") + + // mount in the first container + helpers.Ensure("exec", data.Identifier("rshared"), "sh", "-exc", + "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt") + + // mount in the second container + helpers.Ensure("exec", data.Identifier("rshared-replica"), "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rshared-replica"), "sh", "-exc", "mount --bind /bar /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rshared-replica"), "sh", "-exc", "echo -n fromreplica > /bar/bar.txt") + }, + SubTests: []*test.Case{ + { + Description: "replica can get sub-mounts from original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rshared-replica"), "cat", "/mnt1/replica/foo.txt") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("toreplica")), + }, + { + Description: "sub-mounts from replica propagated to original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rshared"), "cat", "/mnt1/bar/bar.txt") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("fromreplica")), + }, + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("exec", data.Identifier("rshared-replica"), "sh", "-exc", "umount /mnt1/bar") + helpers.Anyhow("exec", data.Identifier("rshared"), "sh", "-exc", "umount /mnt1/replica") + helpers.Anyhow("rm", "-f", data.Identifier("rshared")) + helpers.Anyhow("rm", "-f", data.Identifier("rshared-replica")) }, }, { - propagation: "rslave", - assertFunc: func(containerName, containerNameReplica string) { - // replica can get sub-mounts from original - base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") + Description: "rslave propagation", + Setup: func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rslave") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rslave"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rslave-replica"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rslave", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("exec", data.Identifier("rslave"), "sh", "-exc", + "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt") - // but sub-mounts from replica will not be propagated to the original - base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() + helpers.Ensure("exec", data.Identifier("rslave-replica"), "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rslave-replica"), "sh", "-exc", "mount --bind /bar /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rslave-replica"), "sh", "-exc", "echo -n fromreplica > /bar/bar.txt") + }, + SubTests: []*test.Case{ + { + Description: "replica can get sub-mounts from original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rslave-replica"), "cat", "/mnt1/replica/foo.txt") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("toreplica")), + }, + { + Description: "sub-mounts from replica not propagated to original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rslave"), "cat", "/mnt1/bar/bar.txt") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("exec", data.Identifier("rslave-replica"), "sh", "-exc", "umount /mnt1/bar") + helpers.Anyhow("exec", data.Identifier("rslave"), "sh", "-exc", "umount /mnt1/replica") + helpers.Anyhow("rm", "-f", data.Identifier("rslave")) + helpers.Anyhow("rm", "-f", data.Identifier("rslave-replica")) }, }, { - propagation: "rprivate", - assertFunc: func(containerName, containerNameReplica string) { - // replica can't get sub-mounts from original - base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail() - // and sub-mounts from replica will not be propagated to the original too - base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() + Description: "rprivate propagation", + Setup: func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rprivate") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rprivate"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("rprivate-replica"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rprivate", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("exec", data.Identifier("rprivate"), "sh", "-exc", + "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt") + + helpers.Ensure("exec", data.Identifier("rprivate-replica"), "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rprivate-replica"), "sh", "-exc", "mount --bind /bar /mnt1/bar") + helpers.Ensure("exec", data.Identifier("rprivate-replica"), "sh", "-exc", "echo -n fromreplica > /bar/bar.txt") + }, + SubTests: []*test.Case{ + { + Description: "replica cannot get sub-mounts from original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rprivate-replica"), "cat", "/mnt1/replica/foo.txt") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "sub-mounts from replica not propagated to original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("rprivate"), "cat", "/mnt1/bar/bar.txt") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("exec", data.Identifier("rprivate-replica"), "sh", "-exc", "umount /mnt1/bar") + helpers.Anyhow("exec", data.Identifier("rprivate"), "sh", "-exc", "umount /mnt1/replica") + helpers.Anyhow("rm", "-f", data.Identifier("rprivate")) + helpers.Anyhow("rm", "-f", data.Identifier("rprivate-replica")) }, }, { - propagation: "", - assertFunc: func(containerName, containerNameReplica string) { - // replica can't get sub-mounts from original - base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail() - // and sub-mounts from replica will not be propagated to the original too - base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail() + Description: "default propagation", + Setup: func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("default") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("default"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("run", "-d", "--privileged", + "--name", data.Identifier("default-replica"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir), + testutil.AlpineImage, "top") + + helpers.Ensure("exec", data.Identifier("default"), "sh", "-exc", + "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt") + + helpers.Ensure("exec", data.Identifier("default-replica"), "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar") + helpers.Ensure("exec", data.Identifier("default-replica"), "sh", "-exc", "mount --bind /bar /mnt1/bar") + helpers.Ensure("exec", data.Identifier("default-replica"), "sh", "-exc", "echo -n fromreplica > /bar/bar.txt") + }, + SubTests: []*test.Case{ + { + Description: "replica cannot get sub-mounts from original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("default-replica"), "cat", "/mnt1/replica/foo.txt") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "sub-mounts from replica not propagated to original", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier("default"), "cat", "/mnt1/bar/bar.txt") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("exec", data.Identifier("default-replica"), "sh", "-exc", "umount /mnt1/bar") + helpers.Anyhow("exec", data.Identifier("default"), "sh", "-exc", "umount /mnt1/replica") + helpers.Anyhow("rm", "-f", data.Identifier("default")) + helpers.Anyhow("rm", "-f", data.Identifier("default-replica")) }, }, } - for _, tc := range testCases { - propagationName := tc.propagation - if propagationName == "" { - propagationName = "default" - } - - t.Logf("Running test propagation case %s", propagationName) + testCase.Run(t) +} - rwDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } +func TestRunVolumesFrom(t *testing.T) { + testCase := nerdtest.Setup() - containerName := tID + "-" + propagationName - containerNameReplica := containerName + "-replica" + testCase.Setup = func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rw") + roDir := data.Temp().Dir("ro") + rwVolName := data.Identifier("rw") + roVolName := data.Identifier("ro") - mountOption := fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=%s", rwDir, tc.propagation) - if tc.propagation == "" { - mountOption = fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir) - } + helpers.Ensure("volume", "create", rwVolName) + helpers.Ensure("volume", "create", roVolName) - containers := []struct { - name string - mountOption string - }{ - { - name: containerName, - mountOption: fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir), - }, - { - name: containerNameReplica, - mountOption: mountOption, - }, - } - for _, c := range containers { - base.Cmd("run", "-d", - "--privileged", - "--name", c.name, - "--mount", c.mountOption, - testutil.AlpineImage, - "top").AssertOK() - defer base.Cmd("rm", "-f", c.name).Run() - } + helpers.Ensure("run", + "-d", + "--name", data.Identifier("from"), + "-v", fmt.Sprintf("%s:/mnt1", rwDir), + "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), + "-v", fmt.Sprintf("%s:/mnt3", rwVolName), + "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), + testutil.AlpineImage, + "top", + ) - // mount in the first container - base.Cmd("exec", containerName, "sh", "-exc", "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt").AssertOK() - base.Cmd("exec", containerName, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("from")) - // mount in the second container - base.Cmd("exec", containerNameReplica, "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar").AssertOK() - base.Cmd("exec", containerNameReplica, "sh", "-exc", "mount --bind /bar /mnt1/bar").AssertOK() + helpers.Ensure("run", + "-d", + "--name", data.Identifier("to"), + "--volumes-from", data.Identifier("from"), + testutil.AlpineImage, + "top", + ) - base.Cmd("exec", containerNameReplica, "sh", "-exc", "echo -n fromreplica > /bar/bar.txt").AssertOK() - base.Cmd("exec", containerNameReplica, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("to")) - // call case specific assert function - tc.assertFunc(containerName, containerNameReplica) + // Verify rw mounts are writable via volumes-from container + helpers.Ensure("exec", data.Identifier("to"), "sh", "-exc", "echo -n str1 > /mnt1/file1") + helpers.Ensure("exec", data.Identifier("to"), "sh", "-exc", "echo -n str3 > /mnt3/file3") + // Verify ro mounts are NOT writable + helpers.Fail("exec", data.Identifier("to"), "sh", "-exc", "echo -n str2 > /mnt2/file2") + helpers.Fail("exec", data.Identifier("to"), "sh", "-exc", "echo -n str4 > /mnt4/file4") - // umount mount point in the first privileged container - base.Cmd("exec", containerNameReplica, "sh", "-exc", "umount /mnt1/bar").AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "umount /mnt1/replica").AssertOK() + helpers.Ensure("rm", "-f", data.Identifier("to")) } -} -// isRootfsShareableMount will check if /tmp or / support shareable mount -func isRootfsShareableMount() bool { - existFunc := func(mi mount.Info) bool { - for _, opt := range strings.Split(mi.Optional, " ") { - if strings.HasPrefix(opt, "shared:") { - return true - } - } - return false + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "--volumes-from", data.Identifier("from"), + testutil.AlpineImage, + "cat", "/mnt1/file1", "/mnt3/file3", + ) } - mi, err := mount.Lookup("/tmp") - if err == nil { - return existFunc(mi) - } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("str1str3")) - mi, err = mount.Lookup("/") - if err == nil { - return existFunc(mi) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("to")) + helpers.Anyhow("rm", "-f", data.Identifier("from")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("rw")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("ro")) } - return false -} - -func TestRunVolumesFrom(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - rwDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - roDir, err := os.MkdirTemp(t.TempDir(), "ro") - if err != nil { - t.Fatal(err) - } - rwVolName := tID + "-rw" - roVolName := tID + "-ro" - for _, v := range []string{rwVolName, roVolName} { - defer base.Cmd("volume", "rm", "-f", v).Run() - base.Cmd("volume", "create", v).AssertOK() - } - - fromContainerName := tID + "-from" - toContainerName := tID + "-to" - defer base.Cmd("rm", "-f", fromContainerName).AssertOK() - defer base.Cmd("rm", "-f", toContainerName).AssertOK() - base.Cmd("run", - "-d", - "--name", fromContainerName, - "-v", fmt.Sprintf("%s:/mnt1", rwDir), - "-v", fmt.Sprintf("%s:/mnt2:ro", roDir), - "-v", fmt.Sprintf("%s:/mnt3", rwVolName), - "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName), - testutil.AlpineImage, - "top", - ).AssertOK() - base.Cmd("run", - "-d", - "--name", toContainerName, - "--volumes-from", fromContainerName, - testutil.AlpineImage, - "top", - ).AssertOK() - base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK() - base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail() - base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK() - base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail() - base.Cmd("rm", "-f", toContainerName).AssertOK() - base.Cmd("run", - "--rm", - "--volumes-from", fromContainerName, - testutil.AlpineImage, - "cat", "/mnt1/file1", "/mnt3/file3", - ).AssertOutExactly("str1str3") + testCase.Run(t) } func TestBindMountWhenHostFolderDoesNotExist(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + "-host-dir-not-found" - hostDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hostDir) - hp := filepath.Join(hostDir, "does-not-exist") - base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "--name", containerName, "-d", "-v", fmt.Sprintf("%s:/tmp", - hp), testutil.AlpineImage).AssertOK() - base.Cmd("rm", "-f", containerName).AssertOK() - - // Host directory should get created - _, err = os.Stat(hp) - assert.NilError(t, err) - - // Test for --mount - os.RemoveAll(hp) - base.Cmd("run", "--name", containerName, "-d", "--mount", fmt.Sprintf("type=bind, source=%s, target=/tmp", - hp), testutil.AlpineImage).AssertFail() - _, err = os.Stat(hp) - assert.ErrorIs(t, err, os.ErrNotExist) + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "bind mount with -v auto-creates host directory", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + hp := data.Temp().Path("v-does-not-exist") + data.Labels().Set("hostPath", hp) + return helpers.Command("run", "--name", data.Identifier("v"), "-d", + "-v", fmt.Sprintf("%s:/tmp", hp), + testutil.AlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + _, err := os.Stat(data.Labels().Get("hostPath")) + assert.NilError(t, err, "host directory should exist after -v mount") + }, + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("v")) + }, + }, + { + Description: "bind mount with --mount does not auto-create host directory", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + hp := data.Temp().Path("mount-does-not-exist") + data.Labels().Set("hostPath", hp) + return helpers.Command("run", "--name", data.Identifier("mount"), "-d", + "--mount", fmt.Sprintf("type=bind, source=%s, target=/tmp", hp), + testutil.AlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Output: func(stdout string, t tig.T) { + _, err := os.Stat(data.Labels().Get("hostPath")) + assert.ErrorIs(t, err, os.ErrNotExist, "host directory should NOT exist after --mount failure") + }, + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("mount")) + }, + }, + } + + testCase.Run(t) } func TestRunVolumeWithRootDestination(t *testing.T) { From b064e6ffdfb62ee2cdb62b6850a8da4d2c5b97fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:17:38 +0000 Subject: [PATCH 669/868] build(deps): bump golang.org/x/text Bumps the golang-x group with 1 update in the / directory: [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/text` from 0.38.0 to 0.39.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.38.0...v0.39.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index a403f282139..39f95d634e0 100644 --- a/go.mod +++ b/go.mod @@ -69,7 +69,7 @@ require ( 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 + golang.org/x/text v0.39.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index 29df109e3e2..7b23aaa01e6 100644 --- a/go.sum +++ b/go.sum @@ -441,8 +441,8 @@ 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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= 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= @@ -453,8 +453,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From 2b9ce825298ba832f72a7a34523e86bc4efdbbe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:18:34 +0000 Subject: [PATCH 670/868] build(deps): bump github.com/pelletier/go-toml/v2 from 2.4.2 to 2.4.3 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.4.2 to 2.4.3. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.4.2...v2.4.3) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a403f282139..bf34c07caa2 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.15.1 - github.com/pelletier/go-toml/v2 v2.4.2 + github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 29df109e3e2..44eb6595fc9 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= 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.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q= -github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/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= From c76a63bec6c19ca06851f588ef32bcbf250443c5 Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Wed, 24 Jun 2026 00:49:50 +0530 Subject: [PATCH 671/868] feature: label-based filtering added Signed-off-by: Mujib Ahasan test case added Signed-off-by: Mujib Ahasan events: log errors when retrieving container labels Signed-off-by: Mujib Ahasan --- .../system/system_events_linux_test.go | 51 +++++++++++++++++++ pkg/cmd/system/events.go | 48 +++++++++++++++-- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index d63a450057a..c6b699d8814 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -51,6 +51,27 @@ func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.Testable return cmd } +func testEventLabelFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("pull", testutil.CommonImage) + + cmd := helpers.Command("events", "--filter", data.Labels().Get("filter"), "--format", "json") + cmd.WithTimeout(10 * time.Second) + cmd.Background() + + helpers.Ensure( + "run", + "-d", + "--name", data.Identifier(), + "--label", data.Labels().Get("containerLabel"), + testutil.CommonImage, + "tail", "-f", "/dev/null", + ) + time.Sleep(1 * time.Second) + helpers.Ensure("rm", "-f", data.Identifier()) + + return cmd +} + func TestEventFilters(t *testing.T) { testCase := nerdtest.Setup() @@ -128,6 +149,36 @@ func TestEventFilters(t *testing.T) { "output": "\"Status\":\"unknown\"", }), }, + { + Description: "LabelFilter", + Command: testEventLabelFilterExecutor, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeTimeout, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "filter": "label=com.example.app=myapp", + "containerLabel": "com.example.app=myapp", + "output": startEventOutput(), + }), + }, + { + Description: "LabelKeyOnlyFilter", + Command: testEventLabelFilterExecutor, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeTimeout, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "filter": "label=com.example.app", + "containerLabel": "com.example.app=myapp", + "output": startEventOutput(), + }), + }, } testCase.Run(t) diff --git a/pkg/cmd/system/events.go b/pkg/cmd/system/events.go index a544f071b57..41ddc1ffa8c 100644 --- a/pkg/cmd/system/events.go +++ b/pkg/cmd/system/events.go @@ -44,6 +44,7 @@ type EventOut struct { Topic string Status Status Event string + Labels map[string]string } type Status string @@ -90,6 +91,31 @@ func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, er return strings.EqualFold(string(e.Status), filterValue) }, nil + case "LABEL": + parts := strings.SplitN(filterValue, "=", 2) + key := parts[0] + if key == "" { + return nil, fmt.Errorf("%s is an invalid label filter", filterValue) + } + wantValue := len(parts) == 2 + var value string + if wantValue { + value = parts[1] + } + return func(e *EventOut) bool { + if len(e.Labels) == 0 { + return false + } + got, ok := e.Labels[key] + if !ok { + return false + } + + if !wantValue { + return true + } + return got == value + }, nil } return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter) @@ -161,6 +187,13 @@ func Events(ctx context.Context, client *containerd.Client, options types.System return err } } + labelFilterEnabled := false + for _, f := range options.Filters { + if strings.HasPrefix(strings.ToLower(f), "label=") { + labelFilterEnabled = true + break + } + } filterMap, err := generateEventFilters(options.Filters) if err != nil { return err @@ -175,6 +208,7 @@ func Events(ctx context.Context, client *containerd.Client, options types.System if e != nil { var out []byte var id string + labels := map[string]string{} if e.Event != nil { v, err := typeurl.UnmarshalAny(e.Event) if err != nil { @@ -194,11 +228,19 @@ func Events(ctx context.Context, client *containerd.Client, options types.System } else { _, ok := data["container_id"] if ok { - id = data["container_id"].(string) + if containerID, ok := data["container_id"].(string); ok { + id = containerID + } } } - - eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)} + if labelFilterEnabled && id != "" { + if container, err := client.ContainerService().Get(ctx, id); err != nil { + log.G(ctx).WithError(err).WithField("containerID", id).Debug("failed to retrieve container labels") + } else { + labels = container.Labels + } + } + eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out), labels} match := applyFilters(&eOut, filterMap) if match { if tmpl != nil { From b3e4377faae0eedfbbb5223d2df465b6dad019e2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 7 Jul 2026 18:20:13 +0900 Subject: [PATCH 672/868] tigron: hide env from logs by default Lots of env vars were shown since commit f21b32b (PR 5035) `CI: run test-integration[-rootless] directly on hosts and Lima guests`. GITHUB_TOKEN and ACTIONS_RUNTIME_TOKEN did not seem leaked so far, though. Signed-off-by: Akihiro Suda --- mod/tigron/test/command.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mod/tigron/test/command.go b/mod/tigron/test/command.go index e146b5c9e80..71c44c8a65d 100644 --- a/mod/tigron/test/command.go +++ b/mod/tigron/test/command.go @@ -214,8 +214,16 @@ func (gc *GenericCommand) Run(expect *Expected) { duration = "<1s" } + // The environment may contain secrets (e.g. tokens inherited from the CI + // environment), and test logs may end-up in publicly accessible places. + // Do not display it unless TIGRON_DEBUG_ENV is set. + environ := "(hidden: set TIGRON_DEBUG_ENV=1 to display)" + if debugEnv, _ := strconv.ParseBool(os.Getenv("TIGRON_DEBUG_ENV")); debugEnv { + environ = strings.Join(result.Environ, "\n") + } + debug = append(debug, - []any{envDecorator, strings.Join(result.Environ, "\n")}, + []any{envDecorator, environ}, []any{timeoutDecorator, duration + " (limit: " + gc.cmd.Timeout.String() + ")"}, []any{cwdDecorator, gc.cmd.WorkingDir}, ) From 9f77c08b8234fff0d450b8ad6c20c3d22a71a75a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:32:38 +0000 Subject: [PATCH 673/868] build(deps): bump github.com/ipfs/go-cid from 0.6.1 to 0.6.2 Bumps [github.com/ipfs/go-cid](https://github.com/ipfs/go-cid) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/ipfs/go-cid/releases) - [Commits](https://github.com/ipfs/go-cid/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: github.com/ipfs/go-cid dependency-version: 0.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bee210a7fc7..7dbbf82126b 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/fluent/fluent-logger-golang v1.10.1 github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/ipfs/go-cid v0.6.1 + github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.0 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined github.com/moby/moby/client v0.5.0 diff --git a/go.sum b/go.sum index b85b7bbb003..b657c4285bb 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ipfs/go-cid v0.6.1 h1:T5TnNb08+ueovG76Z5gx1L4Y7QOaGTXHg1F6raWFxIc= -github.com/ipfs/go-cid v0.6.1/go.mod h1:zrY0SwOhjrrIdfPQ/kf+k1sXyJ0QE7cMxfCployLBs0= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= From 53cedc0a6c6fab38ba90837140bf553afec625d4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 8 Jul 2026 21:43:49 +0900 Subject: [PATCH 674/868] test: skip TestIssue3016 for Docker with the containerd image store Docker with the containerd image store (the default since Docker v29 on fresh installs, e.g., on the ubuntu-26.04 GitHub Actions runners) refuses to resolve a reference that is both a tag and an image ID prefix ("ambiguous reference"), while nerdctl and Docker with the classic graph drivers resolve the tag first. Introduce a nerdtest.DockerContainerdSnapshotter requirement that detects the containerd image store from the "driver-type" entry of `docker info`'s DriverStatus, and skip TestIssue3016 with require.Not(nerdtest.DockerContainerdSnapshotter). Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- cmd/nerdctl/image/image_remove_test.go | 5 +++++ pkg/testutil/nerdtest/requirements.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index 6e3f4ad3e36..d0b5f25ea0c 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -311,6 +311,11 @@ func TestRemove(t *testing.T) { func TestIssue3016(t *testing.T) { testCase := nerdtest.Setup() + // Docker with the containerd image store refuses to resolve a reference that is + // both a tag and an image ID prefix ("ambiguous reference"), while nerdctl (and + // Docker with the classic graph drivers) resolves the tag first. + testCase.Require = require.Not(nerdtest.DockerContainerdSnapshotter) + const ( tagIDKey = "tagID" ) diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 2be8b6f9b4c..b7542e4c381 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -120,6 +120,30 @@ var Docker = &test.Requirement{ }, } +// DockerContainerdSnapshotter marks a test as suitable solely for Docker with the containerd +// image store enabled (the default since Docker v29 on fresh installs). +// Generally used as require.Not(nerdtest.DockerContainerdSnapshotter). +var DockerContainerdSnapshotter = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + if isTargetNerdish() { + return false, "current target is not docker" + } + stdout := helpers.Capture("info", "--format", "{{ json . }}") + // DriverStatus is not part of dockercompat.Info (nerdctl does not implement it) + var dinf struct { + DriverStatus [][2]string + } + err := json.Unmarshal([]byte(stdout), &dinf) + assert.NilError(helpers.T(), err, "failed to parse docker info") + for _, kv := range dinf.DriverStatus { + if kv[0] == "driver-type" && kv[1] == "io.containerd.snapshotter.v1" { + return true, "docker is using the containerd snapshotter" + } + } + return false, "docker is not using the containerd snapshotter" + }, +} + // NerdctlNeedsFixing marks a test as unsuitable to be run for Nerdctl, because of a specific known issue which // url must be passed as an argument var NerdctlNeedsFixing = func(issueLink string) *test.Requirement { From 12867764aa23d521b6bf6345b825d542cd937803 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 30 Jun 2026 01:37:27 +0900 Subject: [PATCH 675/868] CI: update Ubuntu (26.04) Signed-off-by: Akihiro Suda --- .../ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 6 ++-- .github/workflows/workflow-lint.yml | 18 +++++------ .github/workflows/workflow-test.yml | 30 +++++++++---------- .github/workflows/workflow-tigron.yml | 6 ++-- Dockerfile | 4 +-- .../linux/test-integration-env.sh | 12 ++++++++ 8 files changed, 46 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 5b6e6ec2211..936cdccebde 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -24,7 +24,7 @@ env: jobs: build: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02113c68336..0f69e8230ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ env: jobs: release: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 40 # The maximum access is "read" for PRs from public forked repos # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 2bbf64fe5b1..5b18b9ab9c1 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -29,7 +29,7 @@ jobs: target: ["rootful", "rootless"] with: timeout: 60 - runner: ubuntu-24.04 + runner: ubuntu-26.04 guest: ${{ matrix.guest }} target: ${{ matrix.target }} go-version: 1.26 @@ -40,11 +40,11 @@ jobs: uses: ./.github/workflows/job-test-in-lima-freebsd.yml with: timeout: 15 - runner: ubuntu-24.04 + runner: ubuntu-26.04 kube: name: "kubernetes" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 15 env: ROOTFUL: true diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 7933223fcc4..4e0c35b8732 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -22,25 +22,25 @@ jobs: fail-fast: false matrix: include: - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 goos: linux - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 goos: freebsd - runner: macos-15 goos: darwin # FIXME: this is currently failing in a nonsensical way, so, running on linux instead... # - runner: windows-2022 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 goos: windows # Additionally lint for canary # FIXME: failing since the release of go1.27rc1 - # - runner: ubuntu-24.04 + # - runner: ubuntu-26.04 # goos: linux # canary: true with: timeout: 10 go-version: "1.26" - runner: ubuntu-24.04 + runner: ubuntu-26.04 # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job # will not run. However, if you test it, it will coerce to `false`, hence: canary: ${{ matrix.canary && true || false }} @@ -53,7 +53,7 @@ jobs: with: timeout: 5 go-version: "1.26" - runner: ubuntu-24.04 + runner: ubuntu-26.04 # Lint for shell and yaml files lint-other: @@ -61,7 +61,7 @@ jobs: uses: ./.github/workflows/job-lint-other.yml with: timeout: 5 - runner: ubuntu-24.04 + runner: ubuntu-26.04 # Verify we can actually build on all supported platforms, and a bunch of architectures build-for-go: @@ -79,12 +79,12 @@ jobs: with: timeout: 10 go-version: ${{ matrix.go-version }} - runner: ubuntu-24.04 + runner: ubuntu-26.04 canary: ${{ matrix.canary && true || false }} zizmor: name: "zizmor" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: "Init: checkout" uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 22f45575dc8..5a6e6b05874 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -23,11 +23,11 @@ jobs: # Run on all supported platforms but freebsd # Additionally run on canary for linux include: - - runner: "ubuntu-24.04" + - runner: "ubuntu-26.04" - runner: "macos-15" - runner: "windows-2025" # FIXME: failing since the release of go1.27rc1 - # - runner: "ubuntu-24.04" + # - runner: "ubuntu-26.04" # canary: true with: runner: ${{ matrix.runner }} @@ -48,10 +48,10 @@ jobs: matrix: include: # Build for arm & amd, current containerd - - runner: ubuntu-24.04 - - runner: ubuntu-24.04-arm + - runner: ubuntu-26.04 + - runner: ubuntu-26.04-arm # Additionally build for old containerd on amd - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 containerd-version: v1.7.33 with: runner: ${{ matrix.runner }} @@ -68,14 +68,14 @@ jobs: include: ###### Rootless # amd64 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootless # arm64 - - runner: ubuntu-24.04-arm + - runner: ubuntu-26.04-arm target: rootless skip-flaky: true # port-slirp4netns - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootless-port-slirp4netns skip-flaky: true # old containerd + old ubuntu + old rootlesskit @@ -84,15 +84,15 @@ jobs: containerd-version: v1.7.33 rootlesskit-version: v1.1.1 # gomodjail - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootless binary: "nerdctl.gomodjail" ###### Rootful # amd64 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootful # arm64 - - runner: ubuntu-24.04-arm + - runner: ubuntu-26.04-arm target: rootful skip-flaky: true # old containerd + old ubuntu @@ -100,13 +100,13 @@ jobs: target: rootful containerd-version: v1.7.33 # ipv6 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootful ipv6: true skip-flaky: true # all canary # FIXME: failing since the release of go1.27rc1 - # - runner: ubuntu-24.04 + # - runner: ubuntu-26.04 # target: rootful # canary: true @@ -135,7 +135,7 @@ jobs: # - runner: windows-2025 # canary: true # Test docker on linux - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 binary: docker with: timeout: 45 @@ -148,7 +148,7 @@ jobs: no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.1 - docker-version: 5:29.6.1-1~ubuntu.24.04~noble + docker-version: 5:29.6.1-1~ubuntu.26.04~resolute # 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 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 576fe49c2c0..f5b09edbd3a 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -26,13 +26,13 @@ jobs: strategy: matrix: include: - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 - runner: macos-15 - runner: windows-2022 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 goos: freebsd # FIXME: failing since the release of go1.27rc1 - # - runner: ubuntu-24.04 + # - runner: ubuntu-26.04 # canary: go-canary steps: - name: "Checkout project" diff --git a/Dockerfile b/Dockerfile index 9e9c49c0f03..00ba8160a35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,8 +44,8 @@ ARG GOMODJAIL_VERSION=v0.3.2@c145bb1e36fe0939c5fa0467f2477878dea8e3d9 # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash ARG GO_VERSION=1.26 -ARG UBUNTU_VERSION=24.04 -ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 +ARG UBUNTU_VERSION=26.04 +ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 ARG NYDUS_VERSION=v2.4.3 ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 ARG KUBO_VERSION=v0.42.0 diff --git a/hack/provisioning/linux/test-integration-env.sh b/hack/provisioning/linux/test-integration-env.sh index 530ffd5e827..586b3836a59 100755 --- a/hack/provisioning/linux/test-integration-env.sh +++ b/hack/provisioning/linux/test-integration-env.sh @@ -155,6 +155,18 @@ host::configuration(){ if [ -e /sys/kernel/security/apparmor/profiles ]; then /usr/local/bin/nerdctl apparmor load fi + + # AppArmor configuration seems needed since Ubuntu 26.04 + if [ -e /etc/apparmor.d/fusermount3 ] && command -v apparmor_parser >/dev/null; then + mkdir -p /etc/apparmor.d/local + cat > /etc/apparmor.d/local/fusermount3 <<-'EOF' + # Allow the soci-snapshotter to mount FUSE filesystems under its root + # (installed by nerdctl's hack/provisioning/linux/test-integration-env.sh) + mount fstype=@{fuse_types} -> /var/lib/soci-snapshotter-grpc/**/, + umount /var/lib/soci-snapshotter-grpc/**/, + EOF + apparmor_parser -r /etc/apparmor.d/fusermount3 + fi } host::services(){ From 74fdaa62109bf579ebace245d678690cd4c42e13 Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Wed, 8 Jul 2026 22:18:44 +0100 Subject: [PATCH 676/868] test: refactor container_run_network_base_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container_run_network_base_test.go | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_base_test.go b/cmd/nerdctl/container/container_run_network_base_test.go index ae939cf4c4d..d6a08fd1bf7 100644 --- a/cmd/nerdctl/container/container_run_network_base_test.go +++ b/cmd/nerdctl/container/container_run_network_base_test.go @@ -27,7 +27,12 @@ import ( "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/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) @@ -44,7 +49,7 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri hostIP, err := nettestutil.NonLoopbackIPv4() assert.NilError(t, err) - type testCase struct { + type portTestCase struct { listenIP net.IP connectIP net.IP hostPort string @@ -55,7 +60,7 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri } lo := net.ParseIP("127.0.0.1") zeroIP := net.ParseIP("0.0.0.0") - testCases := []testCase{ + testCases := []portTestCase{ { listenIP: lo, connectIP: lo, @@ -186,39 +191,57 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri }, } - tID := testutil.Identifier(t) + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + for i := range testCases { + data.Labels().Set(fmt.Sprintf("container-%d", i), data.Identifier(fmt.Sprintf("container-%d", i))) + } + } + for i, tc := range testCases { - i := i - tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName := fmt.Sprintf("%s-%d", tID, i) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName).Run() - pFlag := fmt.Sprintf("%s:%s:%s", tc.listenIP.String(), tc.hostPort, tc.containerPort) - connectURL := fmt.Sprintf("http://%s:%d", tc.connectIP.String(), tc.connectURLPort) - t.Logf("pFlag=%q, connectURL=%q", pFlag, connectURL) - cmd := base.Cmd("run", "-d", - "--name", testContainerName, - "-p", pFlag, - nginxImage) - if tc.runShouldSuccess { - cmd.AssertOK() - } else { - cmd.AssertFail() - return - } - - resp, err := nettestutil.HTTPGet(connectURL, 5, false) - if tc.err != "" { - assert.ErrorContains(t, err, tc.err) - return - } - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(respBody), nginxIndexHTMLSnippet)) + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: fmt.Sprintf("%+v", tc), + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get(fmt.Sprintf("container-%d", i))) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testContainerName := data.Labels().Get(fmt.Sprintf("container-%d", i)) + pFlag := fmt.Sprintf("%s:%s:%s", tc.listenIP.String(), tc.hostPort, tc.containerPort) + helpers.T().Log("pFlag=", pFlag, ", container=", testContainerName) + return helpers.Command("run", "-d", + "--name", testContainerName, + "-p", pFlag, + nginxImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if tc.runShouldSuccess { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + connectURL := fmt.Sprintf("http://%s:%d", tc.connectIP.String(), tc.connectURLPort) + t.Log("connectURL=", connectURL) + + resp, err := nettestutil.HTTPGet(connectURL, 5, false) + if tc.err != "" { + assert.ErrorContains(t, err, tc.err) + return + } + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(respBody), nginxIndexHTMLSnippet)) + }, + } + } + + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + } + }, }) } + testCase.Run(t) } From 5999c713efa3568b350b70d39c71c2e02f7e11ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:32:42 +0000 Subject: [PATCH 677/868] build(deps): bump the golang-x group with 6 updates Bumps the golang-x group with 6 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.53.0` | `0.54.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.56.0` | `0.57.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.21.0` | `0.22.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.46.0` | `0.47.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.44.0` | `0.45.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.39.0` | `0.40.0` | Updates `golang.org/x/crypto` from 0.53.0 to 0.54.0 - [Commits](https://github.com/golang/crypto/compare/v0.53.0...v0.54.0) Updates `golang.org/x/net` from 0.56.0 to 0.57.0 - [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0) Updates `golang.org/x/sync` from 0.21.0 to 0.22.0 - [Commits](https://github.com/golang/sync/compare/v0.21.0...v0.22.0) Updates `golang.org/x/sys` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/sys/compare/v0.46.0...v0.47.0) Updates `golang.org/x/term` from 0.44.0 to 0.45.0 - [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0) Updates `golang.org/x/text` from 0.39.0 to 0.40.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.39.0...v0.40.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.57.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.45.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index bee210a7fc7..718134b6a87 100644 --- a/go.mod +++ b/go.mod @@ -64,12 +64,12 @@ require ( 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.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.39.0 + golang.org/x/crypto v0.54.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 //gomodjail:unconfined + golang.org/x/sys v0.47.0 //gomodjail:unconfined + golang.org/x/term v0.45.0 //gomodjail:unconfined + golang.org/x/text v0.40.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index b85b7bbb003..4a09a55b157 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= 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= @@ -386,8 +386,8 @@ 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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= 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= @@ -398,8 +398,8 @@ 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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.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= @@ -419,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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.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= @@ -430,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.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= 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= @@ -441,8 +441,8 @@ 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.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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= From 82fcb27e3596061103639a4f886281dccc1546fb Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 6 Jul 2026 01:36:00 +0900 Subject: [PATCH 678/868] mount: support the Docker v25 form of recursive read-only (RRO) mounts Read-only bind and volume mounts (`-v src:dst:ro`, `--mount ...,readonly`) are now made recursively read-only when the kernel (>= 5.12) and the OCI runtime (runc >= 1.1, crun >= 1.8.6) support the "rro" mount option, following Docker v25 (moby/moby#45278). The behavior is customizable with the `bind-recursive` option of `--mount` (docker/cli#4316): - `enabled` (default): recursive bind; recursively read-only when supported - `disabled`: non-recursive bind - `writable`: submounts of a read-only mount are kept writable (Docker v24 behavior) - `readonly`: force recursively read-only, or raise an error The boolean aliases of `bind-recursive` (unreleased) are removed, following docker/cli#4671. Whether the OCI runtime supports RRO mounts is detected by running `$RUNTIME features`, with the result cached in the XDG cache directory (e.g., ~/.cache/nerdctl/oci-runtime-features), invalidated when the runtime binary is modified. The old `rro` option of `-v` and `--mount`, introduced in nerdctl v0.14 ahead of Docker, is now deprecated in favor of the Docker v25 form: `--mount type=bind,src=...,dst=...,readonly,bind-propagation=rprivate,bind-recursive=readonly`. It now raises an error (instead of silently degrading to plain "ro") when RRO mounts are not supported. `nerdctl cp` now recognizes the "rro" mount option when refusing to copy into a read-only location. Fix issue 2651 Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- README.md | 5 +- .../container_run_mount_linux_test.go | 141 ++++++++++++++ docs/command-reference.md | 27 ++- docs/dir.md | 7 + pkg/cmd/container/run_mount.go | 4 +- pkg/containerutil/cp_resolve_linux.go | 2 +- pkg/mountutil/mountutil.go | 15 +- pkg/mountutil/mountutil_darwin.go | 4 +- pkg/mountutil/mountutil_freebsd.go | 4 +- pkg/mountutil/mountutil_linux.go | 175 +++++++++++++---- pkg/mountutil/mountutil_linux_test.go | 171 ++++++++++++++--- pkg/mountutil/mountutil_windows.go | 4 +- pkg/mountutil/mountutil_windows_test.go | 6 +- pkg/ociruntimeutil/ociruntimeutil.go | 179 ++++++++++++++++++ .../ociruntimeutil_linux_test.go | 113 +++++++++++ pkg/ociruntimeutil/rro_linux.go | 71 +++++++ 16 files changed, 841 insertions(+), 87 deletions(-) create mode 100644 pkg/ociruntimeutil/ociruntimeutil.go create mode 100644 pkg/ociruntimeutil/ociruntimeutil_linux_test.go create mode 100644 pkg/ociruntimeutil/rro_linux.go diff --git a/README.md b/README.md index 90b92ff807e..318435c215a 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,10 @@ Trivial: - Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too). Requires kernel >= 5.12. -The same feature was later introduced in Docker v25 with a different syntax. nerdctl will support Docker v25 syntax too in the future. + The same feature was later introduced in Docker v25 with a different syntax: read-only mounts are now recursively read-only by default when supported, + and the behavior is customizable with `--mount type=bind,...,readonly,bind-recursive=`. + nerdctl now supports the Docker v25 syntax too, and the old `rro` syntax is deprecated. + ## Similar tools - [`ctr`](https://github.com/containerd/containerd/tree/main/cmd/ctr): incompatible with Docker CLI, and not friendly to users. diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index c94dd004a4b..76a204588c1 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -19,6 +19,7 @@ package container import ( "fmt" "os" + "path/filepath" "strings" "testing" @@ -31,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/mountutil" + "github.com/containerd/nerdctl/v2/pkg/ociruntimeutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -744,6 +746,145 @@ func TestRunVolumeBindMode(t *testing.T) { testCase.Run(t) } +// requiresRRO requires that the kernel and the default OCI runtime support +// recursive read-only (RRO) mounts. +var requiresRRO = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + if err := ociruntimeutil.SupportsRecursivelyReadOnly(""); err != nil { + return false, fmt.Sprintf("recursive read-only mounts are not supported: %v", err) + } + return true, "recursive read-only mounts are supported" + }, +} + +// setupBindMountWithSubmount creates a temp directory ("top") containing a +// writable submount ("top/mnt"), for testing recursive read-only (RRO) mounts, +// and stores the path of the top directory in the "top" label. +func setupBindMountWithSubmount(data test.Data, helpers test.Helpers) { + top := data.Temp().Dir("top") + topMnt := data.Temp().Dir("top", "mnt") + sub := data.Temp().Dir("sub") + assert.NilError(helpers.T(), mobymount.Mount(sub, topMnt, "none", "bind")) + data.Labels().Set("top", top) +} + +func cleanupBindMountWithSubmount(data test.Data, helpers test.Helpers) { + if top := data.Labels().Get("top"); top != "" { + topMnt := filepath.Join(top, "mnt") + if err := mobymount.Unmount(topMnt); err != nil { + helpers.T().Log(fmt.Sprintf("failed to unmount %q: %v", topMnt, err)) + } + } +} + +// TestRunBindMountRecursiveReadOnly tests that read-only bind mounts are +// recursively read-only when the kernel and the OCI runtime support it +// (Docker v25 behavior), and that the mode is customizable with the +// `bind-recursive` option of `--mount`. +func TestRunBindMountRecursiveReadOnly(t *testing.T) { + testCase := nerdtest.Setup() + + // The test creates a bind mount on the host, in a mount namespace shared + // with the daemon. With the rootless harness, the test process runs on the + // host, while the daemon runs inside the mount namespace of RootlessKit, + // so the mount would not be visible to the daemon. + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + requiresRRO, + ) + + testCase.Setup = setupBindMountWithSubmount + + testCase.SubTests = []*test.Case{ + { + Description: "-v :ro is recursively read-only by default", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "-v", data.Labels().Get("top")+":/mnt1:ro", + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "--mount readonly is recursively read-only by default", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly", data.Labels().Get("top")), + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "bind-recursive=writable keeps the submounts writable (Docker v24 behavior)", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly,bind-recursive=writable", data.Labels().Get("top")), + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/file && touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "bind-recursive=readonly forces the recursive read-only mount", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,readonly,bind-propagation=rprivate,bind-recursive=readonly", data.Labels().Get("top")), + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } + + testCase.Cleanup = cleanupBindMountWithSubmount + + testCase.Run(t) +} + +// TestRunBindMountDeprecatedRRO tests the deprecated `rro` option of `-v` and +// `--mount`, which predates the `bind-recursive=readonly` option of Docker v25. +func TestRunBindMountDeprecatedRRO(t *testing.T) { + testCase := nerdtest.Setup() + + // See TestRunBindMountRecursiveReadOnly for the rootless restriction. + testCase.Require = require.All( + require.Not(nerdtest.Docker), + require.Not(nerdtest.Rootless), + requiresRRO, + ) + + testCase.Setup = setupBindMountWithSubmount + + testCase.SubTests = []*test.Case{ + { + Description: "-v :rro", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "-v", data.Labels().Get("top")+":/mnt1:rro,rprivate", + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "--mount rro", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1,rro,bind-propagation=rprivate", data.Labels().Get("top")), + testutil.AlpineImage, + "sh", "-euxc", "! touch /mnt1/mnt/file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } + + testCase.Cleanup = cleanupBindMountWithSubmount + + testCase.Run(t) +} + func TestRunBindMountPropagation(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { diff --git a/docs/command-reference.md b/docs/command-reference.md index 3eb172bae10..726655e9ea2 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -280,11 +280,14 @@ Runtime flags: Volume flags: -- :whale: `-v, --volume :[:]`: Bind mount a volume, e.g., `-v /mnt:/mnt:rro,rprivate` +- :whale: `-v, --volume :[:]`: Bind mount a volume, e.g., `-v /mnt:/mnt:ro` - :whale: option `rw` : Read/Write (when writable) - - :whale: option `ro` : Non-recursive read-only - - :nerd_face: option `rro`: Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too. - Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)). With older runc, `rro` just works as `ro`. + - :whale: option `ro` : Read-only. Recursively read-only (e.g., making children such as `/mnt/usb` read-only, too) when the kernel and the OCI runtime support it + (kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6, as in Docker v25), otherwise non-recursive read-only. + Use `--mount type=bind,...,readonly,bind-recursive=` to control the recursive read-only mode explicitly. + - :nerd_face: option `rro`: **Deprecated** since the same feature was introduced in Docker v25 with a different syntax; use `--mount type=bind,...,readonly,bind-propagation=rprivate,bind-recursive=readonly` instead. + Recursive read-only. Should be used in conjunction with `rprivate`. e.g., `-v /mnt:/mnt:rro,rprivate` makes children such as `/mnt/usb` to be read-only, too. + Requires kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6; an error is raised when the recursive read-only mount is not supported. - :whale: option `shared`, `slave`, `private`: Non-recursive "shared" / "slave" / "private" propagation - :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation - :nerd_face: option `bind`: Not-recursively bind-mounted @@ -302,12 +305,20 @@ Volume flags: - Common Options: - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. - :whale: `dst`, `destination`, `target`: Mount destination spec. - - :whale: `readonly`, `ro`: mount the filesystem read-only. - - :nerd_face: `rro`: mount the filesystem recursively read-only. + - :whale: `readonly`, `ro`: mount the filesystem read-only. Recursively read-only when the kernel and the OCI runtime support it + (kernel >= 5.12, and runc >= 1.1 or crun >= 1.8.6, as in Docker v25). See the `bind-recursive` option below to control the recursive read-only mode explicitly. + - :nerd_face: `rro`: **Deprecated** since the same feature was introduced in Docker v25 with a different syntax; use `readonly` with `bind-propagation=rprivate` and `bind-recursive=readonly` instead. + Mount the filesystem recursively read-only. - Options specific to `bind`: - :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). - - :whale: `bind-recursive`: `enabled`(default) or `disabled`. If set to `disabled`, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. - - :whale: `bind-nonrecursive`: `true` or `false`(default). Deprecated alias for `bind-recursive=disabled` / `bind-recursive=enabled`. If set to true, submounts are not recursively bind-mounted. + - :whale: `bind-recursive`: `enabled`(default), `disabled`, `writable`, or `readonly`. + - `enabled`: submounts are recursively bind-mounted, and a `readonly` mount is recursively read-only when the kernel and the OCI runtime support it. + - `disabled`: submounts are not recursively bind-mounted. + - `writable`: submounts of a `readonly` mount are kept writable (the default behavior of Docker until v24). + - `readonly`: a `readonly` mount is forced to be recursively read-only; an error is raised when the kernel or the OCI runtime does not support it. + Requires `bind-propagation=rprivate` to be specified in conjunction. + Whether the OCI runtime supports recursive read-only mounts is detected by running `$RUNTIME features`, and the result is cached in the XDG cache directory (e.g., `~/.cache/nerdctl/oci-runtime-features`). + - :whale: `bind-nonrecursive`: `true` or `false`(default). Deprecated alias for `bind-recursive=disabled` / `bind-recursive=enabled` (removed in Docker v29). If set to true, submounts are not recursively bind-mounted. - unimplemented options: `consistency` - Options specific to `tmpfs`: - :whale: `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default. diff --git a/docs/dir.md b/docs/dir.md index 43da6dff528..c842b09f4ff 100644 --- a/docs/dir.md +++ b/docs/dir.md @@ -72,3 +72,10 @@ and its networks definitions are private. Files: - `nerdctl-.conflist`: CNI conf list created by nerdctl + +## Cache + +### `/.nerdctl/oci-runtime-features/.json` +e.g., `~/.cache/nerdctl/oci-runtime-features/67865418f7d73228cb3df1357ba93456ab21567f3c1f1b5eca680b0b8cfbc04e.json` + +A cached result of ` features` (e.g., `runc features`). diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 5850cad92f0..266ca070c82 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -96,7 +96,7 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr var parsed []*mountutil.Processed //nolint:prealloc for _, v := range strutil.DedupeStrSlice(options.Volume) { // createDir=true for -v option to allow creation of directory on host if not found. - x, err := mountutil.ProcessFlagV(v, volStore, true) + x, err := mountutil.ProcessFlagV(v, volStore, true, options.Runtime) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr } for _, v := range strutil.DedupeStrSlice(options.Mount) { - x, err := mountutil.ProcessFlagMount(v, volStore) + x, err := mountutil.ProcessFlagMount(v, volStore, options.Runtime) if err != nil { return nil, err } diff --git a/pkg/containerutil/cp_resolve_linux.go b/pkg/containerutil/cp_resolve_linux.go index 1ee747730c2..39fb11d5b1e 100644 --- a/pkg/containerutil/cp_resolve_linux.go +++ b/pkg/containerutil/cp_resolve_linux.go @@ -293,7 +293,7 @@ func (res *resolver) getMount(path string) (*locator, string) { if len(mnt.Destination) > len(loc.containerPath) { loc.readonly = false for _, option := range mnt.Options { - if option == "ro" { + if option == "ro" || option == "rro" { loc.readonly = true } } diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index d55a2cb6646..2f78cb7f1f8 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -59,7 +59,10 @@ type volumeSpec struct { AnonymousVolume string } -func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (*Processed, error) { +// ProcessFlagV processes the value of the `-v` flag. +// ociRuntime is the value of the `--runtime` flag, used for detecting whether the +// OCI runtime supports recursive read-only mounts. +func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool, ociRuntime string) (*Processed, error) { var ( res *Processed volSpec volumeSpec @@ -119,7 +122,7 @@ func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (* rawOpts := res.Mode - options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts) + options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts, ociRuntime) if err != nil { return nil, err } @@ -215,16 +218,12 @@ func handleNamedVolumes(source string, volStore volumestore.VolumeStore) (volume return res, nil } -func getVolumeOptions(src string, vType string, rawOpts string) ([]string, []oci.SpecOpts, error) { +func getVolumeOptions(src, vType, rawOpts, ociRuntime string) ([]string, []oci.SpecOpts, error) { // always call parseVolumeOptions for bind mount to allow the parser to add some default options - var err error - var specOpts []oci.SpecOpts - options, specOpts, err := parseVolumeOptions(vType, src, rawOpts) + options, specOpts, err := parseVolumeOptions(vType, src, rawOpts, ociRuntime) if err != nil { return nil, nil, fmt.Errorf("failed to parse volume options (%q, %q, %q): %w", vType, src, rawOpts, err) } - - specOpts = append(specOpts, specOpts...) return options, specOpts, nil } diff --git a/pkg/mountutil/mountutil_darwin.go b/pkg/mountutil/mountutil_darwin.go index c86d9a3cdec..8d14b76b7f4 100644 --- a/pkg/mountutil/mountutil_darwin.go +++ b/pkg/mountutil/mountutil_darwin.go @@ -40,7 +40,7 @@ func UnprivilegedMountFlags(path string) ([]string, error) { // parseVolumeOptions parses specified optsRaw with using information of // the volume type and the src directory when necessary. -func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) { +func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) { var writeModeRawOpts []string for _, opt := range strings.Split(optsRaw, ",") { switch opt { @@ -67,6 +67,6 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } -func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) { +func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } diff --git a/pkg/mountutil/mountutil_freebsd.go b/pkg/mountutil/mountutil_freebsd.go index 58b32075b82..21749c93d33 100644 --- a/pkg/mountutil/mountutil_freebsd.go +++ b/pkg/mountutil/mountutil_freebsd.go @@ -41,7 +41,7 @@ func UnprivilegedMountFlags(path string) ([]string, error) { // parseVolumeOptions parses specified optsRaw with using information of // the volume type and the src directory when necessary. -func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) { +func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) { var writeModeRawOpts []string for _, opt := range strings.Split(optsRaw, ",") { switch opt { @@ -68,6 +68,6 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } -func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) { +func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index b40cf9750ec..28c67e4dbbd 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -37,6 +37,8 @@ import ( "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" + "github.com/containerd/nerdctl/v2/pkg/ociruntimeutil" + "github.com/containerd/nerdctl/v2/pkg/strutil" ) /* @@ -90,10 +92,56 @@ func UnprivilegedMountFlags(path string) ([]string, error) { return flags, nil } +// supportsRecursivelyReadOnly is replaced in unit tests. +var supportsRecursivelyReadOnly = ociruntimeutil.SupportsRecursivelyReadOnly + +// readOnlyMode is the read-only mode of a mount. +// The modes correspond to the BindOptions of the Docker API >= v1.44. +// https://github.com/moby/moby/pull/45278 +type readOnlyMode int + +const ( + // readOnlyModeRecursiveIfPossible makes the mount recursively read-only when + // the kernel and the OCI runtime support the "rro" mount option, and falls + // back to the plain (non-recursive) read-only otherwise. + // This is the default mode of read-only mounts since Docker v25. + readOnlyModeRecursiveIfPossible readOnlyMode = iota + // readOnlyModeNonRecursive makes the mount read-only, but keeps its submounts + // writable. This was the default mode of read-only mounts until Docker v24. + // Corresponds to `--mount type=bind,readonly,bind-recursive=writable`. + readOnlyModeNonRecursive + // readOnlyModeForceRecursive makes the mount recursively read-only, or + // raises an error when the kernel or the OCI runtime does not support "rro". + // Corresponds to `--mount type=bind,readonly,bind-recursive=readonly`. + readOnlyModeForceRecursive +) + +// readOnlyMountOptions returns the mount options for the given read-only mode. +// Whether the OCI runtime supports the "rro" mount option is detected by running +// `$RUNTIME features`; ociRuntime is the value of the `--runtime` flag. +func readOnlyMountOptions(mode readOnlyMode, ociRuntime string) ([]string, error) { + switch mode { + case readOnlyModeRecursiveIfPossible: + if err := supportsRecursivelyReadOnly(ociRuntime); err != nil { + log.L.WithError(err).Debug("recursive read-only mounts are not supported, falling back to non-recursive read-only") + return []string{"ro"}, nil + } + return []string{"rro"}, nil + case readOnlyModeNonRecursive: + return []string{"ro"}, nil + case readOnlyModeForceRecursive: + if err := supportsRecursivelyReadOnly(ociRuntime); err != nil { + return nil, err + } + return []string{"rro"}, nil + } + return nil, fmt.Errorf("unexpected read-only mode %v", mode) +} + // parseVolumeOptions parses specified optsRaw with using information of // the volume type and the src directory when necessary. -func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) { - return parseVolumeOptionsWithMountInfo(vType, src, optsRaw, getMountInfo) +func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) { + return parseVolumeOptionsWithMountInfo(vType, src, optsRaw, ociRuntime, getMountInfo) } // getMountInfo gets mount.Info of a directory. @@ -107,7 +155,7 @@ func getMountInfo(dir string) (mount.Info, error) { // parseVolumeOptionsWithMountInfo is the testable implementation // of parseVolumeOptions. -func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFunc func(string) (mount.Info, error)) ([]string, []oci.SpecOpts, error) { +func parseVolumeOptionsWithMountInfo(vType, src, optsRaw, ociRuntime string, getMountInfoFunc func(string) (mount.Info, error)) ([]string, []oci.SpecOpts, error) { var ( writeModeRawOpts []string propagationRawOpts []string @@ -154,14 +202,25 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun } else if len(writeModeRawOpts) > 0 { switch writeModeRawOpts[0] { case "ro": - opts = append(opts, "ro") + // Docker (since v25) attempts to make the mount recursively read-only. + // https://github.com/moby/moby/pull/45278 + roOpts, err := readOnlyMountOptions(readOnlyModeRecursiveIfPossible, ociRuntime) + if err != nil { + return nil, nil, err + } + opts = append(opts, roOpts...) case "rro": - // Mount option "rro" is supported since crun v1.4 / runc v1.1 (https://github.com/opencontainers/runc/pull/3272), with kernel >= 5.12. - // Older version of runc just ignores "rro", so we have to add "ro" too, to our best effort. - opts = append(opts, "ro", "rro") + // "rro" was introduced in nerdctl v0.14 (2021), ahead of Docker. + // Docker v25 introduced `--mount type=bind,...,readonly,bind-recursive=readonly` instead. + log.L.Warn("The volume option \"rro\" is deprecated; use `--mount type=bind,src=...,dst=...,readonly,bind-propagation=rprivate,bind-recursive=readonly` instead") if len(propagationRawOpts) != 1 || propagationRawOpts[0] != "rprivate" { log.L.Warn("Mount option \"rro\" should be used in conjunction with \"rprivate\"") } + roOpts, err := readOnlyMountOptions(readOnlyModeForceRecursive, ociRuntime) + if err != nil { + return nil, nil, err + } + opts = append(opts, roOpts...) case "rw": // NOP default: @@ -301,7 +360,7 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { return res, nil } -func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) { +func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) { fields := strings.Split(s, ",") var ( mountType string @@ -309,6 +368,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e dst string bindPropagation string bindNonRecursive bool + bindRecursive string // "enabled", "disabled", "writable", or "readonly" rwOption string tmpfsSize int64 tmpfsMode os.FileMode @@ -332,11 +392,16 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e if len(parts) == 1 { switch key { - case "readonly", "ro", "rro": + case "readonly", "ro": + rwOption = key + continue + case "rro": + log.L.Warn("The mount option \"rro\" is deprecated; use \"readonly\" with \"bind-propagation=rprivate\" and \"bind-recursive=readonly\" instead") rwOption = key continue case "bind-nonrecursive": // Removed in Docker v29, in favor of `bind-recursive=disabled` https://github.com/docker/cli/pull/6241 + log.L.Warn("The mount option \"bind-nonrecursive\" is deprecated; use \"bind-recursive=disabled\" instead") bindNonRecursive = true continue } @@ -367,6 +432,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e if err != nil { return nil, fmt.Errorf("invalid value for %s: %s", key, value) } + if key == "rro" { + log.L.Warn("The mount option \"rro\" is deprecated; use \"readonly\" with \"bind-propagation=rprivate\" and \"bind-recursive=readonly\" instead") + } if trueValue { rwOption = key } @@ -376,29 +444,19 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e bindPropagation = value case "bind-nonrecursive": // Removed in Docker v29, in favor of `bind-recursive=disabled` https://github.com/docker/cli/pull/6241 + log.L.Warn("The mount option \"bind-nonrecursive\" is deprecated; use \"bind-recursive=disabled\" instead") bindNonRecursive, err = strconv.ParseBool(value) if err != nil { return nil, fmt.Errorf("invalid value for %s: %s", key, value) } case "bind-recursive": - // bind-recursive is the Docker option that supersedes bind-nonrecursive. - valS := value - // Allow boolean as an alias to "enabled" or "disabled" - if b, err := strconv.ParseBool(valS); err == nil { - if b { - valS = "enabled" - } else { - valS = "disabled" - } - } - switch valS { - case "enabled": - bindNonRecursive = false - case "disabled": - bindNonRecursive = true + // bind-recursive is the Docker (v25) option that supersedes bind-nonrecursive. + // https://github.com/docker/cli/pull/4316 + switch value { + case "enabled", "disabled", "writable", "readonly": + bindRecursive = value default: - // TODO: support "writable", "readonly" - return nil, fmt.Errorf("invalid value for %s: %s (must be \"enabled\" or \"disabled\")", key, value) + return nil, fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"writable\", or \"readonly\")", key, value) } case "tmpfs-size": tmpfsSize, err = units.RAMInBytes(value) @@ -416,20 +474,56 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e } } + // Resolve the read-only mode of the mount, for Docker (v25) compatibility. + // https://github.com/docker/cli/pull/4316 + roMode := readOnlyModeRecursiveIfPossible + if rwOption == "rro" { + // Deprecated form: force RRO, like `bind-recursive=readonly` (but the + // propagation is not validated, for compatibility with older nerdctl). + roMode = readOnlyModeForceRecursive + if bindPropagation != "rprivate" { + log.L.Warn("Mount option \"rro\" should be used in conjunction with \"bind-propagation=rprivate\"") + } + } + if bindRecursive != "" { + if mountType != Bind { + return nil, fmt.Errorf("the option bind-recursive is only supported for bind mounts") + } + switch bindRecursive { + case "enabled": + bindNonRecursive = false + case "disabled": + bindNonRecursive = true + case "writable": + if rwOption == "" { + return nil, fmt.Errorf("the option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction") + } + roMode = readOnlyModeNonRecursive + case "readonly": + if rwOption == "" { + return nil, fmt.Errorf("the option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction") + } + if bindPropagation != "rprivate" { + return nil, fmt.Errorf("the option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction") + } + if bindNonRecursive { + return nil, fmt.Errorf("the option 'bind-recursive=readonly' conflicts with 'bind-nonrecursive'") + } + roMode = readOnlyModeForceRecursive + } + } + // compose new fileds and join into a string // to call legacy ProcessFlagTmpfs or ProcessFlagV function fields = []string{} options := []string{} - if rwOption != "" { - if rwOption == "readonly" { - rwOption = "ro" - } - options = append(options, rwOption) - } switch mountType { case Tmpfs: fields = []string{dst} + if rwOption != "" { + options = append(options, "ro") + } if tmpfsMode != 0 { options = append(options, fmt.Sprintf("mode=%o", tmpfsMode)) } @@ -437,6 +531,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e options = append(options, getTmpfsSize(tmpfsSize)) } case Volume, Bind: + // The read-only option is not composed here; it is applied to the + // processed mount below, as the legacy volume option syntax cannot + // express all the read-only modes. fields = []string{src, dst} if bindPropagation != "" { options = append(options, bindPropagation) @@ -463,7 +560,19 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e return ProcessFlagTmpfs(fieldsStr) case Volume, Bind: // createDir=false for --mount option to disallow creating directories on host if not found - return ProcessFlagV(fieldsStr, volStore, false) + res, err := ProcessFlagV(fieldsStr, volStore, false, ociRuntime) + if err != nil { + return nil, err + } + if rwOption != "" { + roOpts, err := readOnlyMountOptions(roMode, ociRuntime) + if err != nil { + return nil, err + } + res.Mount.Options = strutil.DedupeStrSlice(append(res.Mount.Options, roOpts...)) + res.Mode = strings.Join(res.Mount.Options, ",") + } + return res, nil } return nil, fmt.Errorf("invalid mount type '%s' must be a volume/bind/tmpfs", mountType) } diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index f4209bc5aae..2074a3c85d7 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -18,6 +18,8 @@ package mountutil import ( "context" + "errors" + "fmt" "slices" "strings" "testing" @@ -30,6 +32,20 @@ import ( "github.com/containerd/containerd/v2/pkg/oci" ) +// stubRROSupport replaces the detection of the recursive read-only (RRO) +// support (which executes `$RUNTIME features` on the real implementation) +// for unit testing. +func stubRROSupport(t *testing.T, supported bool) { + orig := supportsRecursivelyReadOnly + supportsRecursivelyReadOnly = func(string) error { + if supported { + return nil + } + return errors.New("recursive read-only mounts are not supported (stubbed)") + } + t.Cleanup(func() { supportsRecursivelyReadOnly = orig }) +} + // TestParseVolumeOptions tests volume options are parsed as expected. func TestParseVolumeOptions(t *testing.T) { tests := []struct { @@ -37,6 +53,7 @@ func TestParseVolumeOptions(t *testing.T) { vType string src string optsRaw string + rroSupported bool srcOptional []string initialRootfsPropagation string wants []string @@ -66,6 +83,29 @@ func TestParseVolumeOptions(t *testing.T) { optsRaw: "ro", wants: []string{"ro"}, }, + { + name: "read only is recursive when the kernel and the runtime support RRO (Docker v25 behavior)", + vType: "bind", + src: "dummy", + optsRaw: "ro", + rroSupported: true, + wants: []string{"rro", "rprivate"}, + }, + { + name: "deprecated rro option forces recursive read-only", + vType: "bind", + src: "dummy", + optsRaw: "rro,rprivate", + rroSupported: true, + wants: []string{"rro", "rprivate"}, + }, + { + name: "deprecated rro option fails when RRO is not supported", + vType: "bind", + src: "dummy", + optsRaw: "rro,rprivate", + wantFail: true, + }, { name: "duplicated flags are not allowed", vType: "bind", @@ -173,7 +213,8 @@ func TestParseVolumeOptions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - opts, specOpts, err := parseVolumeOptionsWithMountInfo(tt.vType, tt.src, tt.optsRaw, func(string) (mount.Info, error) { + stubRROSupport(t, tt.rroSupported) + opts, specOpts, err := parseVolumeOptionsWithMountInfo(tt.vType, tt.src, tt.optsRaw, "", func(string) (mount.Info, error) { return mount.Info{ Mountpoint: tt.src, Optional: strings.Join(tt.srcOptional, " "), @@ -268,7 +309,8 @@ func TestProcessFlagV(t *testing.T) { for _, tt := range tests { t.Run(tt.rawSpec, func(t *testing.T) { - processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, false) + stubRROSupport(t, false) + processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, false, "") if err != nil { assert.Error(t, err, tt.err) return @@ -333,7 +375,8 @@ func TestProcessFlagVAnonymousVolumes(t *testing.T) { for _, tt := range tests { t.Run(tt.rawSpec, func(t *testing.T) { - processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true) + stubRROSupport(t, false) + processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true, "") if err != nil { assert.ErrorContains(t, err, tt.err) return @@ -368,7 +411,7 @@ func TestProcessFlagMountRW(t *testing.T) { } for _, tt := range rejected { t.Run(tt.spec, func(t *testing.T) { - _, err := ProcessFlagMount(tt.spec, nil) + _, err := ProcessFlagMount(tt.spec, nil, "") assert.ErrorContains(t, err, tt.want) }) } @@ -376,8 +419,9 @@ func TestProcessFlagMountRW(t *testing.T) { // ro/rro still parse into a complete read-only bind mount. src := t.TempDir() accepted := []struct { - spec string - wants *Processed + spec string + rroSupported bool + wants *Processed }{ { spec: "type=bind,source=" + src + ",target=/bar,ro", @@ -387,26 +431,43 @@ func TestProcessFlagMountRW(t *testing.T) { Type: "bind", Source: src, Destination: "/bar", - Options: []string{"rbind", "ro", "rprivate"}, + Options: []string{"rbind", "rprivate", "ro"}, }, }, }, { - spec: "type=bind,source=" + src + ",target=/bar,rro", + // Read-only mounts are recursively read-only when possible (Docker v25 behavior). + spec: "type=bind,source=" + src + ",target=/bar,ro", + rroSupported: true, wants: &Processed{ Type: Bind, Mount: specs.Mount{ Type: "bind", Source: src, Destination: "/bar", - Options: []string{"rbind", "ro", "rro", "rprivate"}, + Options: []string{"rbind", "rprivate", "rro"}, + }, + }, + }, + { + // Deprecated alias of readonly,bind-propagation=rprivate,bind-recursive=readonly + spec: "type=bind,source=" + src + ",target=/bar,rro", + rroSupported: true, + wants: &Processed{ + Type: Bind, + Mount: specs.Mount{ + Type: "bind", + Source: src, + Destination: "/bar", + Options: []string{"rbind", "rprivate", "rro"}, }, }, }, } for _, tt := range accepted { - t.Run(tt.spec, func(t *testing.T) { - got, err := ProcessFlagMount(tt.spec, nil) + t.Run(fmt.Sprintf("%s (rroSupported=%v)", tt.spec, tt.rroSupported), func(t *testing.T) { + stubRROSupport(t, tt.rroSupported) + got, err := ProcessFlagMount(tt.spec, nil, "") assert.NilError(t, err) assert.Equal(t, got.Type, tt.wants.Type) assert.Equal(t, got.Mount.Type, tt.wants.Mount.Type) @@ -415,35 +476,95 @@ func TestProcessFlagMountRW(t *testing.T) { assert.DeepEqual(t, got.Mount.Options, tt.wants.Mount.Options) }) } + + // The deprecated rro option fails when RRO is not supported. + stubRROSupport(t, false) + _, err := ProcessFlagMount("type=bind,source="+src+",target=/bar,rro", nil, "") + assert.ErrorContains(t, err, "not supported") } // TestProcessFlagMountBindRecursive verifies that the Docker `bind-recursive` // option (which supersedes the deprecated `bind-nonrecursive`) is honored and -// maps to non-recursive (bind) or recursive (rbind) mounts. +// maps to non-recursive (bind) or recursive (rbind) mounts, and controls the +// recursive read-only (RRO) mode of read-only mounts. func TestProcessFlagMountBindRecursive(t *testing.T) { src := t.TempDir() accepted := []struct { - spec string - wantBindOpt string + spec string + rroSupported bool + wantOpts []string }{ - {"type=bind,source=" + src + ",target=/bar,bind-recursive=disabled", "bind"}, - {"type=bind,source=" + src + ",target=/bar,bind-recursive=enabled", "rbind"}, - {"type=bind,source=" + src + ",target=/bar,bind-recursive=false", "bind"}, - {"type=bind,source=" + src + ",target=/bar,bind-recursive=1", "rbind"}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=disabled", wantOpts: []string{"bind"}}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=enabled", wantOpts: []string{"rbind"}}, // The deprecated bind-nonrecursive option keeps working. - {"type=bind,source=" + src + ",target=/bar,bind-nonrecursive", "bind"}, - {"type=bind,source=" + src + ",target=/bar,bind-nonrecursive=false", "rbind"}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-nonrecursive", wantOpts: []string{"bind"}}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-nonrecursive=false", wantOpts: []string{"rbind"}}, + // bind-recursive=writable keeps the read-only mount non-recursively read-only (Docker v24 behavior). + { + spec: "type=bind,source=" + src + ",target=/bar,readonly,bind-recursive=writable", + rroSupported: true, + wantOpts: []string{"rbind", "ro"}, + }, + // bind-recursive=readonly forces the recursive read-only mount. + { + spec: "type=bind,source=" + src + ",target=/bar,readonly,bind-propagation=rprivate,bind-recursive=readonly", + rroSupported: true, + wantOpts: []string{"rbind", "rro"}, + }, } for _, tt := range accepted { t.Run(tt.spec, func(t *testing.T) { - got, err := ProcessFlagMount(tt.spec, nil) + stubRROSupport(t, tt.rroSupported) + got, err := ProcessFlagMount(tt.spec, nil, "") assert.NilError(t, err) - assert.Assert(t, slices.Contains(got.Mount.Options, tt.wantBindOpt), - "expected option %q in %v", tt.wantBindOpt, got.Mount.Options) + for _, o := range tt.wantOpts { + assert.Assert(t, slices.Contains(got.Mount.Options, o), + "expected option %q in %v", o, got.Mount.Options) + } }) } - _, err := ProcessFlagMount("type=bind,source="+src+",target=/bar,bind-recursive=bogus", nil) - assert.ErrorContains(t, err, "invalid value for bind-recursive") + rejected := []struct { + spec string + rroSupported bool + want string + }{ + // Boolean aliases were removed for Docker compatibility (https://github.com/docker/cli/pull/4671). + {spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=false", want: "invalid value for bind-recursive"}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=1", want: "invalid value for bind-recursive"}, + {spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=bogus", want: "invalid value for bind-recursive"}, + { + spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=writable", + want: "'bind-recursive=writable' requires 'readonly'", + }, + { + spec: "type=bind,source=" + src + ",target=/bar,bind-recursive=readonly", + want: "'bind-recursive=readonly' requires 'readonly'", + }, + { + spec: "type=bind,source=" + src + ",target=/bar,readonly,bind-recursive=readonly", + want: "'bind-recursive=readonly' requires 'bind-propagation=rprivate'", + }, + { + spec: "type=bind,source=" + src + ",target=/bar,readonly,bind-propagation=rprivate,bind-nonrecursive,bind-recursive=readonly", + want: "conflicts with 'bind-nonrecursive'", + }, + { + spec: "type=volume,source=foo,target=/bar,bind-recursive=enabled", + want: "only supported for bind mounts", + }, + // bind-recursive=readonly fails when RRO is not supported. + { + spec: "type=bind,source=" + src + ",target=/bar,readonly,bind-propagation=rprivate,bind-recursive=readonly", + want: "not supported", + }, + } + for _, tt := range rejected { + t.Run(tt.spec, func(t *testing.T) { + stubRROSupport(t, tt.rroSupported) + _, err := ProcessFlagMount(tt.spec, nil, "") + assert.ErrorContains(t, err, tt.want) + }) + } } diff --git a/pkg/mountutil/mountutil_windows.go b/pkg/mountutil/mountutil_windows.go index d036d420580..69394be4992 100644 --- a/pkg/mountutil/mountutil_windows.go +++ b/pkg/mountutil/mountutil_windows.go @@ -54,7 +54,7 @@ func UnprivilegedMountFlags(path string) ([]string, error) { // parseVolumeOptions parses specified optsRaw with using information of // the volume type and the src directory when necessary. -func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) { +func parseVolumeOptions(vType, src, optsRaw, ociRuntime string) ([]string, []oci.SpecOpts, error) { var writeModeRawOpts []string for _, opt := range strings.Split(optsRaw, ",") { switch opt { @@ -81,7 +81,7 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } -func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) { +func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) { return nil, errdefs.ErrNotImplemented } diff --git a/pkg/mountutil/mountutil_windows_test.go b/pkg/mountutil/mountutil_windows_test.go index aeb55d2bb2c..5695a7e0c1b 100644 --- a/pkg/mountutil/mountutil_windows_test.go +++ b/pkg/mountutil/mountutil_windows_test.go @@ -67,7 +67,7 @@ func TestParseVolumeOptions(t *testing.T) { } for _, tt := range tests { t.Run(strings.Join([]string{tt.vType, tt.src, tt.optsRaw}, "-"), func(t *testing.T) { - opts, _, err := parseVolumeOptions(tt.vType, tt.src, tt.optsRaw) + opts, _, err := parseVolumeOptions(tt.vType, tt.src, tt.optsRaw, "") if err != nil { if tt.wantFail { return @@ -270,7 +270,7 @@ func TestProcessFlagV(t *testing.T) { for _, tt := range tests { t.Run(tt.rawSpec, func(t *testing.T) { - processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true) + processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true, "") if err != nil { assert.Error(t, err, tt.err) return @@ -327,7 +327,7 @@ func TestProcessFlagVAnonymousVolumes(t *testing.T) { for _, tt := range tests { t.Run(tt.rawSpec, func(t *testing.T) { - processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true) + processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true, "") if err != nil { assert.ErrorContains(t, err, tt.err) return diff --git a/pkg/ociruntimeutil/ociruntimeutil.go b/pkg/ociruntimeutil/ociruntimeutil.go new file mode 100644 index 00000000000..88f05baf766 --- /dev/null +++ b/pkg/ociruntimeutil/ociruntimeutil.go @@ -0,0 +1,179 @@ +/* + 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 ociruntimeutil provides client-side utilities for inspecting OCI runtimes +// such as runc and crun. +package ociruntimeutil + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/opencontainers/runtime-spec/specs-go/features" + + "github.com/containerd/log" +) + +// BinaryFromRuntimeStr resolves the path of the OCI runtime binary from runtimeStr, +// which is the value of the `--runtime` flag of `nerdctl run`, e.g., +// "" (default), "io.containerd.runc.v2", "crun", or "/usr/local/sbin/runc". +// +// An error is returned when the binary cannot be determined, e.g., for a shim +// like "io.containerd.kata.v2" that does not expose the runtime binary name. +// +// The resolution is a best-effort guess of the client and may not match the +// actual binary used by the containerd daemon, e.g., when the daemon overrides +// the BinaryName option of the "io.containerd.runc.v2" shim in its config. +func BinaryFromRuntimeStr(runtimeStr string) (string, error) { + if runtimeStr == "" || strings.HasPrefix(runtimeStr, "io.containerd.runc.") { + // The "io.containerd.runc.v2" shim executes "runc" from $PATH by default. + return exec.LookPath("runc") + } + if strings.HasPrefix(runtimeStr, "io.containerd.") || runtimeStr == "wtf.sbk.runj.v1" { + return "", fmt.Errorf("cannot determine the OCI runtime binary for runtime %q", runtimeStr) + } + // runtimeStr refers to a binary such as "crun" or "/usr/local/sbin/runc" + // (consistent with generateRuntimeCOpts in pkg/cmd/container). + binary, err := exec.LookPath(runtimeStr) + if err != nil { + return "", fmt.Errorf("cannot determine the OCI runtime binary for runtime %q: %w", runtimeStr, err) + } + return binary, nil +} + +var ( + featuresCacheMu sync.Mutex + featuresCache = make(map[string]*features.Features) // key: the resolved binary path +) + +// Features returns the parsed output of ` features`. +// https://github.com/opencontainers/runtime-spec/blob/v1.2.1/features.md +// +// The `features` subcommand is supported by runc >= 1.1 and crun >= 1.8.6. +// +// The result is cached in the XDG cache directory (e.g., ~/.cache/nerdctl/oci-runtime-features), +// with the cache entry being invalidated when the binary is modified. +func Features(binary string) (*features.Features, error) { + binPath, err := exec.LookPath(binary) + if err != nil { + return nil, err + } + realPath, err := filepath.EvalSymlinks(binPath) + if err != nil { + return nil, err + } + + featuresCacheMu.Lock() + defer featuresCacheMu.Unlock() + if f, ok := featuresCache[realPath]; ok { + return f, nil + } + + cachePath, err := featuresCachePath(realPath) + if err != nil { + log.L.WithError(err).Debugf("failed to determine the cache path for the features of %q", realPath) + cachePath = "" + } + if cachePath != "" { + // The cache entry is valid only when it is newer than the binary. + if stale, err := isCacheStale(cachePath, realPath); err == nil && !stale { + if b, err := os.ReadFile(cachePath); err == nil { + var f features.Features + if err = json.Unmarshal(b, &f); err == nil { + featuresCache[realPath] = &f + return &f, nil + } + log.L.WithError(err).Warnf("failed to parse the cached OCI runtime features %q (ignored)", cachePath) + } + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, binPath, "features") + out, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { + err = fmt.Errorf("%w: %s", err, strings.TrimSpace(string(ee.Stderr))) + } + return nil, fmt.Errorf("failed to run `%s features`: %w", binPath, err) + } + var f features.Features + if err = json.Unmarshal(out, &f); err != nil { + return nil, fmt.Errorf("failed to parse the output of `%s features`: %w", binPath, err) + } + featuresCache[realPath] = &f + if cachePath != "" { + if err = writeFileAtomically(cachePath, out); err != nil { + log.L.WithError(err).Debugf("failed to cache the OCI runtime features to %q (ignored)", cachePath) + } + } + return &f, nil +} + +// featuresCachePath returns the cache file path for the features of the binary. +func featuresCachePath(realPath string) (string, error) { + // os.UserCacheDir returns $XDG_CACHE_HOME (or ~/.cache) on Linux. + cacheHome, err := os.UserCacheDir() + if err != nil { + return "", err + } + h := sha256.Sum256([]byte(realPath)) + return filepath.Join(cacheHome, "nerdctl", "oci-runtime-features", hex.EncodeToString(h[:])+".json"), nil +} + +// isCacheStale returns whether the cache file is older than the binary, +// i.e., the binary was modified after the cache entry was created. +func isCacheStale(cachePath, realPath string) (bool, error) { + stCache, err := os.Stat(cachePath) + if err != nil { + return true, err + } + stBin, err := os.Stat(realPath) + if err != nil { + return true, err + } + return !stCache.ModTime().After(stBin.ModTime()), nil +} + +func writeFileAtomically(path string, b []byte) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + tmp, err := os.CreateTemp(dir, ".tmp-"+filepath.Base(path)) + if err != nil { + return err + } + defer os.Remove(tmp.Name()) + if _, err = tmp.Write(b); err != nil { + tmp.Close() + return err + } + if err = tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), path) +} diff --git a/pkg/ociruntimeutil/ociruntimeutil_linux_test.go b/pkg/ociruntimeutil/ociruntimeutil_linux_test.go new file mode 100644 index 00000000000..191b4f618f8 --- /dev/null +++ b/pkg/ociruntimeutil/ociruntimeutil_linux_test.go @@ -0,0 +1,113 @@ +/* + 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 ociruntimeutil + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/opencontainers/runtime-spec/specs-go/features" + "gotest.tools/v3/assert" +) + +// fakeRuntime creates a fake OCI runtime binary whose `features` subcommand +// prints featuresJSON, and appends a line to the log file on every execution. +func fakeRuntime(t *testing.T, featuresJSON string) (binary, execLog string) { + t.Helper() + dir := t.TempDir() + binary = filepath.Join(dir, "fake-runtime") + execLog = filepath.Join(dir, "exec.log") + script := `#!/bin/sh +set -eu +echo executed >>` + execLog + ` +if [ "${1:-}" != "features" ]; then + echo >&2 "unknown command ${1:-}" + exit 1 +fi +cat <<'EOF' +` + featuresJSON + ` +EOF +` + assert.NilError(t, os.WriteFile(binary, []byte(script), 0o700)) + return binary, execLog +} + +func countLines(t *testing.T, path string) int { + t.Helper() + b, err := os.ReadFile(path) + if os.IsNotExist(err) { + return 0 + } + assert.NilError(t, err) + n := 0 + for _, c := range b { + if c == '\n' { + n++ + } + } + return n +} + +func TestFeatures(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + binary, execLog := fakeRuntime(t, `{"ociVersionMin": "1.0.0", "ociVersionMax": "1.2.0", "mountOptions": ["ro", "rro", "rbind"]}`) + + f, err := Features(binary) + assert.NilError(t, err) + assert.DeepEqual(t, []string{"ro", "rro", "rbind"}, f.MountOptions) + assert.Equal(t, 1, countLines(t, execLog)) + + // The second call must not execute the binary again (in-process cache). + f, err = Features(binary) + assert.NilError(t, err) + assert.DeepEqual(t, []string{"ro", "rro", "rbind"}, f.MountOptions) + assert.Equal(t, 1, countLines(t, execLog)) + + // Drop the in-process cache: the XDG cache must be used, still without executing the binary. + featuresCacheMu.Lock() + featuresCache = make(map[string]*features.Features) + featuresCacheMu.Unlock() + f, err = Features(binary) + assert.NilError(t, err) + assert.DeepEqual(t, []string{"ro", "rro", "rbind"}, f.MountOptions) + assert.Equal(t, 1, countLines(t, execLog)) + + // Modifying the binary must invalidate the XDG cache entry + // (the cache file is older than the binary now). + // The mtime is set explicitly, as the timestamps of the cache file and the + // binary might collide otherwise. + future := time.Now().Add(time.Hour) + assert.NilError(t, os.Chtimes(binary, future, future)) + featuresCacheMu.Lock() + featuresCache = make(map[string]*features.Features) + featuresCacheMu.Unlock() + f, err = Features(binary) + assert.NilError(t, err) + assert.DeepEqual(t, []string{"ro", "rro", "rbind"}, f.MountOptions) + assert.Equal(t, 2, countLines(t, execLog)) +} + +func TestFeaturesError(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + dir := t.TempDir() + binary := filepath.Join(dir, "fake-runtime-no-features") + assert.NilError(t, os.WriteFile(binary, []byte("#!/bin/sh\necho >&2 'unknown command'\nexit 1\n"), 0o700)) + _, err := Features(binary) + assert.ErrorContains(t, err, "unknown command") +} diff --git a/pkg/ociruntimeutil/rro_linux.go b/pkg/ociruntimeutil/rro_linux.go new file mode 100644 index 00000000000..8f069dd35fd --- /dev/null +++ b/pkg/ociruntimeutil/rro_linux.go @@ -0,0 +1,71 @@ +/* + 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 ociruntimeutil + +import ( + "errors" + "fmt" + "slices" + "sync" + + "github.com/containerd/containerd/v2/pkg/kernelversion" +) + +var ( + rroCacheMu sync.Mutex + rroCache = make(map[string]error) // key: runtimeStr +) + +// SupportsRecursivelyReadOnly returns nil when the kernel and the OCI runtime +// specified by runtimeStr (the value of the `--runtime` flag) support +// recursive read-only (RRO) bind mounts. +// The result is cached per runtimeStr for the lifetime of the process. +func SupportsRecursivelyReadOnly(runtimeStr string) error { + rroCacheMu.Lock() + defer rroCacheMu.Unlock() + if err, ok := rroCache[runtimeStr]; ok { + return err + } + err := supportsRecursivelyReadOnly(runtimeStr) + rroCache[runtimeStr] = err + return err +} + +func supportsRecursivelyReadOnly(runtimeStr string) error { + // Recursive read-only mounts (mount_setattr(2) with MOUNT_ATTR_RDONLY and + // AT_RECURSIVE) require kernel >= 5.12. + ok, err := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{Kernel: 5, Major: 12}) + if err != nil { + return fmt.Errorf("failed to detect whether the kernel supports recursive read-only mounts: %w", err) + } + if !ok { + return errors.New("recursive read-only mounts require kernel >= 5.12") + } + binary, err := BinaryFromRuntimeStr(runtimeStr) + if err != nil { + return fmt.Errorf("failed to detect whether the OCI runtime supports recursive read-only mounts: %w", err) + } + f, err := Features(binary) + if err != nil { + return fmt.Errorf("failed to detect whether the OCI runtime %q supports recursive read-only mounts (hint: recursive read-only mounts require runc >= 1.1 or crun >= 1.8.6): %w", + binary, err) + } + if !slices.Contains(f.MountOptions, "rro") { + return fmt.Errorf("the OCI runtime %q does not support recursive read-only (\"rro\") mounts", binary) + } + return nil +} From 7df1def0e470538eb083a44a9b8fc65690afc669 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Tue, 30 Jun 2026 11:16:11 +0530 Subject: [PATCH 679/868] feat(network): add --ipv4 to allow IPv6-only networks Docker's network create takes --ipv4 (default on) so that --ipv4=false together with --ipv6 yields an IPv6-only network. nerdctl had no equivalent: generateIPAM always appended a default IPv4 range when no v4 subnet was given, so every bridge network ended up with IPv4. Add the flag and carry it as a positive IPv4 option, matching the existing IPv6 field and docker's EnableIPv4. When IPv4 is off, skip the default v4 range, switch the host-local default route to ::/0, and reject an IPv4 subnet. Require at least one address family (docker's "IPv4 or IPv6 must be enabled"), and require an explicit IPv6 subnet since nerdctl does not auto-allocate one. Reject the combination on Windows where IPv6-only is unsupported. Part of #5012. Signed-off-by: Mayur Das --- cmd/nerdctl/network/network_create.go | 6 ++++ .../network/network_create_linux_test.go | 30 +++++++++++++++++++ docs/command-reference.md | 1 + pkg/api/types/network_types.go | 6 +++- pkg/cmd/network/create.go | 13 ++++++++ pkg/netutil/netutil.go | 4 ++- pkg/netutil/netutil_unix.go | 17 +++++++++-- pkg/netutil/netutil_windows.go | 6 +++- 8 files changed, 77 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index f8b5dc85822..596fa5f785f 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -50,6 +50,7 @@ func createCommand() *cobra.Command { cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") cmd.Flags().StringArray("ip-range", nil, `Allocate container ip from a sub-range`) cmd.Flags().StringArray("label", nil, "Set metadata for a network") + cmd.Flags().Bool("ipv4", true, "Enable IPv4 networking (set to false together with --ipv6 for an IPv6-only network)") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") cmd.Flags().Bool("internal", false, "Restrict external access to the network") return cmd @@ -97,6 +98,10 @@ func createAction(cmd *cobra.Command, args []string) error { return err } labels = strutil.DedupeStrSlice(labels) + ipv4, err := cmd.Flags().GetBool("ipv4") + if err != nil { + return err + } ipv6, err := cmd.Flags().GetBool("ipv6") if err != nil { return err @@ -118,6 +123,7 @@ func createAction(cmd *cobra.Command, args []string) error { IPRange: ipRanges, Labels: labels, IPv6: ipv6, + IPv4: &ipv4, Internal: internal, }, cmd.OutOrStdout()) } diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index 76992c85c47..d0d113e7e8d 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -179,6 +179,36 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "ipv6-only with --ipv4=false", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + subnetStr := "2001:db8:9::/64" + data.Labels().Set("subnetStr", subnetStr) + _, _, err := net.ParseCIDR(subnetStr) + assert.Assert(t, err == nil) + + helpers.Ensure("network", "create", data.Identifier(), "--ipv6", "--ipv4=false", "--subnet", subnetStr) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "addr", "show", "dev", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + _, subnet, _ := net.ParseCIDR(data.Labels().Get("subnetStr")) + ip := nerdtest.FindIPv6(stdout) + assert.Assert(t, subnet.Contains(ip), fmt.Sprintf("subnet %s contains ip %s", subnet, ip)) + // With IPv4 disabled the interface must not get a v4 address. + assert.Assert(t, !strings.Contains(stdout, "inet "), "eth0 should have no IPv4 address") + }, + } + }, + }, { Description: "internal enabled", Setup: func(data test.Data, helpers test.Helpers) { diff --git a/docs/command-reference.md b/docs/command-reference.md index 726655e9ea2..a027ef9f128 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1280,6 +1280,7 @@ Flags: - :whale: `--gateway`: IPv4 or IPv6 Gateway for the master subnet - :whale: `--ip-range`: Allocate container ip from a sub-range - :whale: `--label`: Set metadata on a network +- :whale: `--ipv4`: Enable IPv4. Enabled by default; set to false with `--ipv6` and an IPv6 subnet for an IPv6-only network. `--ipv4=false` is not supported on Windows. - :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet. - :whale: `--internal`: Restrict external access to the network. diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go index a2df90ecf84..90969d6612e 100644 --- a/pkg/api/types/network_types.go +++ b/pkg/api/types/network_types.go @@ -35,7 +35,11 @@ type NetworkCreateOptions struct { IPRange []string Labels []string IPv6 bool - Internal bool + // IPv4 enables IPv4 on the network. A nil value defaults to enabled, so a + // directly-constructed NetworkCreateOptions keeps IPv4 on; setting it to + // false together with IPv6 yields an IPv6-only network. + IPv4 *bool + Internal bool } // NetworkInspectOptions specifies options for `nerdctl network inspect`. diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index 89c48cc0db9..f64ba519f3a 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -27,6 +27,19 @@ import ( ) func Create(options types.NetworkCreateOptions, stdout io.Writer) error { + // A nil IPv4 defaults to enabled. + ipv4 := options.IPv4 == nil || *options.IPv4 + // At least one address family must be enabled, matching docker which + // rejects a network with both IPv4 and IPv6 turned off. + if !ipv4 && !options.IPv6 { + return fmt.Errorf("IPv4 or IPv6 must be enabled") + } + if !ipv4 && len(options.Subnets) == 0 { + // IPv6-only needs a concrete IPv6 subnet: unlike docker, nerdctl does + // not auto-allocate one, and the empty-subnet default below would pick + // an IPv4 range, contradicting the disabled IPv4. + return fmt.Errorf("IPv6-only network requires an IPv6 subnet, specify --subnet manually") + } if len(options.Subnets) == 0 { if len(options.Gateway) > 0 || len(options.IPRange) > 0 { return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index e95c17fbb4d..026b210df65 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -339,7 +339,9 @@ func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, if _, ok := netMap[opts.Name]; ok { return nil, errdefs.ErrAlreadyExists } - ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6, opts.Internal) + // A nil IPv4 defaults to enabled. + ipv4 := opts.IPv4 == nil || *opts.IPv4 + ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6, ipv4, opts.Internal) if err != nil { return nil, err } diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index a3cd38c6f93..7e2549da449 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -215,24 +215,35 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": ipamConf := newHostLocalIPAMConfig() if !internal { + // An IPv6-only network has no IPv4 gateway, so its default route + // must be the IPv6 one; otherwise host-local installs an IPv4 + // default route with no matching range. + defaultRoute := "0.0.0.0/0" + if !ipv4 { + defaultRoute = "::/0" + } ipamConf.Routes = []IPAMRoute{ - {Dst: "0.0.0.0/0"}, + {Dst: defaultRoute}, } } ranges, findIPv4, err := e.parseIPAMRanges(subnets, gateways, ipRanges, ipv6) if err != nil { return nil, err } + if !ipv4 && findIPv4 { + return nil, fmt.Errorf("--ipv4=false conflicts with an IPv4 subnet") + } ipamConf.Ranges = append(ipamConf.Ranges, ranges...) - if !findIPv4 { + if ipv4 && !findIPv4 { // The default IPv4 range uses a computed gateway and no ip-range; // any user-supplied gateway or ip-range belongs to an explicit subnet. + // Skipped when IPv4 is disabled, leaving the network IPv6-only. ranges, _, _ = e.parseIPAMRanges([]string{""}, nil, nil, ipv6) ipamConf.Ranges = append(ipamConf.Ranges, ranges...) } diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 5d204c4161d..6fef605b2f3 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -72,12 +72,16 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6 bool, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, error) { switch driver { case "default": default: return nil, fmt.Errorf("unsupported ipam driver %q", driver) } + // IPv6-only networks are not supported on Windows. + if !ipv4 { + return nil, fmt.Errorf("--ipv4=false is not supported on Windows") + } // Windows is single-subnet, so use at most one gateway and one ip-range. gatewayStr := "" From 33a185026af33b58995129e68f2d828df4f4b610 Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Thu, 9 Jul 2026 17:14:12 +0100 Subject: [PATCH 680/868] test: refactor container_list_linux_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container/container_list_linux_test.go | 1273 +++++++++-------- 1 file changed, 716 insertions(+), 557 deletions(-) diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index 38420293575..9d23eaa73cd 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -17,9 +17,7 @@ package container import ( - "errors" "fmt" - "os" "slices" "strings" "testing" @@ -39,59 +37,39 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -type psTestContainer struct { - name string - labels map[string]string - volumes []string - network string -} - -// When keepAlive is false, the container will exit immediately with status 1. -func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*testutil.Base, psTestContainer) { - base := testutil.NewBase(t) - - base.Cmd("pull", "--quiet", testutil.CommonImage).AssertOK() - - testContainerName := testutil.Identifier(t) + identity - rwVolName := testContainerName + "-rw" - // A container can mount named and anonymous volumes - rwDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - base.Cmd("network", "create", testContainerName).AssertOK() - t.Cleanup(func() { - base.Cmd("rm", "-f", testContainerName).AssertOK() - base.Cmd("volume", "rm", "-f", rwVolName).Run() - base.Cmd("network", "rm", testContainerName).Run() - os.RemoveAll(rwDir) - }) +// setupPsTestContainer creates a test container with labels, volumes, and network. +// When keepAlive is false, the container exits immediately with status 1. +// Container info is stored in data.Labels() keyed by identity prefix: +// - container-{identity}: container name +// - network-{identity}: network name (same as container) +// - vol-{identity}: named volume name +// - labelkey-{identity}: label key +// - labelval-{identity}: label value +// - volume-{identity}-{0..3}: volume mount components for filter tests +func setupPsTestContainer(data test.Data, helpers test.Helpers, identity string, keepAlive bool) { + containerName := data.Identifier(identity) + rwVolName := containerName + "-rw" + rwDir := data.Temp().Dir(identity + "-rw") + + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + helpers.Ensure("network", "create", containerName) + helpers.Ensure("volume", "create", rwVolName) // A container can have multiple labels. - // Therefore, this test container has multiple labels to check it. + // Therefore, this test container has labels to check. testLabels := make(map[string]string) - keys := []string{ - testutil.Identifier(t) + identity, - testutil.Identifier(t) + identity, - } - // fill the value of testLabels - for _, k := range keys { - testLabels[k] = k - } - base.Cmd("volume", "create", rwVolName).AssertOK() + testLabels[containerName] = containerName + mnt1 := fmt.Sprintf("%s:/%s_mnt1", rwDir, identity) mnt2 := fmt.Sprintf("%s:/%s_mnt3", rwVolName, identity) args := []string{ - "run", - "-d", - "--name", - testContainerName, - "--label", - formatter.FormatLabels(testLabels), + "run", "-d", + "--name", containerName, + "--label", formatter.FormatLabels(testLabels), "-v", mnt1, "-v", mnt2, - "--net", testContainerName, + "--net", containerName, } if keepAlive { args = append(args, testutil.CommonImage, "top") @@ -99,576 +77,757 @@ func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*tes args = append(args, "--restart=no", testutil.CommonImage, "false") } - base.Cmd(args...).AssertOK() + helpers.Ensure(args...) if keepAlive { - base.EnsureContainerStarted(testContainerName) + nerdtest.EnsureContainerStarted(helpers, containerName) + // dd if=/dev/zero of=test_file bs=1M count=25 + // let the container occupy 25MiB space. + helpers.Ensure("exec", containerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25") } else { - base.EnsureContainerExited(testContainerName, 1) + nerdtest.EnsureContainerExited(helpers, containerName, 1) } - // dd if=/dev/zero of=test_file bs=1M count=25 - // let the container occupy 25MiB space. - if keepAlive { - base.Cmd("exec", testContainerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25").AssertOK() - } + data.Labels().Set("container-"+identity, containerName) + data.Labels().Set("network-"+identity, containerName) + data.Labels().Set("vol-"+identity, rwVolName) + data.Labels().Set("labelkey-"+identity, containerName) + data.Labels().Set("labelval-"+identity, containerName) + volumes := []string{} volumes = append(volumes, strings.Split(mnt1, ":")...) volumes = append(volumes, strings.Split(mnt2, ":")...) - - return base, psTestContainer{ - name: testContainerName, - labels: testLabels, - volumes: volumes, - network: testContainerName, + for i, v := range volumes { + data.Labels().Set(fmt.Sprintf("volume-%s-%d", identity, i), v) } } -func TestContainerList(t *testing.T) { - base, testContainer := preparePsTestContainer(t, "list", true) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { - // An example of nerdctl/docker ps -n 1 -s - // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE - // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } +// cleanupPsTestContainer removes the container, volume, and network created by setupPsTestContainer. +func cleanupPsTestContainer(data test.Data, helpers test.Helpers, identity string) { + containerName := data.Identifier(identity) + helpers.Anyhow("rm", "-f", containerName) + helpers.Anyhow("volume", "rm", "-f", containerName+"-rw") + helpers.Anyhow("network", "rm", containerName) +} - container, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, container, testContainer.name) +func TestContainerList(t *testing.T) { + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "list", true) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "list") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-n", "1", "-s") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerName := data.Labels().Get("container-list") + + // An example of nerdctl/docker ps -n 1 -s + // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE + // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) - image, _ := tab.ReadRow(lines[1], "IMAGE") - assert.Equal(t, image, testutil.CommonImage) + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "failed to parse header") - size, _ := tab.ReadRow(lines[1], "SIZE") + container, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, container, containerName) - // there is some difference between nerdctl and docker in calculating the size of the container - expectedSize := "26.2MB (virtual " - if !nerdtest.IsDocker() { - expectedSize = "25.0 MiB (virtual " - } + image, _ := tab.ReadRow(lines[1], "IMAGE") + assert.Equal(t, image, testutil.CommonImage) - if !strings.Contains(size, expectedSize) { - return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) + size, _ := tab.ReadRow(lines[1], "SIZE") + // there is some difference between nerdctl and docker in calculating the size of the container + expectedSize := "26.2MB (virtual " + if !nerdtest.IsDocker() { + expectedSize = "25.0 MiB (virtual " + } + assert.Assert(t, strings.Contains(size, expectedSize), + "expect container size %s, but got %s", expectedSize, size) + }, } - - return nil - }) + } + testCase.Run(t) } func TestContainerListWideMode(t *testing.T) { - testutil.DockerIncompatible(t) - base, testContainer := preparePsTestContainer(t, "listWithMode", true) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error { - - // An example of nerdctl ps --format wide - // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE - // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "listWithMode", true) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "listWithMode") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-n", "1", "--format", "wide") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerName := data.Labels().Get("container-listWithMode") + + // An example of nerdctl ps --format wide + // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE + // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "failed to parse header") - container, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, container, testContainer.name) + container, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, container, containerName) - image, _ := tab.ReadRow(lines[1], "IMAGE") - assert.Equal(t, image, testutil.CommonImage) + image, _ := tab.ReadRow(lines[1], "IMAGE") + assert.Equal(t, image, testutil.CommonImage) - runtime, _ := tab.ReadRow(lines[1], "RUNTIME") - assert.Equal(t, runtime, "io.containerd.runc.v2") + runtime, _ := tab.ReadRow(lines[1], "RUNTIME") + assert.Equal(t, runtime, "io.containerd.runc.v2") - size, _ := tab.ReadRow(lines[1], "SIZE") - expectedSize := "25.0 MiB (virtual " - if !strings.Contains(size, expectedSize) { - return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) + size, _ := tab.ReadRow(lines[1], "SIZE") + expectedSize := "25.0 MiB (virtual " + assert.Assert(t, strings.Contains(size, expectedSize), + "expect container size %s, but got %s", expectedSize, size) + }, } - return nil - }) + } + testCase.Run(t) } func TestContainerListWithLabels(t *testing.T) { - base, testContainer := preparePsTestContainer(t, "listWithLabels", true) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error { - - // An example of nerdctl ps --format "{{.Labels}}" - // key1=value1,key2=value2,key3=value3 - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %d", len(lines)) - } - - // check labels using map - // 1. the results has no guarantee to show the same order. - // 2. the results has no guarantee to show only configured labels. - labelsMap, err := strutil.ParseCSVMap(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse labels: %v", err) - } - - for i := range testContainer.labels { - if value, ok := labelsMap[i]; ok { - assert.Equal(t, value, testContainer.labels[i]) - } + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "listWithLabels", true) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "listWithLabels") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-n", "1", "--format", "{{.Labels}}") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + // An example of nerdctl ps --format "{{.Labels}}" + // key1=value1,key2=value2,key3=value3 + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Equal(t, len(lines), 1, "expected 1 line") + + // check labels using map + // 1. the results has no guarantee to show the same order. + // 2. the results has no guarantee to show only configured labels. + labelsMap, err := strutil.ParseCSVMap(lines[0]) + assert.NilError(t, err, "failed to parse labels") + + labelKey := data.Labels().Get("labelkey-listWithLabels") + labelVal := data.Labels().Get("labelval-listWithLabels") + if value, ok := labelsMap[labelKey]; ok { + assert.Equal(t, value, labelVal) + } + }, } - return nil - }) + } + testCase.Run(t) } func TestContainerListWithNames(t *testing.T) { - base, testContainer := preparePsTestContainer(t, "listWithNames", true) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "--format", "{{.Names}}").AssertOutWithFunc(func(stdout string) error { + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "listWithNames", true) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "listWithNames") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-n", "1", "--format", "{{.Names}}") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerName := data.Labels().Get("container-listWithNames") - // An example of nerdctl ps --format "{{.Names}}" - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %d", len(lines)) + // An example of nerdctl ps --format "{{.Names}}" + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Equal(t, len(lines), 1, "expected 1 line") + assert.Equal(t, lines[0], containerName) + }, } - - assert.Equal(t, lines[0], testContainer.name) - - return nil - }) + } + testCase.Run(t) } func TestContainerListWithFilter(t *testing.T) { - base, testContainerA := preparePsTestContainer(t, "listWithFilterA", true) - _, testContainerB := preparePsTestContainer(t, "listWithFilterB", true) - _, testContainerC := preparePsTestContainer(t, "listWithFilterC", false) - - base.Cmd("ps", "--filter", "name="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - - containerName, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, containerName, testContainerA.name) - id, _ := tab.ReadRow(lines[1], "CONTAINER ID") - base.Cmd("ps", "-q", "--filter", "id="+id).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %d", len(lines)) - } - if lines[0] != id { - return errors.New("failed to filter by id") - } - return nil - }) - base.Cmd("ps", "-q", "--filter", "id="+id+id).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) > 0 { - for _, line := range lines { - if line != "" { - return fmt.Errorf("unexpected container found: %s", line) - } - } - } - return nil - }) - base.Cmd("ps", "-q", "--filter", "id=").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) > 0 { - for _, line := range lines { - if line != "" { - return fmt.Errorf("unexpected container found: %s", line) - } - } - } - return nil - }) - return nil - }) - - // should support regexp - base.Cmd("ps", "--filter", "name=.*"+testContainerA.name+".*").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - - containerName, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, containerName, testContainerA.name) - return nil - }) - - // fully anchored regexp - base.Cmd("ps", "--filter", "name=^"+testContainerA.name+"$").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "A", true) + setupPsTestContainer(data, helpers, "B", true) + setupPsTestContainer(data, helpers, "C", false) - containerName, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, containerName, testContainerA.name) - return nil - }) - - base.Cmd("ps", "-q", "--filter", "name="+testContainerA.name+testContainerA.name).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) > 0 { - for _, line := range lines { - if line != "" { - return fmt.Errorf("unexpected container found: %s", line) - } - } - } - return nil - }) + containerA := data.Labels().Get("container-A") + containerB := data.Labels().Get("container-B") - base.Cmd("ps", "-q", "--filter", "name=").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) == 0 { - return errors.New("expect at least 1 container, got 0") - } - return nil - }) + ctrA := nerdtest.InspectContainer(helpers, containerA) + data.Labels().Set("fullIdA", ctrA.ID) + data.Labels().Set("shortIdA", ctrA.ID[:12]) - base.Cmd("ps", "--filter", "name=listWithFilter").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 3 { - return fmt.Errorf("expected at least 3 lines, got %d", len(lines)) - } + ctrB := nerdtest.InspectContainer(helpers, containerB) + data.Labels().Set("fullIdB", ctrB.ID) - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerNames := map[string]struct{}{ - testContainerA.name: {}, testContainerB.name: {}, - } - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if _, ok := containerNames[containerName]; !ok { - return fmt.Errorf("unexpected container %s found", containerName) - } - } - return nil - }) - - // docker filter by id only support full ID no truncate - // https://github.com/docker/for-linux/issues/258 - // yet nerdctl also support truncate ID - base.Cmd("ps", "--no-trunc", "--filter", "since="+testContainerA.name).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - var id string - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if containerName != testContainerB.name { - return fmt.Errorf("unexpected container %s found", containerName) + commonLen := 0 + for commonLen < len(containerA) && commonLen < len(containerB) { + if containerA[commonLen] != containerB[commonLen] { + break } - id, _ = tab.ReadRow(line, "CONTAINER ID") + commonLen++ } - base.Cmd("ps", "--filter", "before="+id).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } + data.Labels().Set("commonPrefix", strings.TrimRight(containerA[:commonLen], "-")) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "A") + cleanupPsTestContainer(data, helpers, "B") + cleanupPsTestContainer(data, helpers, "C") + } + testCase.SubTests = containerListFilterSubTests() + testCase.Run(t) +} - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - foundA := false - for idx, line := range lines { - if idx == 0 { - continue +func containerListFilterSubTests() []*test.Case { + return []*test.Case{ + { + Description: "filter by name shows correct container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "name="+data.Labels().Get("container-A")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, containerA) + }, } - containerName, _ := tab.ReadRow(line, "NAMES") - if containerName == testContainerA.name { - foundA = true - break + }, + }, + { + Description: "filter by truncated id", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-q", "--filter", "id="+data.Labels().Get("shortIdA")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + shortID := data.Labels().Get("shortIdA") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Equal(t, len(lines), 1, fmt.Sprintf("expected 1 line, got %d", len(lines))) + assert.Equal(t, lines[0], shortID) + }, } - } - // there are other containers such as **wordpress** could be listed since - // their created times are ahead of testContainerB too - if !foundA { - return fmt.Errorf("expected container %s not found", testContainerA.name) - } - return nil - }) - return nil - }) - - // docker filter by id only support full ID no truncate - // https://github.com/docker/for-linux/issues/258 - // yet nerdctl also support truncate ID - base.Cmd("ps", "--no-trunc", "--filter", "before="+testContainerB.name).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - foundA := false - var id string - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if containerName == testContainerA.name { - foundA = true - id, _ = tab.ReadRow(line, "CONTAINER ID") - break - } - } - // there are other containers such as **wordpress** could be listed since - // their created times are ahead of testContainerB too - if !foundA { - return fmt.Errorf("expected container %s not found", testContainerA.name) - } - base.Cmd("ps", "--filter", "since="+id).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - for idx, line := range lines { - if idx == 0 { - continue + }, + }, + { + Description: "filter by doubled id returns empty", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + id := data.Labels().Get("shortIdA") + return helpers.Command("ps", "-q", "--filter", "id="+id+id) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + assert.Equal(t, line, "", "unexpected container found: "+line) + } + }, } - containerName, _ := tab.ReadRow(line, "NAMES") - if containerName != testContainerB.name { - return fmt.Errorf("unexpected container %s found", containerName) + }, + }, + { + Description: "filter by empty id returns empty", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-q", "--filter", "id=") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + assert.Equal(t, line, "", "unexpected container found: "+line) + } + }, } - } - return nil - }) - return nil - }) - - for _, testContainer := range []psTestContainer{testContainerA, testContainerB} { - for _, volume := range testContainer.volumes { - base.Cmd("ps", "--filter", "volume="+volume).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) + }, + }, + { + Description: "filter by name regexp", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerA := data.Labels().Get("container-A") + return helpers.Command("ps", "--filter", "name=.*"+containerA+".*") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, containerA) + }, } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) + }, + }, + { + Description: "filter by name anchored regexp", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerA := data.Labels().Get("container-A") + return helpers.Command("ps", "--filter", "name=^"+containerA+"$") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, containerA) + }, } - containerName, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, containerName, testContainer.name) - return nil - }) - } - } - - base.Cmd("ps", "--filter", "network="+testContainerA.network).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerName, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, containerName, testContainerA.name) - return nil - }) - - for key, value := range testContainerB.labels { - base.Cmd("ps", "--filter", "label="+key+"="+value).AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerNames := map[string]struct{}{ - testContainerB.name: {}, - } - for idx, line := range lines { - if idx == 0 { - continue + }, + }, + { + Description: "filter by doubled name returns empty", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerA := data.Labels().Get("container-A") + return helpers.Command("ps", "-q", "--filter", "name="+containerA+containerA) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + assert.Equal(t, line, "", "unexpected container found: "+line) + } + }, } - containerName, _ := tab.ReadRow(line, "NAMES") - if _, ok := containerNames[containerName]; !ok { - return fmt.Errorf("unexpected container %s found", containerName) + }, + }, + { + Description: "filter by empty name returns all", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-q", "--filter", "name=") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expect at least 1 container, got 0") + }, } - } - return nil - }) + }, + }, + { + Description: "filter by partial name shows multiple containers", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "name="+data.Labels().Get("commonPrefix")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + containerB := data.Labels().Get("container-B") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + containerNames := map[string]struct{}{ + containerA: {}, containerB: {}, + } + for idx, line := range lines { + if idx == 0 { + continue + } + containerName, _ := tab.ReadRow(line, "NAMES") + _, ok := containerNames[containerName] + assert.Assert(t, ok, "unexpected container %s found", containerName) + } + }, + } + }, + }, + // docker filter by id only support full ID no truncate + // https://github.com/docker/for-linux/issues/258 + // yet nerdctl also support truncate ID + { + Description: "filter since by name shows only later container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--no-trunc", "--filter", "since="+data.Labels().Get("container-A")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerB := data.Labels().Get("container-B") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + name, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, name, containerB, "unexpected container found") + } + }, + } + }, + }, + { + Description: "filter before by full id includes earlier container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "before="+data.Labels().Get("fullIdB")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + // there are other containers that could be listed since + // their created times are ahead of containerB too + foundA := false + for idx, line := range lines { + if idx == 0 { + continue + } + name, _ := tab.ReadRow(line, "NAMES") + if name == containerA { + foundA = true + break + } + } + assert.Assert(t, foundA, "expected container %s not found", containerA) + }, + } + }, + }, + { + Description: "filter before by name includes earlier container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--no-trunc", "--filter", "before="+data.Labels().Get("container-B")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + // there are other containers that could be listed since + // their created times are ahead of containerB too + foundA := false + for idx, line := range lines { + if idx == 0 { + continue + } + name, _ := tab.ReadRow(line, "NAMES") + if name == containerA { + foundA = true + break + } + } + assert.Assert(t, foundA, "expected container %s not found", containerA) + }, + } + }, + }, + { + Description: "filter since by full id shows only later container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "since="+data.Labels().Get("fullIdA")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerB := data.Labels().Get("container-B") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + name, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, name, containerB, "unexpected container found") + } + }, + } + }, + }, + { + Description: "filter by volume", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + for _, identity := range []string{"A", "B"} { + containerName := data.Labels().Get("container-" + identity) + for i := 0; i < 4; i++ { + vol := data.Labels().Get(fmt.Sprintf("volume-%s-%d", identity, i)) + helpers.Command("ps", "--filter", "volume="+vol). + Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, + "expected at least 2 lines for volume=%s, got %d", vol, len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + name, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, name, containerName) + }, + }) + } + } + }, + }, + { + Description: "filter by network", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "network="+data.Labels().Get("network-A")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerA := data.Labels().Get("container-A") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + containerName, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, containerName, containerA) + }, + } + }, + }, + { + Description: "filter by label", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + labelKey := data.Labels().Get("labelkey-B") + labelVal := data.Labels().Get("labelval-B") + return helpers.Command("ps", "--filter", "label="+labelKey+"="+labelVal) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerB := data.Labels().Get("container-B") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + containerName, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, containerName, containerB, "unexpected container found") + } + }, + } + }, + }, + { + Description: "filter by exited with -a", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--filter", "exited=1") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerC := data.Labels().Get("container-C") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + containerName, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, containerName, containerC, "unexpected container found") + } + }, + } + }, + }, + { + Description: "filter by status=exited with -a", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--filter", "status=exited") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerC := data.Labels().Get("container-C") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + containerName, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, containerName, containerC, "unexpected container found") + } + }, + } + }, + }, + { + Description: "filter by status=exited without -a", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--filter", "status=exited") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerC := data.Labels().Get("container-C") + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines, got %d", len(lines)) + + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err) + for idx, line := range lines { + if idx == 0 { + continue + } + containerName, _ := tab.ReadRow(line, "NAMES") + assert.Equal(t, containerName, containerC, "unexpected container found") + } + }, + } + }, + }, } - - base.Cmd("ps", "-a", "--filter", "exited=1").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerNames := map[string]struct{}{ - testContainerC.name: {}, - } - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if _, ok := containerNames[containerName]; !ok { - return fmt.Errorf("unexpected container %s found", containerName) - } - } - return nil - }) - - base.Cmd("ps", "-a", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerNames := map[string]struct{}{ - testContainerC.name: {}, - } - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if _, ok := containerNames[containerName]; !ok { - return fmt.Errorf("unexpected container %s found", containerName) - } - } - return nil - }) - - // filter container state without option "-a". - base.Cmd("ps", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - containerNames := map[string]struct{}{ - testContainerC.name: {}, - } - for idx, line := range lines { - if idx == 0 { - continue - } - containerName, _ := tab.ReadRow(line, "NAMES") - if _, ok := containerNames[containerName]; !ok { - return fmt.Errorf("unexpected container %s found", containerName) - } - } - return nil - }) } func TestContainerListCheckCreatedTime(t *testing.T) { - base, _ := preparePsTestContainer(t, "checkCreatedTimeA", true) - preparePsTestContainer(t, "checkCreatedTimeB", true) - preparePsTestContainer(t, "checkCreatedTimeC", false) - preparePsTestContainer(t, "checkCreatedTimeD", false) - - var createdTimes []string + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer(data, helpers, "checkCreatedTimeA", true) + setupPsTestContainer(data, helpers, "checkCreatedTimeB", true) + setupPsTestContainer(data, helpers, "checkCreatedTimeC", false) + setupPsTestContainer(data, helpers, "checkCreatedTimeD", false) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + cleanupPsTestContainer(data, helpers, "checkCreatedTimeA") + cleanupPsTestContainer(data, helpers, "checkCreatedTimeB") + cleanupPsTestContainer(data, helpers, "checkCreatedTimeC") + cleanupPsTestContainer(data, helpers, "checkCreatedTimeD") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--format", "'{{json .CreatedAt}}'", "-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) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 4, "expected at least 4 lines, got %d", len(lines)) - base.Cmd("ps", "--format", "'{{json .CreatedAt}}'", "-a").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 4 { - return fmt.Errorf("expected at least 4 lines, got %d", len(lines)) + reversed := make([]string, len(lines)) + copy(reversed, lines) + slices.Reverse(reversed) + assert.Assert(t, slices.IsSorted(reversed), "expected containers in descending order") + }, } - createdTimes = append(createdTimes, lines...) - return nil - }) - - slices.Reverse(createdTimes) - if !slices.IsSorted(createdTimes) { - t.Errorf("expected containers in decending order") } + testCase.Run(t) } func TestContainerListStatusFilter(t *testing.T) { From 502993b83db81283d4f9eb0cabe1f73f509a9a57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:20:40 +0000 Subject: [PATCH 681/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.2 to 2.3.3. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.2...v2.3.3) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7bff7dfc61e..c970d1e19bd 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.2 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.3.3 //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 diff --git a/go.sum b/go.sum index 26098553c1a..f96c4de6dbc 100644 --- a/go.sum +++ b/go.sum @@ -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.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= -github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= +github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k= +github.com/containerd/containerd/v2 v2.3.3/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= From 3d3ca7535024181c60a7cb4771ae18f321df52ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:20:59 +0000 Subject: [PATCH 682/868] build(deps): bump github.com/rootless-containers/rootlesskit/v3 Bumps [github.com/rootless-containers/rootlesskit/v3](https://github.com/rootless-containers/rootlesskit) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/rootless-containers/rootlesskit/releases) - [Commits](https://github.com/rootless-containers/rootlesskit/compare/v3.0.1...v3.0.2) --- updated-dependencies: - dependency-name: github.com/rootless-containers/rootlesskit/v3 dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7bff7dfc61e..aca774a93c8 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/opencontainers/selinux v1.15.1 github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.0.2 //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 diff --git a/go.sum b/go.sum index 26098553c1a..e071d6e9110 100644 --- a/go.sum +++ b/go.sum @@ -276,8 +276,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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.1 h1:AbYUo1O3b8YFL9X8YuEaaWkVFTwdU551CKyuOt0nJok= -github.com/rootless-containers/rootlesskit/v3 v3.0.1/go.mod h1:7HrjR+SnuEMVvGexEinuuTmhvVW+kxj+6+pkQ/O4ja4= +github.com/rootless-containers/rootlesskit/v3 v3.0.2 h1:8hRSEUdzKxmMdy+PHD/rJ3E0+nWEkh9woMDbXjSnMKQ= +github.com/rootless-containers/rootlesskit/v3 v3.0.2/go.mod h1:oFY5X3mj9mOpi/F+oBaD5ojo6QZhr9JdcpsQ6qepz6Q= 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= From fd6efb8e8297d86b00f17efb62ff70ff5092936d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 11 Jul 2026 00:26:40 +0900 Subject: [PATCH 683/868] update RootlessKit (3.0.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 diff --git a/Dockerfile b/Dockerfile index 00ba8160a35..d7f7bdceb86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ 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.1@BINARY +ARG ROOTLESSKIT_VERSION=v3.0.2@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 deleted file mode 100644 index 97504505bb6..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 +++ /dev/null @@ -1,6 +0,0 @@ -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/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 new file mode 100644 index 00000000000..d0d72174aeb --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 @@ -0,0 +1,6 @@ +823ce5dc80fe24da6c1b1e3c37a70fa6e8ba3067636d9a763228031ffeb43dc8 rootlesskit-aarch64.tar.gz +7c403f30aefa6e3faa0843c7d8abbbb0ea2b23e55392958c8e36e883979b6743 rootlesskit-armv7l.tar.gz +9fb855acf3fc23c524c978c21c17e37f374f483152e1ac537a231265e9b3a680 rootlesskit-ppc64le.tar.gz +f23a10d51bcd3c0ee1f22287730c18e2a8094911d436c41bf70fb396d324d88e rootlesskit-riscv64.tar.gz +ba5b8ad5fa39d9b99af7402ebf9eb53549e6cf76453b9636f262a88ac94d04e7 rootlesskit-s390x.tar.gz +f4f2764cdd99db4f3fa715acac9d760c49a5e7c2838f180bdbe3188cec248dfb rootlesskit-x86_64.tar.gz From 5fd9458ebb3d585ca99d35a31714724b9dfe68e4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 11 Jul 2026 00:23:15 +0900 Subject: [PATCH 684/868] update containerd (2.3.3) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 6 +++--- Dockerfile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 5a6e6b05874..1febbd999e1 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -52,7 +52,7 @@ jobs: - runner: ubuntu-26.04-arm # Additionally build for old containerd on amd - runner: ubuntu-26.04 - containerd-version: v1.7.33 + containerd-version: v1.7.34 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -81,7 +81,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.33 + containerd-version: v1.7.34 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-26.04 @@ -98,7 +98,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.33 + containerd-version: v1.7.34 # ipv6 - runner: ubuntu-26.04 target: rootful diff --git a/Dockerfile b/Dockerfile index 00ba8160a35..05e4992373f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.2@fff62f14765df376e5fc36f5a8f8e795b5670f61 +ARG CONTAINERD_VERSION=v2.3.3@aad11006b869517fcd3009450b6f82da282e1a9b ARG RUNC_VERSION=v1.5.0@c4bb59526d0c9cf3a3a46a04d08ca031749a2119 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From b46f2b4f8c3f86d3e17b363f35018f6ce5c7a396 Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Thu, 2 Jul 2026 23:07:58 +0530 Subject: [PATCH 685/868] feat(run): add support for --expose and --publish-all Signed-off-by: Mujib Ahasan test case added Signed-off-by: Mujib Ahasan --- cmd/nerdctl/container/container_run.go | 2 + .../container/container_run_network.go | 13 ++++ .../container_run_network_linux_test.go | 77 +++++++++++++++++++ docs/command-reference.md | 4 +- pkg/api/types/container_network_types.go | 4 + pkg/cmd/container/create.go | 70 +++++++++++++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 8 ++ pkg/labels/labels.go | 2 + 8 files changed, 178 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index b52994dd063..e635516407e 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -133,6 +133,8 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringSlice("dns-option", nil, "Set DNS options") // publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman) cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host") + cmd.Flags().StringSlice("expose", nil, "Expose a port or a range of ports") + cmd.Flags().BoolP("publish-all", "P", false, "Publish all exposed ports to random ports") cmd.Flags().String("ip", "", "IPv4 address to assign to the container") cmd.Flags().String("ip6", "", "IPv6 address to assign to the container") cmd.Flags().StringP("hostname", "h", "", "Container host name") diff --git a/cmd/nerdctl/container/container_run_network.go b/cmd/nerdctl/container/container_run_network.go index 09f6a1b4b59..ce9c1206d3d 100644 --- a/cmd/nerdctl/container/container_run_network.go +++ b/cmd/nerdctl/container/container_run_network.go @@ -187,6 +187,19 @@ func loadNetworkFlags(cmd *cobra.Command, globalOpts types.GlobalCommandOptions) return netOpts, err } portSlice = strutil.DedupeStrSlice(portSlice) + + expose, err := cmd.Flags().GetStringSlice("expose") + if err != nil { + return netOpts, err + } + netOpts.ExposedPorts = strutil.DedupeStrSlice(expose) + + publishAll, err := cmd.Flags().GetBool("publish-all") + if err != nil { + return netOpts, err + } + netOpts.PublishAll = publishAll + portMappings := []cni.PortMapping{} for _, p := range portSlice { pm, err := portutil.ParseFlagP(p) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 25c62db3733..3f6d8f610db 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -1198,3 +1198,80 @@ func TestReservePorts(t *testing.T) { } testCase.Run(t) } + +func TestRunExposeOnly(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", data.Identifier()) + helpers.Ensure("run", "-d", "--name", data.Labels().Get("containerName"), "--expose", "8089", testutil.NginxAlpineImage) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "exposed ports are shown in inspect", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("containerName"), "--format", "{{json .Config.ExposedPorts}}") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, `"80/tcp":{}`), stdout) + assert.Assert(t, strings.Contains(stdout, `"8089/tcp":{}`), stdout) + }), + }, + { + Description: "expose does not publish ports", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("port", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("8089/tcp ->")), + }, + } + + testCase.Run(t) +} + +func TestRunExposePublishAll(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Rootless), // Automatic port allocation is only supported in rootful mode. + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", data.Identifier()) + helpers.Ensure("run", "-d", "--name", data.Labels().Get("containerName"), "--expose", "8089", "-P", testutil.NginxAlpineImage) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "exposed ports are shown in inspect", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("containerName"), "--format", "{{json .Config.ExposedPorts}}") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, `"80/tcp":{}`), stdout) + assert.Assert(t, strings.Contains(stdout, `"8089/tcp":{}`), stdout) + }), + }, + { + Description: "publish-all publishes image and CLI exposed ports", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("port", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "80/tcp ->"), stdout) + assert.Assert(t, strings.Contains(stdout, "8089/tcp ->"), stdout) + }), + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 726655e9ea2..4a5dcc67ee2 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -466,8 +466,8 @@ IPFS flags: - :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`) Unimplemented `docker run` flags: - `--device-cgroup-rule`, `--disable-content-trust`, `--expose`, - `--health-start-interval`, `--link*`, `--publish-all`, `--storage-opt`, + `--device-cgroup-rule`, `--disable-content-trust`, + `--health-start-interval`, `--link*`, `--storage-opt`, `--volume-driver` ### :whale: nerdctl exec diff --git a/pkg/api/types/container_network_types.go b/pkg/api/types/container_network_types.go index cecf51f6dbd..50b925d64ba 100644 --- a/pkg/api/types/container_network_types.go +++ b/pkg/api/types/container_network_types.go @@ -46,4 +46,8 @@ type NetworkOptions struct { UTSNamespace string // PortMappings specifies a list of ports to publish from the container to the host PortMappings []cni.PortMapping + // Additional ports exposed via --expose. Used to generate PortMappings when PublishAll is enabled. + ExposedPorts []string + // Automatically publish all exposed ports by generating PortMappings. + PublishAll bool } diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index b25add2d606..d0a975adcec 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -38,6 +38,7 @@ import ( "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/cio" "github.com/containerd/containerd/v2/pkg/oci" + "github.com/containerd/go-cni" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/annotations" @@ -293,6 +294,31 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } cOpts = append(cOpts, restartOpts...) + var imageExposedPorts map[string]struct{} + + if ensuredImage != nil { + imageExposedPorts = ensuredImage.ImageConfig.ExposedPorts + } + + exposedPorts := mergeExposedPorts(imageExposedPorts, netManager.NetworkOptions().ExposedPorts) + + internalLabels.exposedPorts = exposedPorts + + if netManager.NetworkOptions().PublishAll { + publishAllPortMappings, err := generatePublishAllPortMappings(exposedPorts) + if err != nil { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err + } + + netOpts := netManager.NetworkOptions() + netOpts.PortMappings = append(netOpts.PortMappings, publishAllPortMappings...) + + netManager, err = containerutil.NewNetworkingOptionsManager(options.GOptions, netOpts, client) + if err != nil { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err + } + } + if err = netManager.VerifyNetworkOptions(ctx); err != nil { return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), fmt.Errorf("failed to verify networking settings: %w", err) } @@ -447,6 +473,40 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa return c, nil, nil } +func mergeExposedPorts(imageExposedPorts map[string]struct{}, cliExposedPorts []string) map[string]struct{} { + exposedPorts := map[string]struct{}{} + + for port := range imageExposedPorts { + exposedPorts[port] = struct{}{} + } + + for _, port := range cliExposedPorts { + if port == "" { + continue + } + if !strings.Contains(port, "/") { + port += "/tcp" + } + exposedPorts[port] = struct{}{} + } + + return exposedPorts +} + +func generatePublishAllPortMappings(exposedPorts map[string]struct{}) ([]cni.PortMapping, error) { + var portMappings []cni.PortMapping + + for port := range exposedPorts { + pm, err := portutil.ParseFlagP(port) + if err != nil { + return nil, err + } + portMappings = append(portMappings, pm...) + } + + return portMappings, nil +} + func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage, options types.ContainerCreateOptions) (opts []oci.SpecOpts, cOpts []containerd.NewContainerOpts, err error) { if !options.Rootfs { cOpts = append(cOpts, @@ -769,6 +829,8 @@ type internalLabels struct { healthcheck string privileged bool + + exposedPorts map[string]struct{} } // WithInternalLabels sets the internal labels for a container. @@ -835,6 +897,14 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO } } + if len(internalLabels.exposedPorts) > 0 { + exposedPortsJSON, err := json.Marshal(internalLabels.exposedPorts) + if err != nil { + return nil, err + } + m[labels.ExposedPorts] = string(exposedPortsJSON) + } + if internalLabels.macAddress != "" { m[labels.MACAddress] = internalLabels.macAddress } diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 7626c3f5b92..1e7211307fd 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -578,6 +578,14 @@ func ContainerFromNative(n *native.Container) (*Container, error) { Labels: n.Labels, Image: c.Image, } + if exposedPortsJSON := n.Labels[labels.ExposedPorts]; exposedPortsJSON != "" { + var exposedPorts nat.PortSet + if err := json.Unmarshal([]byte(exposedPortsJSON), &exposedPorts); err != nil { + return nil, fmt.Errorf("failed to unmarshal exposed ports: %w", err) + } + c.Config.ExposedPorts = exposedPorts + } + if n.Labels[labels.Hostname] != "" { hostname = n.Labels[labels.Hostname] } diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index eaec0720efb..78f35552b6c 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -134,4 +134,6 @@ const ( // Privileged indicates whether the container was created with --privileged. Privileged = Prefix + "privileged" + // ExposedPorts is a JSON-marshalled string of nat.PortSet. + ExposedPorts = Prefix + "exposed-ports" ) From a4be5bb9f1959863142134258908438351f6a144 Mon Sep 17 00:00:00 2001 From: akshitguptaa Date: Sun, 12 Jul 2026 23:24:25 +0530 Subject: [PATCH 686/868] rootless: enable IPv6 in RootlessKit and update detection API Signed-off-by: akshitguptaa --- docs/rootless.md | 6 ++++++ extras/rootless/containerd-rootless.sh | 19 +++++++++++++++++ pkg/testutil/testutil_linux.go | 29 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/rootless.md b/docs/rootless.md index 011fa3ff7b2..4c796dc3fef 100644 --- a/docs/rootless.md +++ b/docs/rootless.md @@ -165,6 +165,12 @@ Rootless containerd recognizes the following environment variables to configure the host loopback IP address (127.0.0.1) and abstract sockets are exposed to Dockerfile's "RUN" instructions during `nerdctl build` (not `nerdctl run`). The drawback is fixed in BuildKit v0.13. Upgrading from a prior version of BuildKit needs removing the old systemd unit: `containerd-rootless-setuptool.sh uninstall-buildkit && rm -f ~/.config/buildkit/buildkitd.toml` +* `CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6=(true|false)`: whether to enable IPv6 inside the RootlessKit network namespace. + Defaults to "false". After enabling this, create IPv6-capable CNI networks with + `nerdctl network create --ipv6 --subnet ` as usual. Note that this mainly + affects outgoing connections with `slirp4netns` and `pasta` network drivers. + It does not affect port forwarding in the built-in port driver. + The `gvisor-tap-vsock` network driver does not currently support IPv6. To set these variables, create `~/.config/systemd/user/containerd.service.d/override.conf` as follows: ```ini diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh index bfa7acc4578..631ba91174f 100755 --- a/extras/rootless/containerd-rootless.sh +++ b/extras/rootless/containerd-rootless.sh @@ -44,6 +44,11 @@ # the host loopback IP address (127.0.0.1) and abstract sockets are exposed to Dockerfile's "RUN" instructions during `nerdctl build` (not `nerdctl run`). # The drawback is fixed in BuildKit v0.13. Upgrading from a prior version of BuildKit needs removing the old systemd unit: # `containerd-rootless-setuptool.sh uninstall-buildkit && rm -f ~/.config/buildkit/buildkitd.toml` +# * CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6=(true|false): whether to enable IPv6 inside the RootlessKit network namespace. +# Defaults to "false". +# This mainly affects outgoing connections with slirp4netns and pasta network drivers. +# It does not affect port forwarding in the built-in port driver. +# Note: The gvisor-tap-vsock network driver does not currently support IPv6. # See also: https://github.com/containerd/nerdctl/blob/main/docs/rootless.md#configuring-rootlesskit @@ -78,6 +83,7 @@ if [ -z "$_CONTAINERD_ROOTLESS_CHILD" ]; then : "${CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX:=auto}" : "${CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP:=auto}" : "${CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS:=auto}" + : "${CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6:=false}" net=$CONTAINERD_ROOTLESS_ROOTLESSKIT_NET mtu=$CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU if [ -z "$net" ]; then @@ -137,6 +143,19 @@ if [ -z "$_CONTAINERD_ROOTLESS_CHILD" ]; then ;; esac + case "$CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6" in + 1 | true) + CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS="--ipv6 $CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS" + ;; + 0 | false) + # NOP + ;; + *) + echo "Unknown CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6 value: $CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6" + exit 1 + ;; + esac + # Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces. # # --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index 3f7f2c85337..305d3496d6d 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -16,6 +16,14 @@ package testutil +import ( + "context" + + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" +) + var ( AlpineImage = GetTestImage("alpine") BusyboxImage = GetTestImage("busybox") @@ -53,3 +61,24 @@ const ( // Foreign layer digest NonDistBlobDigest = "sha256:be691b1535726014cdf3b715ff39361b19e121ca34498a9ceea61ad776b9c215" ) + +// RootlessKitIPv6Enabled reports whether the running RootlessKit parent process supports IPv6. +func RootlessKitIPv6Enabled(ctx context.Context) bool { + rlkClient, err := rootlessutil.NewRootlessKitClient() + if err != nil { + log.G(ctx).WithError(err).Warn("failed to create RootlessKit client") + return false + } + + info, err := rlkClient.Info(ctx) + if err != nil { + log.G(ctx).WithError(err).Warn("failed to get RootlessKit info") + return false + } + + if info != nil && info.NetworkDriver != nil { + return info.NetworkDriver.IPv6 + } + + return false +} From 624b7697246a3aee661fbc19c68bcbfc33f28475 Mon Sep 17 00:00:00 2001 From: akshitguptaa Date: Sun, 12 Jul 2026 23:25:04 +0530 Subject: [PATCH 687/868] ci: add IPv6 target to rootless test matrix Signed-off-by: akshitguptaa --- .github/workflows/job-test-in-host.yml | 2 ++ .github/workflows/workflow-test.yml | 5 +++++ .../container/container_run_network_linux_test.go | 4 ++-- hack/test-integration-rootless.sh | 9 +++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index bd4e29e352b..50e5a0c6f00 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -312,6 +312,7 @@ jobs: INPUTS_TARGET: ${{ inputs.target }} INPUTS_IPV6: ${{ inputs.ipv6 }} CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER: ${{ inputs.target == 'rootless-port-slirp4netns' && 'slirp4netns' || '' }} + CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6: ${{ inputs.ipv6 && 'true' || '' }} # FIXME: this must go - if: ${{ env.SHOULD_RUN == 'yes' && !fromJSON(inputs.skip-flaky) }} @@ -332,3 +333,4 @@ jobs: INPUTS_TARGET: ${{ inputs.target }} INPUTS_IPV6: ${{ inputs.ipv6 }} CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER: ${{ inputs.target == 'rootless-port-slirp4netns' && 'slirp4netns' || '' }} + CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6: ${{ inputs.ipv6 && 'true' || '' }} diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 1febbd999e1..6a294cdded6 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -87,6 +87,11 @@ jobs: - runner: ubuntu-26.04 target: rootless binary: "nerdctl.gomodjail" + # ipv6 + - runner: ubuntu-24.04 + target: rootless + ipv6: true + skip-flaky: true ###### Rootful # amd64 - runner: ubuntu-26.04 diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 25c62db3733..300014f2f04 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -838,8 +838,8 @@ func TestHostsFileMounts(t *testing.T) { } func TestRunContainerWithStaticIP6(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Static IP6 assignment is not supported rootless mode yet.") + if rootlessutil.IsRootless() && !testutil.RootlessKitIPv6Enabled(t.Context()) { + t.Skip("Rootless IPv6 requires CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6=true; see docs/rootless.md") } networkName := "test-network" networkSubnet := "2001:db8:5::/64" diff --git a/hack/test-integration-rootless.sh b/hack/test-integration-rootless.sh index 3bf6bd4d981..1d7763731c8 100755 --- a/hack/test-integration-rootless.sh +++ b/hack/test-integration-rootless.sh @@ -82,6 +82,15 @@ if [ ! -e "$HOME/.config/nerdctl-test-setup-done" ]; then systemctl --user daemon-reload fi + if [ "${CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6:-false}" = "true" ]; then + mkdir -p "$HOME/.config/systemd/user/containerd.service.d" + cat <<-EOF >"$HOME/.config/systemd/user/containerd.service.d/ipv6.conf" + [Service] + Environment="CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6=true" + EOF + systemctl --user daemon-reload + fi + containerd-rootless-setuptool.sh install if grep -q "options use-vc" /etc/resolv.conf; then containerd-rootless-setuptool.sh nsenter -- sh -euc 'echo "options use-vc" >>/etc/resolv.conf' From cf942ccf9afaa1ec20166d77a04aafd7857118d3 Mon Sep 17 00:00:00 2001 From: s3onghyun Date: Mon, 13 Jul 2026 15:20:10 +0900 Subject: [PATCH 688/868] save: add --quiet/-q to suppress progress output Signed-off-by: s3onghyun --- cmd/nerdctl/image/image_save.go | 6 ++++++ cmd/nerdctl/image/image_save_test.go | 31 ++++++++++++++++++++++++++++ docs/command-reference.md | 1 + pkg/api/types/image_types.go | 2 ++ pkg/cmd/image/save.go | 6 +++++- 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 89a1eac1e5a..61bcbab54a3 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -45,6 +45,7 @@ func SaveCommand() *cobra.Command { SilenceErrors: true, } cmd.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") + cmd.Flags().BoolP("quiet", "q", false, "Suppress the progress output") // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" @@ -70,11 +71,16 @@ func saveOptions(cmd *cobra.Command) (types.ImageSaveOptions, error) { if err != nil { return types.ImageSaveOptions{}, err } + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return types.ImageSaveOptions{}, err + } return types.ImageSaveOptions{ GOptions: globalOptions, AllPlatforms: allPlatforms, Platform: platform, + Quiet: quiet, }, err } diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 7b97f523761..7671570d31c 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -66,6 +66,37 @@ func TestSaveContent(t *testing.T) { testCase.Run(t) } +func TestSaveQuiet(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + // --quiet is a nerdctl-specific flag, so this is skipped under the Docker compatibility mode. + Require: require.All(require.Not(require.Windows), require.Not(nerdtest.Docker)), + Setup: func(_ test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("save", "--quiet", "-o", filepath.Join(data.Temp().Path(), "out.tar"), testutil.CommonImage) + }, + Expected: func(data test.Data, _ test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(_ string, t tig.T) { + // The archive is still written correctly with --quiet. + rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) + assert.NilError(t, err) + etcOSReleaseBytes, err := os.ReadFile(filepath.Join(rootfsPath, "/etc/os-release")) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(etcOSReleaseBytes), "Alpine")) + }, + } + }, + } + + testCase.Run(t) +} + func TestSave(t *testing.T) { testCase := nerdtest.Setup() diff --git a/docs/command-reference.md b/docs/command-reference.md index 93c481b4c11..d0c18183b22 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -928,6 +928,7 @@ Usage: `nerdctl save [OPTIONS] IMAGE [IMAGE...]` Flags: - :whale: `-o, --output`: Write to a file, instead of STDOUT +- :nerd_face: `-q, --quiet`: Suppress the progress output - :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform - :nerd_face: `--all-platforms`: Export content for all platforms diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 2053d7dca06..d85e5f06665 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -279,6 +279,8 @@ type ImageSaveOptions struct { AllPlatforms bool // Export content for a specific platform Platform []string + // Quiet suppresses the progress output. + Quiet bool } // ImageSignOptions contains options for signing an image. It contains options from diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index a35a83a97f5..9c38bf79e4f 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -103,7 +103,11 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio w := nopWriteCloser{options.Stdout} - pf, done := transferutil.ProgressHandler(ctx, os.Stderr) + progressOutput := io.Writer(os.Stderr) + if options.Quiet { + progressOutput = io.Discard + } + pf, done := transferutil.ProgressHandler(ctx, progressOutput) defer done() return client.Transfer(ctx, From 168db2a246432a83e4e891fae4cdfa2245679df9 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Fri, 22 May 2026 10:44:31 +0800 Subject: [PATCH 689/868] image: align erofs convert with containerd Add raw and zstd EROFS conversion support with containerd's EROFS converter. Fixes: #4861 Signed-off-by: Chengyu Zhu --- cmd/nerdctl/image/image_convert.go | 26 ++++++ cmd/nerdctl/image/image_convert_linux_test.go | 43 ++++++++-- cmd/nerdctl/image/image_inspect.go | 10 ++- docs/command-reference.md | 4 +- docs/erofs.md | 53 ++++++++++++ go.mod | 1 + pkg/api/types/image_types.go | 11 +++ pkg/cmd/image/convert.go | 37 ++++++++- pkg/cmd/image/prune.go | 8 +- pkg/cmd/image/remove.go | 10 ++- pkg/imgutil/push/push.go | 7 ++ pkg/imgutil/transfer.go | 4 +- pkg/platformutil/platformutil.go | 82 ++++++++++++++++++- 13 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 docs/erofs.md diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 105dd0ea161..e4a3cf1f252 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -97,6 +97,12 @@ func convertCommand() *cobra.Command { cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans") // #endregion + // #region erofs flags + cmd.Flags().String("erofs", "", "Convert image layers to EROFS media type. Supported values: raw, zstd") + cmd.Flags().String("erofs-compressors", "", "Specify mkfs.erofs compressor options (e.g. 'lz4hc,12')") + cmd.Flags().String("erofs-mkfs-options", "", "Specify extra mkfs.erofs options (e.g. '-T0 --mkfs-time')") + // #endregion + // #region generic flags cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers") cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types") @@ -248,6 +254,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { } // #endregion + // #region erofs flags + erofs, err := cmd.Flags().GetString("erofs") + if err != nil { + return types.ImageConvertOptions{}, err + } + erofsCompressors, err := cmd.Flags().GetString("erofs-compressors") + if err != nil { + return types.ImageConvertOptions{}, err + } + erofsMkfsOptions, err := cmd.Flags().GetString("erofs-mkfs-options") + if err != nil { + return types.ImageConvertOptions{}, err + } + // #endregion + // #region generic flags uncompress, err := cmd.Flags().GetBool("uncompress") if err != nil { @@ -323,6 +344,11 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { AllPlatforms: allPlatforms, }, }, + ErofsOptions: types.ErofsOptions{ + Erofs: erofs, + ErofsCompressors: erofsCompressors, + ErofsMkfsOptions: erofsMkfsOptions, + }, ProgressOutput: progressOutput, Stdout: cmd.OutOrStdout(), }, nil diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 629db629920..9c10b247288 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -52,7 +53,7 @@ func TestImageConvert(t *testing.T) { return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "nydus", @@ -66,7 +67,7 @@ func TestImageConvert(t *testing.T) { return helpers.Command("image", "convert", "--oci", "--nydus", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "zstd", @@ -77,7 +78,7 @@ func TestImageConvert(t *testing.T) { return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "zstdchunked", @@ -88,7 +89,35 @@ func TestImageConvert(t *testing.T) { return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "erofs raw", + Require: require.All( + require.Binary("mkfs.erofs"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--erofs", "raw", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "erofs zstd", + Require: require.All( + require.Binary("mkfs.erofs"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--erofs", "zstd", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "soci", @@ -107,7 +136,7 @@ func TestImageConvert(t *testing.T) { "--soci-min-layer-size", "0", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "soci with all-platforms", @@ -126,7 +155,7 @@ func TestImageConvert(t *testing.T) { "--soci-min-layer-size", "0", testutil.CommonImage, data.Identifier("converted-image")) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, }, } @@ -188,7 +217,7 @@ func TestImageConvertNydusVerify(t *testing.T) { cmd.WithTimeout(30 * time.Second) return cmd }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) diff --git a/cmd/nerdctl/image/image_inspect.go b/cmd/nerdctl/image/image_inspect.go index 023f23f9650..c56c0f09795 100644 --- a/cmd/nerdctl/image/image_inspect.go +++ b/cmd/nerdctl/image/image_inspect.go @@ -21,12 +21,16 @@ import ( "github.com/spf13/cobra" + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/platforms" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/platformutil" ) func inspectCommand() *cobra.Command { @@ -97,7 +101,11 @@ func imageInspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("unknown mode %q", options.Mode) } - client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform) + var clientOpts []containerd.Opt + if options.Platform == "" { + clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformutil.IgnoreOSFeaturesMatcher(platforms.Default(), platformutil.ErofsOSFeature))) + } + client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform, clientOpts...) if err != nil { return err } diff --git a/docs/command-reference.md b/docs/command-reference.md index 93c481b4c11..f848a880866 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1029,6 +1029,9 @@ Flags: - `--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) +- `--erofs=` : convert image layers to EROFS media type. Supported values: `raw`, `zstd` (see [`./erofs.md`](./erofs.md)) +- `--erofs-compressors=` : specify mkfs.erofs compressor options, e.g. `lz4hc,12` +- `--erofs-mkfs-options=` : specify extra mkfs.erofs options, e.g. `-T0 --mkfs-time` - `--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 @@ -1038,7 +1041,6 @@ Flags: - `--soci-span-size` : Span size in bytes that soci index uses to segment layer data. Default is 4 MiB. - `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. - ### :nerd_face: nerdctl image encrypt Encrypt image layers. See [`./ocicrypt.md`](./ocicrypt.md). diff --git a/docs/erofs.md b/docs/erofs.md new file mode 100644 index 00000000000..2425cace8e7 --- /dev/null +++ b/docs/erofs.md @@ -0,0 +1,53 @@ +# EROFS Image Conversion + +EROFS is a read-only filesystem supported by containerd's `erofs` snapshotter and differ. nerdctl can convert image layers to EROFS media types with `nerdctl image convert --erofs`. + +## Prerequisites + +- Install containerd with the `erofs` snapshotter and differ plugins enabled. +- Install `mkfs.erofs` for `nerdctl image convert --erofs`. + +Check that containerd has loaded the EROFS plugins: + +```console +ctr plugins ls | grep erofs +``` + +## Configure containerd transfer unpack + +containerd 2.3+ provides an EROFS unpack configuration by default when the `erofs` snapshotter and differ plugins are available. + +If `plugins."io.containerd.transfer.v1.local".unpack_config` is configured manually, add an EROFS entry to `/etc/containerd/config.toml` and restart containerd: + +```toml +[[plugins."io.containerd.transfer.v1.local".unpack_config]] + platform = "linux(+erofs)/amd64" + snapshotter = "erofs" + differ = "erofs" +``` + +Replace `amd64` with the target architecture as needed. The `linux(+erofs)/ARCH` entry also allows the `erofs` snapshotter to unpack regular `linux/ARCH` tar/gzip images. + +## Convert an image + +Convert an image to raw EROFS blobs: + +```console +nerdctl image convert --erofs raw example.com/foo:latest example.com/foo:erofs +``` + +Convert an image to zstd-compressed EROFS blobs: + +```console +nerdctl image convert --erofs zstd example.com/foo:latest example.com/foo:erofs-zstd +``` + +`--erofs-compressors` passes compressor options to `mkfs.erofs`, and `--erofs-mkfs-options` passes extra `mkfs.erofs` options. See [`command-reference.md`](./command-reference.md) for flag details. + +## Pull and unpack with EROFS snapshotter + +Push the converted image to a registry, then pull it with the `erofs` snapshotter: + +```console +nerdctl image pull --snapshotter erofs example.com/foo:erofs +``` diff --git a/go.mod b/go.mod index 0965b568bb6..ddea7943876 100644 --- a/go.mod +++ b/go.mod @@ -149,6 +149,7 @@ require ( require ( cyphar.com/go-pathrs v0.2.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/google/uuid v1.6.0 // 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 diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 2053d7dca06..b5d3198d500 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -75,6 +75,7 @@ type ImageConvertOptions struct { NydusOptions OverlaybdOptions SociConvertOptions + ErofsOptions } // EstargzOptions contains eStargz conversion options @@ -152,6 +153,16 @@ type SociConvertOptions struct { // #endregion } +// ErofsOptions contains EROFS conversion options +type ErofsOptions struct { + // Erofs convert image layers to EROFS media type. Supported values: "raw" and "zstd" + Erofs string + // ErofsCompressors specifies mkfs compressor options, e.g. "lz4hc,12" + ErofsCompressors string + // ErofsMkfsOptions specifies extra options for mkfs.erofs, e.g. "-T0 --mkfs-time" + ErofsMkfsOptions string +} + // ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`. type ImageCryptOptions struct { Stdout io.Writer diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index 005309fce92..d5540ee5abd 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/images/converter" + erofsconvert "github.com/containerd/containerd/v2/core/images/converter/erofs" "github.com/containerd/containerd/v2/core/images/converter/uncompress" "github.com/containerd/log" nydusconvert "github.com/containerd/nydus-snapshotter/pkg/converter" @@ -92,8 +93,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa overlaybd := options.Overlaybd nydus := options.Nydus soci := options.Soci + erofs := options.Erofs != "" var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error) - if estargz || zstd || zstdchunked || overlaybd || nydus || soci { + if estargz || zstd || zstdchunked || overlaybd || nydus || soci || erofs { convertCount := 0 if estargz { convertCount++ @@ -113,12 +115,16 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa if soci { convertCount++ } + if erofs { + convertCount++ + } if convertCount > 1 { - return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus and --soci lead to conflict, only one of them can be used") + return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus, --soci and --erofs lead to conflict, only one of them can be used") } var convertFunc converter.ConvertFunc + var updateManifestFunc converter.UpdateManifestFunc var convertType string switch { case estargz: @@ -149,6 +155,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa convertFunc = overlaybdconvert.IndexConvertFunc(obdOpts...) convertOpts = append(convertOpts, converter.WithIndexConvertFunc(convertFunc)) convertType = "overlaybd" + case erofs: + convertFunc, updateManifestFunc, err = getErofsConverter(options) + if err != nil { + return err + } + convertType = "erofs" case nydus: nydusOpts, err := getNydusConvertOpts(options) if err != nil { @@ -188,6 +200,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa if convertType != "overlaybd" { convertOpts = append(convertOpts, converter.WithLayerConvertFunc(convertFunc)) } + if updateManifestFunc != nil { + convertOpts = append(convertOpts, converter.WithUpdateManifest(updateManifestFunc)) + } if !options.Oci { if nydus || overlaybd { log.G(ctx).Warnf("option --%s should be used in conjunction with --oci, forcibly enabling on oci mediatype for %s conversion", convertType, convertType) @@ -369,6 +384,24 @@ func getZstdchunkedConverter(options types.ImageConvertOptions) (converter.Conve return zstdchunkedconvert.LayerConvertFuncWithCompressionLevel(zstd.EncoderLevelFromZstd(options.ZstdChunkedCompressionLevel), esgzOpts...), nil } +func getErofsConverter(options types.ImageConvertOptions) (converter.ConvertFunc, converter.UpdateManifestFunc, error) { + var convertOpts []erofsconvert.ConvertOpt + switch options.Erofs { + case "raw": + case "zstd": + convertOpts = append(convertOpts, erofsconvert.WithBlobCompression("zstd")) + default: + return nil, nil, fmt.Errorf("invalid value %q for --erofs, supported values are: raw, zstd", options.Erofs) + } + if options.ErofsCompressors != "" { + convertOpts = append(convertOpts, erofsconvert.WithCompressors(options.ErofsCompressors)) + } + if options.ErofsMkfsOptions != "" { + convertOpts = append(convertOpts, erofsconvert.WithMkfsOptions(strings.Fields(options.ErofsMkfsOptions))) + } + return erofsconvert.LayerConvertFunc(convertOpts...), erofsconvert.UpdateManifestPlatform, nil +} + func getNydusConvertOpts(options types.ImageConvertOptions) (*nydusconvert.PackOption, error) { workDir := options.NydusWorkDir if workDir == "" { diff --git a/pkg/cmd/image/prune.go b/pkg/cmd/image/prune.go index da29fbdb486..21d8fb1595f 100644 --- a/pkg/cmd/image/prune.go +++ b/pkg/cmd/image/prune.go @@ -25,10 +25,10 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/log" - "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/platformutil" ) // Prune will remove all dangling images. If all is specified, will also remove all images not referenced by any container. @@ -68,10 +68,14 @@ func Prune(ctx context.Context, client *containerd.Client, options types.ImagePr return err } + platformMatcher, err := platformutil.NewMatchComparer(false, nil) + if err != nil { + return err + } delOpts := []images.DeleteOpt{images.SynchronousDelete()} removedImages := make(map[string][]digest.Digest) for _, image := range imagesToBeRemoved { - digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict()) + digests, err := image.RootFS(ctx, contentStore, platformMatcher) if err != nil { log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs") } diff --git a/pkg/cmd/image/remove.go b/pkg/cmd/image/remove.go index 44aafa5fab4..5e5c9ccd9fd 100644 --- a/pkg/cmd/image/remove.go +++ b/pkg/cmd/image/remove.go @@ -25,11 +25,11 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/log" - "github.com/containerd/platforms" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" + "github.com/containerd/nerdctl/v2/pkg/platformutil" ) // Remove removes a list of `images`. @@ -41,6 +41,10 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio cs := client.ContentStore() is := client.ImageService() + platformMatcher, err := platformutil.NewMatchComparer(false, nil) + if err != nil { + return err + } containerList, err := client.Containers(ctx) if err != nil { return err @@ -103,7 +107,7 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio return fmt.Errorf("conflict: unable to delete %s (must be forced) - image is being used by stopped container %s", found.Req, cid) } // digests is used only for emulating human-readable output of `docker rmi` - digests, err := found.Image.RootFS(ctx, cs, platforms.DefaultStrict()) + digests, err := found.Image.RootFS(ctx, cs, platformMatcher) if err != nil { log.G(ctx).WithError(err).Warning("failed to enumerate rootfs") } @@ -156,7 +160,7 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio return false, fmt.Errorf("conflict: unable to delete %s (must be forced) - image is being used by stopped container %s", found.Req, cid) } // digests is used only for emulating human-readable output of `docker rmi` - digests, err := found.Image.RootFS(ctx, cs, platforms.DefaultStrict()) + digests, err := found.Image.RootFS(ctx, cs, platformMatcher) if err != nil { log.G(ctx).WithError(err).Warning("failed to enumerate rootfs") } diff --git a/pkg/imgutil/push/push.go b/pkg/imgutil/push/push.go index 94c6acca71c..89684e9dbaf 100644 --- a/pkg/imgutil/push/push.go +++ b/pkg/imgutil/push/push.go @@ -48,6 +48,7 @@ func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resol } desc := img.Target + ctx = withErofsLayerRefKeyPrefixes(ctx) ongoing := newPushJobs(pushTracker) eg, ctx := errgroup.WithContext(ctx) @@ -172,3 +173,9 @@ func (j *pushjobs) status() []jobs.StatusInfo { return statuses } + +func withErofsLayerRefKeyPrefixes(ctx context.Context) context.Context { + ctx = remotes.WithMediaTypeKeyPrefix(ctx, images.MediaTypeErofsLayer, "layer") + ctx = remotes.WithMediaTypeKeyPrefix(ctx, images.MediaTypeErofsLayer+"+zstd", "layer") + return ctx +} diff --git a/pkg/imgutil/transfer.go b/pkg/imgutil/transfer.go index 532f9982aee..86c5aee5dba 100644 --- a/pkg/imgutil/transfer.go +++ b/pkg/imgutil/transfer.go @@ -42,7 +42,8 @@ import ( func prepareImageStore(ctx context.Context, parsedReference *referenceutil.ImageReference, options types.ImagePullOptions) (*transferimage.Store, error) { var storeOpts []transferimage.StoreOpt if len(options.OCISpecPlatform) > 0 { - storeOpts = append(storeOpts, transferimage.WithPlatforms(options.OCISpecPlatform...)) + platforms := platformutil.AppendOSFeatureVariants(options.OCISpecPlatform, platformutil.ErofsOSFeature) + storeOpts = append(storeOpts, transferimage.WithPlatforms(platforms...)) } unpackEnabled := len(options.OCISpecPlatform) == 1 @@ -175,6 +176,7 @@ func preparePushStore(pushRef string, options types.ImagePushOptions) (*transfer storeOpts := []transferimage.StoreOpt{} if len(platformsSlice) > 0 { + platformsSlice = platformutil.AppendOSFeatureVariants(platformsSlice, platformutil.ErofsOSFeature) storeOpts = append(storeOpts, transferimage.WithPlatforms(platformsSlice...)) } diff --git a/pkg/platformutil/platformutil.go b/pkg/platformutil/platformutil.go index ac076d0b980..2168e8909d0 100644 --- a/pkg/platformutil/platformutil.go +++ b/pkg/platformutil/platformutil.go @@ -18,6 +18,7 @@ package platformutil import ( "fmt" + "slices" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -26,13 +27,15 @@ import ( "github.com/containerd/nerdctl/v2/pkg/strutil" ) +const ErofsOSFeature = "erofs" + // NewMatchComparerFromOCISpecPlatformSlice returns MatchComparer. // If platformz is empty, NewMatchComparerFromOCISpecPlatformSlice returns All (not DefaultStrict). func NewMatchComparerFromOCISpecPlatformSlice(platformz []ocispec.Platform) platforms.MatchComparer { if len(platformz) == 0 { return platforms.All } - return platforms.Ordered(platformz...) + return IgnoreOSFeaturesMatcher(platforms.Ordered(platformz...), ErofsOSFeature) } // NewMatchComparer returns MatchComparer. @@ -45,10 +48,83 @@ func NewMatchComparer(all bool, ss []string) (platforms.MatchComparer, error) { } if len(ss) == 0 { // return DefaultStrict, not Default - return platforms.DefaultStrict(), nil + return IgnoreOSFeaturesMatcher(platforms.DefaultStrict(), ErofsOSFeature), nil } op, err := NewOCISpecPlatformSlice(false, ss) - return platforms.Ordered(op...), err + return IgnoreOSFeaturesMatcher(platforms.Ordered(op...), ErofsOSFeature), err +} + +// IgnoreOSFeaturesMatcher wraps a MatchComparer and ignores selected os.features +// on candidate platforms before delegating to the wrapped matcher. +func IgnoreOSFeaturesMatcher(mc platforms.MatchComparer, features ...string) platforms.MatchComparer { + return ignoreOSFeaturesMatcher{ + MatchComparer: mc, + features: features, + } +} + +func AppendOSFeatureVariants(platformz []ocispec.Platform, features ...string) []ocispec.Platform { + if len(platformz) == 0 || len(features) == 0 { + return platformz + } + out := slices.Clone(platformz) + seen := make(map[string]struct{}, len(platformz)*2) + for _, p := range out { + seen[platforms.FormatAll(platforms.Normalize(p))] = struct{}{} + } + for _, p := range platformz { + var added bool + for _, feature := range features { + if slices.Contains(p.OSFeatures, feature) { + continue + } + p.OSFeatures = append(p.OSFeatures, feature) + added = true + } + if added { + p = platforms.Normalize(p) + key := platforms.FormatAll(p) + if _, ok := seen[key]; ok { + continue + } + out = append(out, p) + seen[key] = struct{}{} + } + } + return out +} + +type ignoreOSFeaturesMatcher struct { + platforms.MatchComparer + features []string +} + +func (m ignoreOSFeaturesMatcher) Match(p ocispec.Platform) bool { + return m.MatchComparer.Match(p) || m.MatchComparer.Match(withoutOSFeatures(p, m.features)) +} + +func (m ignoreOSFeaturesMatcher) Less(p1, p2 ocispec.Platform) bool { + p1Match := m.MatchComparer.Match(p1) + p2Match := m.MatchComparer.Match(p2) + if p1Match != p2Match { + return p1Match + } + if p1Match { + return m.MatchComparer.Less(p1, p2) + } + return m.MatchComparer.Less(withoutOSFeatures(p1, m.features), withoutOSFeatures(p2, m.features)) +} + +func withoutOSFeatures(p ocispec.Platform, features []string) ocispec.Platform { + if !slices.ContainsFunc(p.OSFeatures, func(feature string) bool { + return slices.Contains(features, feature) + }) { + return p + } + p.OSFeatures = slices.DeleteFunc(slices.Clone(p.OSFeatures), func(feature string) bool { + return slices.Contains(features, feature) + }) + return p } // NewOCISpecPlatformSlice returns a slice of ocispec.Platform From d5361fd28803bbe8b6e45bdc7d649dd6a1110980 Mon Sep 17 00:00:00 2001 From: s3onghyun Date: Fri, 10 Jul 2026 03:15:25 +0900 Subject: [PATCH 690/868] Allocate a free host port from the range for a single container port When the container side is a single port and the host side is a range (e.g. `-p 3000-3001:8080`), nerdctl now treats the range as a pool and binds the container port to the first free host port in it, using getUsedPorts to skip ports already in use, matching Docker's behavior. Previously the extra host ports were silently dropped. Genuine range/range mismatches of unequal length are still rejected. The host IP is validated and normalized once, and the pool test occupies the first port of a range and asserts the free successor is chosen, so it fails without this change (Linux-only; skipped in rootless). Signed-off-by: s3onghyun --- pkg/portutil/portutil.go | 54 ++++++++++++++------- pkg/portutil/portutil_linux_test.go | 73 +++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 pkg/portutil/portutil_linux_test.go diff --git a/pkg/portutil/portutil.go b/pkg/portutil/portutil.go index 73853767f16..e99c924f1ed 100644 --- a/pkg/portutil/portutil.go +++ b/pkg/portutil/portutil.go @@ -76,6 +76,17 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) { ip, hostPort, containerPort := splitParts(splitBySlash[0]) + // Validate and normalize the host IP once. An empty IP is passed through to + // getUsedPorts below as "all interfaces"; for error messages and the resulting + // PortMapping it is normalized to 0.0.0.0. + if ip != "" && net.ParseIP(ip) == nil { + return nil, fmt.Errorf("invalid ip address: %s", ip) + } + hostIP := ip + if hostIP == "" { + hostIP = "0.0.0.0" + } + if containerPort == "" { return nil, fmt.Errorf("no port specified: %s", splitBySlash[0]) } @@ -107,32 +118,43 @@ func ParseFlagP(s string) ([]cni.PortMapping, error) { if err != nil { return nil, err } - for i := startHostPort; i <= endHostPort; i++ { - if usedPorts[i] { - return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", ip, i) + if startPort == endPort && startHostPort != endHostPort { + // Docker-compatible behavior: a single container port with a host port + // range (e.g. "3000-3001:8080") treats the range as a pool and binds the + // container port to the first free host port in it, rather than silently + // collapsing to the first port and dropping the rest of the range. + // https://github.com/moby/moby/blob/master/daemon/libnetwork/portallocator/portallocator.go + found := false + for p := startHostPort; p <= endHostPort; p++ { + if !usedPorts[p] { + startHostPort, endHostPort = p, p + found = true + break + } + } + if !found { + return nil, fmt.Errorf("bind for %s failed: all ports in range %s are already allocated", hostIP, hostPort) + } + } else { + for i := startHostPort; i <= endHostPort; i++ { + if usedPorts[i] { + return nil, fmt.Errorf("bind for %s:%d failed: port is already allocated", hostIP, i) + } } } } if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { - if endPort != startPort { - return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) - } + // Both container and host sides are ranges but of unequal length — a genuine + // mismatch (the single-container-port pool case above has already collapsed + // the host range to one port, so it does not reach here). + return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) } for i := int32(0); i <= (int32(endPort) - int32(startPort)); i++ { res.ContainerPort = int32(startPort) + i res.HostPort = int32(startHostPort) + i - if ip == "" { - //TODO handle ipv6 - res.HostIP = "0.0.0.0" - } else { - // TODO handle ipv6 - if net.ParseIP(ip) == nil { - return nil, fmt.Errorf("invalid ip address: %s", ip) - } - res.HostIP = ip - } + res.HostIP = hostIP mr = append(mr, res) } diff --git a/pkg/portutil/portutil_linux_test.go b/pkg/portutil/portutil_linux_test.go new file mode 100644 index 00000000000..8d69aab179b --- /dev/null +++ b/pkg/portutil/portutil_linux_test.go @@ -0,0 +1,73 @@ +/* + 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 portutil + +import ( + "fmt" + "net" + "testing" + + "gotest.tools/v3/assert" +) + +// TestParseFlagPHostRangePool verifies the Docker-compatible behavior for a single +// container port with a host port range (e.g. "3000-3001:8080"): the container port +// is bound to one free host port from the range, not collapsed-and-dropped. The test +// occupies the first port of a two-port range and asserts that the container port is +// bound to the next free host port (first+1); without the pool-allocation fix it would +// be dropped onto the occupied first port and this assertion would fail. +// +// This lives in a _linux_test.go file because getUsedPorts is only implemented on Linux. +func TestParseFlagPHostRangePool(t *testing.T) { + // Occupy the first port of a two-port range and confirm that the single + // container port is bound to the next free host port, not collapsed onto the + // occupied first port. Without the pool fix this asserts the wrong port. + var occupied net.Listener + var first int + for attempt := 0; attempt < 50; attempt++ { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + continue + } + p := l.Addr().(*net.TCPAddr).Port + if p+1 > 65535 { + l.Close() + continue + } + // Ensure the successor port is currently free. + probe, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", p+1)) + if err != nil { + l.Close() + continue + } + probe.Close() + occupied, first = l, p + break + } + if occupied == nil { + t.Fatal("could not find an occupied port with a free successor") + } + defer occupied.Close() + + got, err := ParseFlagP(fmt.Sprintf("127.0.0.1:%d-%d:8080/tcp", first, first+1)) + assert.NilError(t, err) + assert.Equal(t, len(got), 1) + assert.Equal(t, got[0].ContainerPort, int32(8080)) + assert.Equal(t, got[0].Protocol, "tcp") + assert.Equal(t, got[0].HostIP, "127.0.0.1") + assert.Equal(t, got[0].HostPort, int32(first+1)) +} From 62d0435e48aa3b9139d63a3dbb5eae53ccac22bd Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Tue, 14 Jul 2026 21:39:32 +0100 Subject: [PATCH 691/868] test: refactor container_cp_linux_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container/container_cp_linux_test.go | 711 ++++++++---------- 1 file changed, 332 insertions(+), 379 deletions(-) diff --git a/cmd/nerdctl/container/container_cp_linux_test.go b/cmd/nerdctl/container/container_cp_linux_test.go index 6235df02ed3..93ef6661874 100644 --- a/cmd/nerdctl/container/container_cp_linux_test.go +++ b/cmd/nerdctl/container/container_cp_linux_test.go @@ -17,7 +17,9 @@ package container import ( + "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -28,6 +30,9 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/icmd" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/tarutil" @@ -84,15 +89,13 @@ type testcases struct { expect icmd.Expected // expectation // Optional - catFile string // path that we "cat" - defaults to destinationSpec if not specified - setup func(base *testutil.Base, container string, destPath string) // additional test setup if needed - tearDown func() // additional cleanup if needed - volume func(base *testutil.Base, id string) (string, string, bool) // volume creation function if needed (should return the volume name, mountPoint, readonly flag) + catFile string // path that we "cat" - defaults to destinationSpec if not specified + setup func(helpers test.Helpers, container string, destPath string) // additional test setup if needed + tearDown func() // additional cleanup if needed + volume func(helpers test.Helpers, id string) (string, string, bool) // volume creation function if needed (should return the volume name, mountPoint, readonly flag) } func TestCopyToContainer(t *testing.T) { - t.Parallel() - testGroups := []*testgroup{ { description: "Copying to container, SRC_PATH is a file, absolute", @@ -137,8 +140,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -147,8 +150,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -158,8 +161,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -174,8 +177,8 @@ func TestCopyToContainer(t *testing.T) { // frustrating Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -185,8 +188,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -196,8 +199,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -207,8 +210,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -218,8 +221,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -230,8 +233,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 0, }, // FIXME the way we handle volume is not right - too complicated for the test author - volume: func(base *testutil.Base, id string) (string, string, bool) { - base.Cmd("volume", "create", id).Run() + volume: func(helpers test.Helpers, id string) (string, string, bool) { + helpers.Ensure("volume", "create", id) return id, pathIsAVolumeMount, false }, }, @@ -242,8 +245,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrTargetIsReadOnly.Error(), }, - volume: func(base *testutil.Base, id string) (string, string, bool) { - base.Cmd("volume", "create", id).Run() + volume: func(helpers test.Helpers, id string) (string, string, bool) { + helpers.Ensure("volume", "create", id) return id, pathIsAVolumeMount, true }, }, @@ -293,8 +296,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -304,8 +307,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -315,8 +318,8 @@ func TestCopyToContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -331,8 +334,8 @@ func TestCopyToContainer(t *testing.T) { // frustrating Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, }, { @@ -342,8 +345,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -353,8 +356,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -364,8 +367,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -375,8 +378,8 @@ func TestCopyToContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, }, @@ -390,16 +393,16 @@ func TestCopyToContainer(t *testing.T) { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, }, @@ -414,16 +417,16 @@ func TestCopyToContainer(t *testing.T) { description: "DEST_PATH is a directory, relative", destinationSpec: pathIsADirRelative, catFile: filepath.Join(pathIsADirRelative, srcFileName), - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { description: "DEST_PATH is a directory, absolute", destinationSpec: pathIsADirAbsolute, catFile: filepath.Join(pathIsADirAbsolute, srcFileName), - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "mkdir", "-p", destPath) }, }, { @@ -437,8 +440,8 @@ func TestCopyToContainer(t *testing.T) { { description: "DEST_PATH is a file", destinationSpec: pathIsAFileAbsolute, - setup: func(base *testutil.Base, container string, destPath string) { - base.Cmd("exec", container, "touch", destPath).AssertOK() + setup: func(helpers test.Helpers, container string, destPath string) { + helpers.Ensure("exec", container, "touch", destPath) }, expect: icmd.Expected{ ExitCode: 1, @@ -449,14 +452,12 @@ func TestCopyToContainer(t *testing.T) { }, } - for _, tg := range testGroups { - cpTestHelper(t, tg) - } + testCase := nerdtest.Setup() + testCase.SubTests = cpBuildSubTests(testGroups) + testCase.Run(t) } func TestCopyFromContainer(t *testing.T) { - t.Parallel() - testGroups := []*testgroup{ { description: "Copying from container, SRC_PATH specifies a file", @@ -499,9 +500,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -510,9 +511,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -522,9 +523,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -534,9 +535,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -546,9 +547,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -558,9 +559,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -570,9 +571,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -582,9 +583,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -595,9 +596,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, }, @@ -645,11 +646,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - err := os.MkdirAll(filepath.Dir(destPath), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + setup: func(helpers test.Helpers, container string, destPath string) { + err := os.WriteFile(destPath, []byte(""), filePerm) + assert.NilError(helpers.T(), err) }, }, { @@ -659,11 +658,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrCannotCopyDirToFile.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - err := os.MkdirAll(filepath.Dir(destPath), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + setup: func(helpers test.Helpers, container string, destPath string) { + err := os.WriteFile(destPath, []byte(""), filePerm) + assert.NilError(helpers.T(), err) }, }, { @@ -673,11 +670,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - err := os.MkdirAll(filepath.Dir(destPath), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + setup: func(helpers test.Helpers, container string, destPath string) { + err := os.WriteFile(destPath, []byte(""), filePerm) + assert.NilError(helpers.T(), err) }, }, { @@ -687,11 +682,9 @@ func TestCopyFromContainer(t *testing.T) { ExitCode: 1, Err: containerutil.ErrDestinationIsNotADir.Error(), }, - setup: func(base *testutil.Base, container string, destPath string) { - err := os.MkdirAll(filepath.Dir(destPath), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(destPath, []byte(""), filePerm) - assert.NilError(t, err) + setup: func(helpers test.Helpers, container string, destPath string) { + err := os.WriteFile(destPath, []byte(""), filePerm) + assert.NilError(helpers.T(), err) }, }, { @@ -701,9 +694,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -713,9 +706,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -725,9 +718,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -737,9 +730,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -749,10 +742,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { - // Don't make the topmost dir as this is where the tarball must extract - err := os.MkdirAll(filepath.Dir(destPath), dirPerm) - assert.NilError(t, err) + setup: func(helpers test.Helpers, container string, destPath string) { + err := os.MkdirAll(destPath, dirPerm) + assert.NilError(helpers.T(), err) }, }, }, @@ -769,9 +761,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -781,9 +773,9 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, { @@ -793,45 +785,79 @@ func TestCopyFromContainer(t *testing.T) { expect: icmd.Expected{ ExitCode: 0, }, - setup: func(base *testutil.Base, container string, destPath string) { + setup: func(helpers test.Helpers, container string, destPath string) { err := os.MkdirAll(destPath, dirPerm) - assert.NilError(t, err) + assert.NilError(helpers.T(), err) }, }, }, }, } - for _, tg := range testGroups { - cpTestHelper(t, tg) - } + testCase := nerdtest.Setup() + testCase.SubTests = cpBuildSubTests(testGroups) + testCase.Run(t) } -func assertCatHelper(base *testutil.Base, catPath string, fileContent []byte, container string, expectedUID int, containerIsStopped bool) { - base.T.Logf("catPath=%q", catPath) +func assertCatHelper(helpers test.Helpers, t tig.T, catPath string, fileContent []byte, container string, expectedUID int, containerIsStopped bool) { + t.Log(fmt.Sprintf("catPath=%q", catPath)) if container != "" && containerIsStopped { - base.Cmd("start", container).AssertOK() - defer base.Cmd("stop", container).AssertOK() + helpers.Ensure("start", container) + defer func() { helpers.Ensure("stop", container) }() } if container == "" { got, err := os.ReadFile(catPath) - assert.NilError(base.T, err, "Failed reading from file") - assert.DeepEqual(base.T, fileContent, got) + assert.NilError(t, err, "Failed reading from file") + assert.DeepEqual(t, fileContent, got) st, err := os.Stat(catPath) - assert.NilError(base.T, err) + assert.NilError(t, err) stSys := st.Sys().(*syscall.Stat_t) expected := uint32(expectedUID) actual := stSys.Uid - assert.DeepEqual(base.T, expected, actual) + assert.DeepEqual(t, expected, actual) + } else { + content := helpers.Capture("exec", container, "sh", "-c", "--", fmt.Sprintf("ls -lA /; echo %q; cat %q", catPath, catPath)) + assert.Assert(t, strings.Contains(content, string(fileContent))) + uid := helpers.Capture("exec", container, "stat", "-c", "%u", catPath) + assert.Assert(t, uid == fmt.Sprintf("%d\n", expectedUID)) + } +} + +func cpCreateFileOnHost(t tig.T, sourceFile string, sourceFileContent []byte, fromStdin bool) { + if fromStdin { + d := filepath.Dir(sourceFile) + tarCpFolder := filepath.Join(d, cpFolderName) + tarBinary, _, err := tarutil.FindTarBinary() + assert.NilError(t, err) + err = os.MkdirAll(tarCpFolder, dirPerm) + assert.NilError(t, err) + err = os.WriteFile(filepath.Join(tarCpFolder, srcFileName), sourceFileContent, filePerm) + assert.NilError(t, err) + err = exec.Command(tarBinary, "-cf", sourceFile, "-C", tarCpFolder, ".").Run() + assert.NilError(t, err) + err = os.RemoveAll(tarCpFolder) + assert.NilError(t, err) } else { - base.Cmd("exec", container, "sh", "-c", "--", fmt.Sprintf("ls -lA /; echo %q; cat %q", catPath, catPath)).AssertOutContains(string(fileContent)) - base.Cmd("exec", container, "stat", "-c", "%u", catPath).AssertOutExactly(fmt.Sprintf("%d\n", expectedUID)) + err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm) + assert.NilError(t, err) + err = os.WriteFile(sourceFile, sourceFileContent, filePerm) + assert.NilError(t, err) } } -func cpTestHelper(t *testing.T, tg *testgroup) { - // Get the source path +func cpBuildSubTests(testGroups []*testgroup) []*test.Case { + var groupSubTests []*test.Case + for _, tg := range testGroups { + groupSubTests = append(groupSubTests, &test.Case{ + Description: tg.description, + SubTests: cpBuildCaseSubTests(tg), + }) + } + return groupSubTests +} + +func cpBuildCaseSubTests(tg *testgroup) []*test.Case { groupSourceSpec := tg.sourceSpec groupSourceDir := groupSourceSpec fromStdin := false @@ -842,15 +868,7 @@ func cpTestHelper(t *testing.T, tg *testgroup) { } else if tg.sourceIsAFile { groupSourceDir = filepath.Dir(groupSourceSpec) } - - // Copy direction copyToContainer := tg.toContainer - // Description - description := tg.description - // Test cases - testCases := tg.testCases - - // Compute UIDs dependent on cp direction var srcUID, destUID int if copyToContainer { srcUID = os.Geteuid() @@ -859,269 +877,204 @@ func cpTestHelper(t *testing.T, tg *testgroup) { srcUID = 42 destUID = os.Geteuid() } + var subTests []*test.Case + for _, tc := range tg.testCases { + subTests = append(subTests, cpSingleCaseSubTest(tc, copyToContainer, groupSourceSpec, groupSourceDir, fromStdin, srcUID, destUID)) + } + return subTests +} - t.Run(description, func(t *testing.T) { - t.Parallel() - - for _, tc := range testCases { - testCase := tc - - t.Run(testCase.description, func(t *testing.T) { - t.Parallel() - - // Compute test-specific values - testID := testutil.Identifier(t) - containerRunning := testID + "-r" - containerStopped := testID + "-s" - sourceFileContent := []byte(testID) - tempDir := t.TempDir() - - base := testutil.NewBase(t) - // Change working directory for commands to execute to the newly created temp directory on the host - // Note that ChDir won't do in a parallel context - and that setup func on the host below - // has to deal with that problem separately by making sure relative paths are resolved against temp - base.Dir = tempDir - - // Prepare the specs and derived variables - sourceSpec := groupSourceSpec - catFile := testCase.catFile - - destinationSpec := testCase.destinationSpec - toStdout := false - // tarball destination just sets up the dir to extract to - if destinationSpec == "-" { - toStdout = true - destinationSpec = filepath.Dir(catFile) - } +func cpSingleCaseSubTest(tc testcases, copyToContainer bool, groupSourceSpec, groupSourceDir string, fromStdin bool, srcUID, destUID int) *test.Case { + return &test.Case{ + Description: tc.description, + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + testID := data.Identifier() + containerRunning := testID + "-r" + containerStopped := testID + "-s" + sourceFileContent := []byte(testID) + tempDir := data.Temp().Dir("work") + data.Labels().Set("containerRunning", containerRunning) + data.Labels().Set("containerStopped", containerStopped) + data.Labels().Set("sourceFileContent", string(sourceFileContent)) + data.Labels().Set("tempDir", tempDir) - // If the test case does not specify a catFile, start with the destination spec - if catFile == "" { - catFile = destinationSpec + sourceSpec := groupSourceSpec + catFile := tc.catFile + destinationSpec := tc.destinationSpec + toStdout := false + if destinationSpec == "-" { + toStdout = true + destinationSpec = filepath.Dir(catFile) + } + if catFile == "" { + catFile = destinationSpec + } + sourceFile := filepath.Join(groupSourceDir, srcFileName) + if copyToContainer { + if !filepath.IsAbs(catFile) { + catFile = filepath.Join(string(os.PathSeparator), catFile) } - - sourceFile := filepath.Join(groupSourceDir, srcFileName) - if copyToContainer { - if !filepath.IsAbs(catFile) { - catFile = filepath.Join(string(os.PathSeparator), catFile) - } - - if fromStdin { - sourceFile = filepath.Join(tempDir, groupSourceDir, tarballName) - } else { - // Use an absolute path for evaluation - // If the sourceFile is still relative, make it absolute to the temp - sourceFile = filepath.Join(tempDir, sourceFile) - // If the spec path for source on the host was absolute, make sure we put that under tempDir - if filepath.IsAbs(sourceSpec) { - sourceSpec = tempDir + sourceSpec - } - } + if fromStdin { + sourceFile = filepath.Join(tempDir, groupSourceDir, tarballName) } else { - // If we are copying to host, we need to make sure we have an absolute path to cat, relative to temp, - // whether it is relative, or "absolute" - catFile = filepath.Join(tempDir, catFile) - // If the spec for destination on the host was absolute, make sure we put that under tempDir - if filepath.IsAbs(destinationSpec) { - destinationSpec = tempDir + destinationSpec + sourceFile = filepath.Join(tempDir, sourceFile) + if filepath.IsAbs(sourceSpec) { + sourceSpec = tempDir + sourceSpec } } - - // Teardown: clean-up containers and optional volume - tearDown := func() { - base.Cmd("rm", "-f", containerRunning).Run() - base.Cmd("rm", "-f", containerStopped).Run() - if testCase.volume != nil { - volID, _, _ := testCase.volume(base, testID) - base.Cmd("volume", "rm", volID).Run() - } + } else { + catFile = filepath.Join(tempDir, catFile) + if filepath.IsAbs(destinationSpec) { + destinationSpec = tempDir + destinationSpec } + } + data.Labels().Set("sourceSpec", sourceSpec) + data.Labels().Set("catFile", catFile) + data.Labels().Set("destinationSpec", destinationSpec) + data.Labels().Set("sourceFile", sourceFile) + if toStdout { + data.Labels().Set("toStdout", "true") + } - createFileOnHost := func() { - switch fromStdin { - case true: - d := filepath.Dir(sourceFile) - tarCpFolder := filepath.Join(d, cpFolderName) - tarBinary, _, err := tarutil.FindTarBinary() - assert.NilError(t, err) - - err = os.MkdirAll(tarCpFolder, dirPerm) - assert.NilError(t, err) - err = os.WriteFile(filepath.Join(tarCpFolder, srcFileName), sourceFileContent, filePerm) - assert.NilError(t, err) - - err = exec.Command(tarBinary, "-cf", sourceFile, "-C", tarCpFolder, ".").Run() - assert.NilError(t, err) - err = os.RemoveAll(tarCpFolder) - assert.NilError(t, err) - case false: - // Create file on the host - err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm) - assert.NilError(t, err) - err = os.WriteFile(sourceFile, sourceFileContent, filePerm) - assert.NilError(t, err) - } + args := []string{"run", "-d", "-w", containerCwd} + if tc.volume != nil { + vol, mount, ro := tc.volume(helpers, testID) + volArg := fmt.Sprintf("%s:%s", vol, mount) + if ro { + volArg += ":ro" } - - // Setup: create volume, containers, create the source file - setup := func() { - args := []string{"run", "-d", "-w", containerCwd} - if testCase.volume != nil { - vol, mount, ro := testCase.volume(base, testID) - volArg := fmt.Sprintf("%s:%s", vol, mount) - if ro { - volArg += ":ro" - } - args = append(args, "-v", volArg) - } - base.Cmd(append(args, "--name", containerRunning, testutil.CommonImage, "sleep", "Inf")...).AssertOK() - base.Cmd(append(args, "--name", containerStopped, testutil.CommonImage, "sleep", "Inf")...).AssertOK() - + args = append(args, "-v", volArg) + } + helpers.Ensure(append(args, "--name", containerRunning, testutil.CommonImage, "sleep", nerdtest.Infinity)...) + helpers.Ensure(append(args, "--name", containerStopped, testutil.CommonImage, "sleep", nerdtest.Infinity)...) + if copyToContainer { + cpCreateFileOnHost(helpers.T(), sourceFile, sourceFileContent, fromStdin) + } else { + mkSrcScript := fmt.Sprintf("cd /; mkdir -p %q && echo -n %q >%q && chown %d %q", filepath.Dir(sourceFile), sourceFileContent, sourceFile, srcUID, sourceFile) + helpers.Ensure("exec", containerRunning, "sh", "-euc", mkSrcScript) + helpers.Ensure("exec", containerStopped, "sh", "-euc", mkSrcScript) + } + if tc.setup != nil { + setupDest := strings.TrimSuffix(destinationSpec, string(os.PathSeparator)) + if !filepath.IsAbs(setupDest) { if copyToContainer { - createFileOnHost() + setupDest = filepath.Join(string(os.PathSeparator), setupDest) } else { - // Create file content in the container - // Note: cd /, otherwise we end-up in the container cwd, which is NOT obeyed by cp - mkSrcScript := fmt.Sprintf("cd /; mkdir -p %q && echo -n %q >%q && chown %d %q", filepath.Dir(sourceFile), sourceFileContent, sourceFile, srcUID, sourceFile) - base.Cmd("exec", containerRunning, "sh", "-euc", mkSrcScript).AssertOK() - base.Cmd("exec", containerStopped, "sh", "-euc", mkSrcScript).AssertOK() - } - - // If we have optional setup, run that now - if testCase.setup != nil { - // Some specs may come with a trailing slash (proper or improper) - // Setup should still work in all cases (including if its a file), and get through to the actual test - setupDest := destinationSpec - setupDest = strings.TrimSuffix(setupDest, string(os.PathSeparator)) - if !filepath.IsAbs(setupDest) { - if copyToContainer { - setupDest = filepath.Join(string(os.PathSeparator), setupDest) - } else { - setupDest = filepath.Join(tempDir, setupDest) - } - } - testCase.setup(base, containerRunning, setupDest) - testCase.setup(base, containerStopped, setupDest) + setupDest = filepath.Join(tempDir, setupDest) } - - // Stop the "stopped" container - base.Cmd("stop", containerStopped).AssertOK() } + tc.setup(helpers, containerRunning, setupDest) + tc.setup(helpers, containerStopped, setupDest) + } + helpers.Ensure("stop", containerStopped) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + testID := data.Identifier() + helpers.Anyhow("rm", "-f", testID+"-r") + helpers.Anyhow("rm", "-f", testID+"-s") + if tc.volume != nil { + helpers.Anyhow("volume", "rm", testID) + } + if tc.tearDown != nil { + tc.tearDown() + } + }, + SubTests: []*test.Case{ + cpRunningSubTest(tc, copyToContainer, fromStdin, destUID), + cpStoppedSubTest(tc, copyToContainer, fromStdin, destUID), + }, + } +} - tearDown() - t.Cleanup(tearDown) - // If we have custom teardown, do that - if testCase.tearDown != nil { - testCase.tearDown() - t.Cleanup(testCase.tearDown) - } - - // Do the setup - setup() - - // If Docker, removes the err part of expectation - if nerdtest.IsDocker() { - testCase.expect.Err = "" - } - - // Build the final src and dest specifiers, including `containerXYZ:` - container := "" - if copyToContainer { - if fromStdin { - if toStdout { - nerdctlCmd := base.Cmd("cp", "-", "-") - nerdctlCmd.Run() - nerdctlCmd.Assert(testCase.expect) - } else { - sourceSpec = "-" - f, err := os.Open(sourceFile) - assert.NilError(t, err) - nerdctlCmd := base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec) - nerdctlCmd.Stdin = f - - nerdctlCmd.Run() - nerdctlCmd.Assert(testCase.expect) - f.Close() - } - } else { - base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect) - } - container = containerRunning - } else { - nerdctlCmd := base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec) - if toStdout { - out := nerdctlCmd.Out() - nerdctlCmd.Assert(testCase.expect) - - // Since we can't check tar file directly easily, extract to the same destination - tarDst := filepath.Dir(catFile) - tarBinary, _, err := tarutil.FindTarBinary() - assert.NilError(t, err) +func cpRunningSubTest(tc testcases, copyToContainer bool, fromStdin bool, destUID int) *test.Case { + return cpContainerSubTest("running container", tc, copyToContainer, fromStdin, destUID, false) +} - tarCmd := exec.Command(tarBinary, "-C", tarDst, "-xf", "-") - tarCmd.Stdin = strings.NewReader(out) - tarCmd.Stdout = os.Stdout +func cpStoppedSubTest(tc testcases, copyToContainer bool, fromStdin bool, destUID int) *test.Case { + return cpContainerSubTest("stopped container", tc, copyToContainer, fromStdin, destUID, true) +} - tarCmd.Run() - assert.NilError(t, tarCmd.Err) - } else { - nerdctlCmd.Assert(testCase.expect) - } +func cpContainerSubTest(description string, tc testcases, copyToContainer bool, fromStdin bool, destUID int, stopped bool) *test.Case { + return &test.Case{ + Description: description, + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + if stopped && copyToContainer { + tempDir := data.Labels().Get("tempDir") + sourceFileContent := []byte(data.Labels().Get("sourceFileContent")) + sourceFile := data.Labels().Get("sourceFile") + err := os.RemoveAll(tempDir) + assert.NilError(helpers.T(), err) + err = os.MkdirAll(tempDir, dirPerm) + assert.NilError(helpers.T(), err) + cpCreateFileOnHost(helpers.T(), sourceFile, sourceFileContent, fromStdin) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + container := data.Labels().Get("containerRunning") + if stopped { + container = data.Labels().Get("containerStopped") + } + sourceSpec := data.Labels().Get("sourceSpec") + sourceFile := data.Labels().Get("sourceFile") + destinationSpec := data.Labels().Get("destinationSpec") + tempDir := data.Labels().Get("tempDir") + toStdout := data.Labels().Get("toStdout") == "true" + if fromStdin && toStdout { + return helpers.Command("cp", "-", "-") + } + if copyToContainer { + if fromStdin { + cmd := helpers.Command("cp", "-", container+":"+destinationSpec) + cmd.WithFeeder(func() io.Reader { + f, err := os.Open(sourceFile) + assert.NilError(helpers.T(), err) + return f + }) + cmd.WithCwd(tempDir) + return cmd } - - // Run the actual test for the running container - // If we expect the op to be a success, also check the destination file - if testCase.expect.ExitCode == 0 { - assertCatHelper(base, catFile, sourceFileContent, container, destUID, false) + cmd := helpers.Command("cp", sourceSpec, container+":"+destinationSpec) + cmd.WithCwd(tempDir) + return cmd + } + cmd := helpers.Command("cp", container+":"+sourceSpec, destinationSpec) + cmd.WithCwd(tempDir) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + toStdout := data.Labels().Get("toStdout") == "true" + if stopped && rootlessutil.IsRootless() && !nerdtest.IsDocker() && !(fromStdin && toStdout) { + return &test.Expected{ExitCode: 1, Errors: []error{containerutil.ErrRootlessCannotCp}} + } + exitCode := tc.expect.ExitCode + expectErr := tc.expect.Err + if nerdtest.IsDocker() { + expectErr = "" + } + exp := &test.Expected{ExitCode: exitCode} + if expectErr != "" { + exp.Errors = []error{errors.New(expectErr)} + } + if exitCode == 0 { + catFile := data.Labels().Get("catFile") + if !copyToContainer && toStdout && data.Labels().Get("sourceSpec") == srcDirName { + catFile = filepath.Join(filepath.Dir(catFile), filepath.Base(srcDirName), srcFileName) } - - // When copying container > host, we get shadowing from the previous container, possibly hiding failures - // Solution: clear-up the tempDir + sourceFileContent := []byte(data.Labels().Get("sourceFileContent")) + container := "" if copyToContainer { - err := os.RemoveAll(tempDir) - assert.NilError(t, err) - err = os.MkdirAll(tempDir, dirPerm) - assert.NilError(t, err) - createFileOnHost() - defer os.RemoveAll(tempDir) - } - - // ... and for the stopped container - container = "" - var cmd *testutil.Cmd - if fromStdin && toStdout { - cmd = base.Cmd("cp", "-", "-") - } else if copyToContainer { - container = containerStopped - cmd = base.Cmd("cp", sourceSpec, containerStopped+":"+destinationSpec) - if fromStdin { - f, err := os.Open(sourceFile) - assert.NilError(t, err) - defer f.Close() - cmd.Stdin = f - } - } else { - cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec) - } - - if rootlessutil.IsRootless() && !nerdtest.IsDocker() { - if fromStdin && toStdout { - // Regular assert test case should work fine if src and dst are invalid - cmd.Assert(testCase.expect) - } else { - cmd.Assert( - icmd.Expected{ - ExitCode: 1, - Err: containerutil.ErrRootlessCannotCp.Error(), - }) + container = data.Labels().Get("containerRunning") + if stopped { + container = data.Labels().Get("containerStopped") } - return } - - cmd.Assert(testCase.expect) - if testCase.expect.ExitCode == 0 { - assertCatHelper(base, catFile, sourceFileContent, container, destUID, true) + exp.Output = func(stdout string, t tig.T) { + assertCatHelper(helpers, t, catFile, sourceFileContent, container, destUID, stopped) } - }) - } - }) + } + return exp + }, + } } From e7b209d90f7855614415d020940396ae3c719cdc Mon Sep 17 00:00:00 2001 From: Kevin Maris Date: Mon, 13 Jul 2026 16:08:50 -0600 Subject: [PATCH 692/868] update: fix --cpus writing cpuset instead of quota/period The `--cpus` flag was incorrectly assigning opts.CpusetCpus to the CPU Cpus (cpuset) field. Persist the converted quota and period instead, matching how --cpu-quota and --cpu-period are applied. test: run update --cpus subtest against Docker The test-wide require.Not(nerdtest.Docker) prevented the --cpus subtest from running. Moved the docker skipping down to the subtests that need it so the --cpus subtest will run. The cpus subtest also is changed to verify against the container cgroup files but now requires cgroup v2. Closes #5066 Signed-off-by: Kevin Maris --- cmd/nerdctl/container/container_update.go | 7 +++---- .../container/container_update_linux_test.go | 14 +++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/container/container_update.go b/cmd/nerdctl/container/container_update.go index 51240b87d24..507e051c3fa 100644 --- a/cmd/nerdctl/container/container_update.go +++ b/cmd/nerdctl/container/container_update.go @@ -295,10 +295,9 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string, spec.Linux.Resources.CPU.Period = &opts.CPUPeriod } } - if cmd.Flags().Changed("cpus") { - if spec.Linux.Resources.CPU.Cpus != opts.CpusetCpus { - spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus - } + if cmd.Flags().Changed("cpus") && opts.CPUQuota != -1 && opts.CPUPeriod != 0 { + spec.Linux.Resources.CPU.Quota = &opts.CPUQuota + spec.Linux.Resources.CPU.Period = &opts.CPUPeriod } if cmd.Flags().Changed("cpuset-mems") { if spec.Linux.Resources.CPU.Mems != opts.CpusetMems { diff --git a/cmd/nerdctl/container/container_update_linux_test.go b/cmd/nerdctl/container/container_update_linux_test.go index f51b277bbaf..93be65076ef 100644 --- a/cmd/nerdctl/container/container_update_linux_test.go +++ b/cmd/nerdctl/container/container_update_linux_test.go @@ -30,7 +30,6 @@ import ( func TestUpdateContainer(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { containerName := testutil.Identifier(t) @@ -48,6 +47,7 @@ func TestUpdateContainer(t *testing.T) { { Description: "should fail on unsupported restart policy value", NoParallel: true, + Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("update", "--memory", "999999999", "--restart", "123", containerName) @@ -57,12 +57,24 @@ func TestUpdateContainer(t *testing.T) { { Description: "should not update memory in inspect", NoParallel: true, + Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { containerName := data.Labels().Get("containerName") return helpers.Command("inspect", "--mode=native", containerName) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain(`"limit": 999999999,`)), }, + { + Description: "should persist the quota and period converted from --cpus", + NoParallel: true, + Require: nerdtest.CGroupV2, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + containerName := data.Labels().Get("containerName") + helpers.Ensure("update", "--cpus", "0.5", containerName) + return helpers.Command("exec", containerName, "cat", "/sys/fs/cgroup/cpu.max") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("50000 100000")), + }, } testCase.Run(t) From ee43259b948da6f4370dfc721add5fd97c37c521 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:32:21 +0000 Subject: [PATCH 693/868] build(deps): bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/192e21d79ab29983730a13d1382995c2307fbcaa...6599ee8b7a49aef6a770f63d261d214911a7ce02) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 4e0c35b8732..a37b1b7540c 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,7 +92,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 + uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From 76fc92eda11b9bb323b9669629207e0b3f818411 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Thu, 16 Jul 2026 12:28:52 +0900 Subject: [PATCH 694/868] compose: guard x-nerdctl-* extension type assertions The compose image verify/pull/push paths read x-nerdctl-* extension values out of the parsed compose file and assert them to string without checking the type. compose-go accepts any value under an x-* key, so a malformed entry like `x-nerdctl-verify: 123` loads fine and then panics the CLI with "interface conversion: interface {} is int, not string" instead of being ignored or reported. Use the comma-ok form at every extension site (compose up in pkg/cmd/compose, compose pull and push in pkg/composer) so a non-string value falls back to the default rather than crashing. Adds a regression test that drives the real compose-go loader with an int-valued x-nerdctl-verify and checks the provider defaults to none. Signed-off-by: Arpit Jain --- pkg/cmd/compose/compose.go | 24 +++++++------- pkg/cmd/compose/compose_test.go | 55 +++++++++++++++++++++++++++++++++ pkg/composer/pull.go | 24 +++++++------- pkg/composer/push.go | 8 ++--- 4 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 pkg/cmd/compose/compose_test.go diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index fd4e2cfa466..146c4997b99 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -162,27 +162,27 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op func imageVerifyOptionsFromCompose(ps *serviceparser.Service) types.ImageVerifyOptions { var opt types.ImageVerifyOptions - if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { - opt.Provider = verifier.(string) + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify].(string); ok { + opt.Provider = verifier } else { opt.Provider = "none" } // for cosign, if key is given, use key mode, otherwise use keyless mode. - if keyVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { - opt.CosignKey = keyVal.(string) + if keyVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey].(string); ok { + opt.CosignKey = keyVal } - if ciVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity]; ok { - opt.CosignCertificateIdentity = ciVal.(string) + if ciVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity].(string); ok { + opt.CosignCertificateIdentity = ciVal } - if cirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp]; ok { - opt.CosignCertificateIdentityRegexp = cirVal.(string) + if cirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp].(string); ok { + opt.CosignCertificateIdentityRegexp = cirVal } - if coiVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer]; ok { - opt.CosignCertificateOidcIssuer = coiVal.(string) + if coiVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer].(string); ok { + opt.CosignCertificateOidcIssuer = coiVal } - if coirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp]; ok { - opt.CosignCertificateOidcIssuerRegexp = coirVal.(string) + if coirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp].(string); ok { + opt.CosignCertificateOidcIssuerRegexp = coirVal } return opt } diff --git a/pkg/cmd/compose/compose_test.go b/pkg/cmd/compose/compose_test.go new file mode 100644 index 00000000000..7d943bc3a34 --- /dev/null +++ b/pkg/cmd/compose/compose_test.go @@ -0,0 +1,55 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +// TestImageVerifyOptionsFromComposeNonStringExtension makes sure a non-string +// x-nerdctl-verify value in a compose file does not crash the CLI. compose-go +// accepts any value under an x-* key, so a user typo like `x-nerdctl-verify: 123` +// used to reach an unguarded type assertion and panic with +// "interface conversion: interface {} is int, not string". +func TestImageVerifyOptionsFromComposeNonStringExtension(t *testing.T) { + const dockerComposeYAML = ` +services: + app: + image: alpine:latest + x-nerdctl-verify: 123 +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + svcConfig, err := project.GetService("app") + assert.NilError(t, err) + + ps, err := serviceparser.Parse(project, svcConfig) + assert.NilError(t, err) + + opt := imageVerifyOptionsFromCompose(ps) + // A non-string verify value is ignored and falls back to the default. + assert.Equal(t, "none", opt.Provider) +} diff --git a/pkg/composer/pull.go b/pkg/composer/pull.go index 758d342f49e..ae65c01d479 100644 --- a/pkg/composer/pull.go +++ b/pkg/composer/pull.go @@ -52,23 +52,23 @@ func (c *Composer) pullServiceImage(ctx context.Context, image string, platform if po.Quiet { args = append(args, "--quiet") } - if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { - args = append(args, "--verify="+verifier.(string)) + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify].(string); ok { + args = append(args, "--verify="+verifier) } - if publicKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { - args = append(args, "--cosign-key="+publicKey.(string)) + if publicKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey].(string); ok { + args = append(args, "--cosign-key="+publicKey) } - if certificateIdentity, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity]; ok { - args = append(args, "--cosign-certificate-identity="+certificateIdentity.(string)) + if certificateIdentity, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity].(string); ok { + args = append(args, "--cosign-certificate-identity="+certificateIdentity) } - if certificateIdentityRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp]; ok { - args = append(args, "--cosign-certificate-identity-regexp="+certificateIdentityRegexp.(string)) + if certificateIdentityRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp].(string); ok { + args = append(args, "--cosign-certificate-identity-regexp="+certificateIdentityRegexp) } - if certificateOidcIssuer, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer]; ok { - args = append(args, "--cosign-certificate-oidc-issuer="+certificateOidcIssuer.(string)) + if certificateOidcIssuer, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer].(string); ok { + args = append(args, "--cosign-certificate-oidc-issuer="+certificateOidcIssuer) } - if certificateOidcIssuerRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp]; ok { - args = append(args, "--cosign-certificate-oidc-issuer-regexp="+certificateOidcIssuerRegexp.(string)) + if certificateOidcIssuerRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp].(string); ok { + args = append(args, "--cosign-certificate-oidc-issuer-regexp="+certificateOidcIssuerRegexp) } if c.Options.Experimental { diff --git a/pkg/composer/push.go b/pkg/composer/push.go index 5f384601863..02f69277a9c 100644 --- a/pkg/composer/push.go +++ b/pkg/composer/push.go @@ -48,11 +48,11 @@ func (c *Composer) pushServiceImage(ctx context.Context, image string, platform if platform != "" { args = append(args, "--platform="+platform) } - if signer, ok := ps.Unparsed.Extensions[serviceparser.ComposeSign]; ok { - args = append(args, "--sign="+signer.(string)) + if signer, ok := ps.Unparsed.Extensions[serviceparser.ComposeSign].(string); ok { + args = append(args, "--sign="+signer) } - if privateKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPrivateKey]; ok { - args = append(args, "--cosign-key="+privateKey.(string)) + if privateKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPrivateKey].(string); ok { + args = append(args, "--cosign-key="+privateKey) } if c.Options.Experimental { args = append(args, "--experimental") From 7857dcaa59f86a71a01850732063e3775f3aec61 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Mon, 22 Jun 2026 16:26:13 +0530 Subject: [PATCH 695/868] feat(mount): support --mount type=image Mount an image's filesystem into a container read-only, matching Docker: --mount type=image,source=,destination=. The source image is ensured and unpacked, a read-only snapshot view of its rootfs is created and mounted at the destination, and the view is removed when the container is deleted. A readonly/ro option is accepted for compatibility but ignored, as Docker also mounts images read-only. image-subpath is not yet supported. Signed-off-by: Mayur Das --- .../container_run_mount_image_linux_test.go | 137 ++++++++++++++++++ docs/command-reference.md | 7 +- pkg/cmd/container/create.go | 35 ++++- pkg/cmd/container/remove.go | 17 +++ pkg/cmd/container/run_mount.go | 91 +++++++++++- pkg/labels/labels.go | 4 + pkg/mountutil/mountutil.go | 4 + pkg/mountutil/mountutil_linux.go | 31 +++- pkg/mountutil/mountutil_linux_test.go | 97 +++++++++++++ 9 files changed, 414 insertions(+), 9 deletions(-) create mode 100644 cmd/nerdctl/container/container_run_mount_image_linux_test.go diff --git a/cmd/nerdctl/container/container_run_mount_image_linux_test.go b/cmd/nerdctl/container/container_run_mount_image_linux_test.go new file mode 100644 index 00000000000..4e7435f6130 --- /dev/null +++ b/cmd/nerdctl/container/container_run_mount_image_linux_test.go @@ -0,0 +1,137 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// TestRunMountTypeImage verifies that `--mount type=image` mounts the source +// image's filesystem into the container so its files are readable at the target. +func TestRunMountTypeImage(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img", testutil.CommonImage), + testutil.CommonImage, "cat", "/mnt/img/etc/os-release") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("Alpine"), + } + } + + testCase.Run(t) +} + +// TestRunMountTypeImageMultipleDestinations verifies the same image can be +// mounted at two destinations in one container. +func TestRunMountTypeImageMultipleDestinations(t *testing.T) { + testCase := nerdtest.Setup() + // nerdctl-only: Docker keys an image mount by its source image and rejects + // mounting the same image twice ("mount already exists with name"). + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/a", testutil.CommonImage), + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/b", testutil.CommonImage), + testutil.CommonImage, "cat", "/mnt/a/etc/os-release", "/mnt/b/etc/os-release") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("Alpine"), + } + } + + testCase.Run(t) +} + +// TestRunMountTypeImageReadOnly verifies an image mount is read-only so writing +// fails. This matches Docker, which also mounts images read-only. +func TestRunMountTypeImageReadOnly(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img", testutil.CommonImage), + testutil.CommonImage, "touch", "/mnt/img/should-fail") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("Read-only file system")}, + } + } + + testCase.Run(t) +} + +// TestRunMountTypeImageErrors verifies that an image mount missing its source, +// or using the not-yet-supported image-subpath option, is rejected. Docker +// implements image-subpath, so that case diverges and the test is not run +// against Docker. +func TestRunMountTypeImageErrors(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + + testCase.SubTests = []*test.Case{ + { + Description: "missing source", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--mount", "type=image,destination=/mnt/img", + testutil.CommonImage, "true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("source")}, + } + }, + }, + { + Description: "image-subpath not supported", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=etc", testutil.CommonImage), + testutil.CommonImage, "true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("image-subpath")}, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 726655e9ea2..0d3e9beacca 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -298,10 +298,9 @@ Volume flags: Consists of multiple key-value pairs, separated by commas and each consisting of a `=` tuple. e.g., `-- mount type=bind,source=/src,target=/app,bind-propagation=shared`. - - :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`. + - :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`, `image`. The default type will be set to `volume` if not specified. i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volume,src=vol-1,dst=/app,readonly` - - unimplemented type: `image` - Common Options: - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. - :whale: `dst`, `destination`, `target`: Mount destination spec. @@ -326,6 +325,10 @@ Volume flags: Defaults to `1777` or world-writable. - Options specific to `volume`: - unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt` + - Options specific to `image`: + - :whale: `src`, `source`: image reference (mandatory). + - :whale: Currently, the image filesystem is mounted read-only. + - unimplemented options: `image-subpath` - :whale: `--volumes-from`: Mount volumes from the specified container(s), e.g. "--volumes-from my-container". Rootfs flags: diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index b25add2d606..3033503fba3 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -70,7 +70,7 @@ import ( ) // Create will create a container. -func Create(ctx context.Context, client *containerd.Client, args []string, netManager containerutil.NetworkOptionsManager, options types.ContainerCreateOptions) (containerd.Container, func(), error) { +func Create(ctx context.Context, client *containerd.Client, args []string, netManager containerutil.NetworkOptionsManager, options types.ContainerCreateOptions) (_ containerd.Container, _ func(), retErr error) { // Acquire an exclusive lock on the volume store until we are done to avoid being raced by any other // volume operations (or any other operation involving volume manipulation) volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) @@ -94,6 +94,23 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.platform = options.Platform internalLabels.namespace = options.GOptions.Namespace + // If creation fails after image-mount views are created, remove them so the + // snapshots do not leak (the cleanup label is only persisted on success). + defer func() { + if retErr == nil { + return + } + var keys []string + for _, mp := range internalLabels.mountPoints { + if mp.ImageMountSnapshot != "" { + keys = append(keys, mp.ImageMountSnapshot) + } + } + if len(keys) > 0 { + removeImageMountViews(ctx, client.SnapshotService(options.GOptions.Snapshotter), keys) + } + }() + var ( id = idgen.GenerateID() opts []oci.SpecOpts @@ -807,6 +824,22 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.AnonymousVolumes] = string(anonVolumeJSON) } + // Record the snapshot keys of any type=image mount views so they can be + // removed when the container is deleted. + var imageMountSnapshots []string + for _, mp := range internalLabels.mountPoints { + if mp.ImageMountSnapshot != "" { + imageMountSnapshots = append(imageMountSnapshots, mp.ImageMountSnapshot) + } + } + if len(imageMountSnapshots) > 0 { + b, err := json.Marshal(imageMountSnapshots) + if err != nil { + return nil, err + } + m[labels.ImageMountSnapshots] = string(b) + } + if internalLabels.pidFile != "" { m[labels.PIDFile] = internalLabels.pidFile } diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index b9df2b2acaf..744d20aabb3 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -109,6 +109,13 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions return err } + // Capture the container's snapshotter before deletion: image-mount views were + // created against it, which may differ from the current --snapshotter flag. + imageMountSnapshotter := globalOptions.Snapshotter + if info, err := c.Info(ctx); err == nil && info.Snapshotter != "" { + imageMountSnapshotter = info.Snapshotter + } + // Get datastore dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) if err != nil { @@ -275,6 +282,16 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions } } } + + // Remove the read-only views backing type=image mounts - soft failure. + if snapshotsJSON, ok := containerLabels[labels.ImageMountSnapshots]; ok { + var keys []string + if err = json.Unmarshal([]byte(snapshotsJSON), &keys); err != nil { + log.G(ctx).WithError(err).Warnf("failed to unmarshal image-mount snapshots for container %q", id) + } else { + removeImageMountViews(ctx, client.SnapshotService(imageMountSnapshotter), keys) + } + } }() // Get the task. diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 266ca070c82..34ab8fa1230 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -37,6 +37,7 @@ import ( "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/core/leases" "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/continuity/fs" "github.com/containerd/errdefs" @@ -122,17 +123,83 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr return parsed, nil } +// gcRootLabel marks a snapshot as a GC root so containerd does not reclaim it. +const gcRootLabel = "containerd.io/gc.root" + +// setupImageMount ensures and unpacks ref, then creates a read-only GC-rooted +// snapshot view of its rootfs. Image mounts are always read-only, matching +// Docker. It returns the OCI mount for destination and the view's snapshot key. +func setupImageMount(ctx context.Context, client *containerd.Client, options types.ContainerCreateOptions, ref, destination string) (specs.Mount, string, error) { + ensured, err := imgutil.EnsureImage(ctx, client, ref, options.ImagePullOpt) + if err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to ensure image %q for image mount: %w", ref, err) + } + if err := ensured.Image.Unpack(ctx, options.GOptions.Snapshotter); err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to unpack image %q for image mount: %w", ref, err) + } + diffIDs, err := ensured.Image.RootFS(ctx) + if err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to get rootfs of image %q for image mount: %w", ref, err) + } + chainID := identity.ChainID(diffIDs).String() + + snapshotKey := idgen.GenerateID() + "-image-mount" + s := client.SnapshotService(options.GOptions.Snapshotter) + mounts, err := s.View(ctx, snapshotKey, chainID, snapshots.WithLabels(map[string]string{ + gcRootLabel: time.Now().UTC().Format(time.RFC3339), + })) + if err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to create read-only view of image %q: %w", ref, err) + } + // overlayfs and native snapshotters each yield a single mount for a view. + if len(mounts) != 1 { + if rmErr := s.Remove(ctx, snapshotKey); rmErr != nil && !errdefs.IsNotFound(rmErr) { + log.G(ctx).WithError(rmErr).Warnf("failed to remove image-mount snapshot %q", snapshotKey) + } + return specs.Mount{}, "", fmt.Errorf("image mount expects exactly one mount from the snapshotter, got %d", len(mounts)) + } + + m := mounts[0] + opts := m.Options + // A view without an upper dir is already read-only; make it explicit for + // bind-backed snapshotters. + if !strutil.InStringSlice(opts, "ro") { + opts = append(opts, "ro") + } + return specs.Mount{ + Type: m.Type, + Source: m.Source, + Destination: destination, + Options: opts, + }, snapshotKey, nil +} + +// removeImageMountViews removes the snapshotter views created for type=image +// mounts. NotFound is ignored; other failures are logged but not fatal. +func removeImageMountViews(ctx context.Context, s snapshots.Snapshotter, keys []string) { + for _, k := range keys { + if err := s.Remove(ctx, k); err != nil && !errdefs.IsNotFound(err) { + log.G(ctx).WithError(err).Warnf("failed to remove image-mount snapshot %q", k) + } + } +} + // generateMountOpts generates volume-related mount opts. // Other mounts such as procfs mount are not handled here. func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredImage *imgutil.EnsuredImage, - volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) { + volStore volumestore.VolumeStore, options types.ContainerCreateOptions) (opts []oci.SpecOpts, anonVolumes []string, mountPoints []*mountutil.Processed, retErr error) { //nolint:prealloc var ( - opts []oci.SpecOpts - anonVolumes []string - userMounts []specs.Mount - mountPoints []*mountutil.Processed + userMounts []specs.Mount + imageMountViews []string ) + // Remove any image-mount views created here if this function fails, so a + // partial setup does not leak snapshots. + defer func() { + if retErr != nil && len(imageMountViews) > 0 { + removeImageMountViews(ctx, client.SnapshotService(options.GOptions.Snapshotter), imageMountViews) + } + }() mounted := make(map[string]struct{}) var imageVolumes map[string]struct{} var tempDir string @@ -229,6 +296,20 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm } else if len(parsed) > 0 { ociMounts := make([]specs.Mount, len(parsed)) for i, x := range parsed { + // type=image: build the read-only view now and record its snapshot + // key for cleanup on container removal. + if x.Type == mountutil.Image { + m, snapshotKey, err := setupImageMount(ctx, client, options, x.Mount.Source, x.Mount.Destination) + if err != nil { + return nil, nil, nil, err + } + imageMountViews = append(imageMountViews, snapshotKey) + ociMounts[i] = m + x.ImageMountSnapshot = snapshotKey + mounted[filepath.Clean(x.Mount.Destination)] = struct{}{} + continue + } + ociMounts[i] = x.Mount mounted[filepath.Clean(x.Mount.Destination)] = struct{}{} diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index eaec0720efb..c4d38f54803 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -80,6 +80,10 @@ const ( // AnonymousVolumes is a JSON-marshalled string of []string AnonymousVolumes = Prefix + "anonymous-volumes" + // ImageMountSnapshots is a JSON-marshalled []string of snapshotter keys for + // the read-only views backing `--mount type=image`, removed on container deletion. + ImageMountSnapshots = Prefix + "image-mount-snapshots" + // Platform is the normalized platform string like "linux/ppc64le". Platform = Prefix + "platform" diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index 2f78cb7f1f8..f4ce3bd8f1d 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -39,6 +39,7 @@ const ( Bind = "bind" Volume = "volume" Tmpfs = "tmpfs" + Image = "image" Npipe = "npipe" pathSeparator = string(os.PathSeparator) ) @@ -50,6 +51,9 @@ type Processed struct { AnonymousVolume string // anonymous volume name Mode string Opts []oci.SpecOpts + // ImageMountSnapshot is the snapshotter key of the read-only view for a + // type=image mount; empty for other mount types. + ImageMountSnapshot string } type volumeSpec struct { diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 28c67e4dbbd..02da626213b 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -419,9 +419,11 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str mountType = Tmpfs case "bind": mountType = Bind + case "image": + mountType = Image case "volume": default: - return nil, fmt.Errorf("invalid mount type '%s' must be a volume/bind/tmpfs", value) + return nil, fmt.Errorf("invalid mount type '%s' must be a volume/bind/tmpfs/image", value) } case "source", "src": src = value @@ -438,6 +440,10 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str if trueValue { rwOption = key } + case "image-subpath": + // image-subpath is Docker's option to mount a subdirectory of the + // image; it is not implemented yet. + return nil, fmt.Errorf("mount option %q is not yet supported", key) case "bind-propagation": // here don't validate the propagation value // parseVolumeOptions will do that. @@ -474,6 +480,29 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str } } + // type=image's source is an image reference resolved later with a containerd + // client; validate the intent here. Like Docker, an image mount is always + // read-only: a readonly/ro option is accepted for compatibility but the + // mount is read-only regardless of its value. + if mountType == Image { + if src == "" { + return nil, fmt.Errorf("type=image requires a source (the image reference)") + } + if dst == "" { + return nil, fmt.Errorf("type=image requires a destination") + } + return &Processed{ + Type: Image, + // Mode "ro" so inspect/label metadata reports the mount read-only. + Mode: "ro", + Mount: specs.Mount{ + Type: Image, + Source: src, + Destination: cleanMount(dst), + }, + }, nil + } + // Resolve the read-only mode of the mount, for Docker (v25) compatibility. // https://github.com/docker/cli/pull/4316 roMode := readOnlyModeRecursiveIfPossible diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 2074a3c85d7..5ee383d7065 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -568,3 +568,100 @@ func TestProcessFlagMountBindRecursive(t *testing.T) { }) } } + +// TestProcessFlagMountImage tests parsing and validation of `--mount type=image`. +func TestProcessFlagMountImage(t *testing.T) { + tests := []struct { + rawSpec string + wants *Processed + err string + }{ + { + // Image mounts are always read-only, so Mode is "ro". + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{ + Type: Image, + Source: "alpine:latest", + Destination: "/mnt/img", + }, + }, + }, + { + // target and src aliases must work too. + rawSpec: "type=image,src=alpine:latest,target=/mnt/img", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{ + Type: Image, + Source: "alpine:latest", + Destination: "/mnt/img", + }, + }, + }, + { + rawSpec: "type=image,destination=/mnt/img", + err: "requires a source", + }, + { + rawSpec: "type=image,source=alpine:latest", + err: "requires a destination", + }, + { + // ro and rro are accepted for compatibility; image mounts are + // read-only regardless, so Mode stays "ro". + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,ro", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + }, + }, + { + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,rro", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + }, + }, + { + // rw is not a valid --mount token, so it is rejected at parse time, + // same as for a bind mount and same as Docker. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,rw", + err: "must be a key=value pair", + }, + { + // readonly=false is accepted but ignored: like Docker, the image + // mount stays read-only (Mode "ro"). + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,readonly=false", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + }, + }, + { + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=etc", + err: "image-subpath", + }, + } + for _, tt := range tests { + t.Run(tt.rawSpec, func(t *testing.T) { + got, err := ProcessFlagMount(tt.rawSpec, nil, "") + if tt.err != "" { + assert.ErrorContains(t, err, tt.err) + return + } + assert.NilError(t, err) + assert.Equal(t, got.Type, tt.wants.Type) + assert.Equal(t, got.Mode, tt.wants.Mode) + assert.Equal(t, got.Mount.Type, tt.wants.Mount.Type) + assert.Equal(t, got.Mount.Source, tt.wants.Mount.Source) + assert.Equal(t, got.Mount.Destination, tt.wants.Mount.Destination) + }) + } +} From 1d0697297453aa21fc9d67bf494518e6396b38d4 Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Thu, 16 Jul 2026 23:53:38 +0530 Subject: [PATCH 696/868] Refractor container_run_mount_windows_test.go to use Tigron Signed-off-by: Mujib Ahasan --- .../container_run_mount_windows_test.go | 405 +++++++++++------- 1 file changed, 254 insertions(+), 151 deletions(-) diff --git a/cmd/nerdctl/container/container_run_mount_windows_test.go b/cmd/nerdctl/container/container_run_mount_windows_test.go index d75ef3f221e..ca5db265667 100644 --- a/cmd/nerdctl/container/container_run_mount_windows_test.go +++ b/cmd/nerdctl/container/container_run_mount_windows_test.go @@ -17,9 +17,9 @@ package container import ( + "encoding/json" "errors" "fmt" - "os" "strings" "testing" @@ -35,190 +35,293 @@ import ( ) func TestRunMountVolume(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - rwDir, err := os.MkdirTemp(t.TempDir(), "rw") - if err != nil { - t.Fatal(err) - } - roDir, err := os.MkdirTemp(t.TempDir(), "ro") - if err != nil { - t.Fatal(err) - } - rwVolName := tID + "-rw" - roVolName := tID + "-ro" - for _, v := range []string{rwVolName, roVolName} { - defer base.Cmd("volume", "rm", "-f", v).Run() - base.Cmd("volume", "create", v).AssertOK() - } - - containerName := tID - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", - "-d", - "--name", containerName, - "-v", fmt.Sprintf("%s:C:/mnt1", rwDir), - "-v", fmt.Sprintf("%s:C:/mnt2:ro", roDir), - "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), - "-v", fmt.Sprintf("%s:C:/mnt4:ro", roVolName), - testutil.CommonImage, - "ping localhost -t", - ).AssertOK() - - base.Cmd("exec", containerName, "cmd", "/c", "echo -n str1 > C:/mnt1/file1").AssertOK() - base.Cmd("exec", containerName, "cmd", "/c", "echo -n str2 > C:/mnt2/file2").AssertFail() - base.Cmd("exec", containerName, "cmd", "/c", "echo -n str3 > C:/mnt3/file3").AssertOK() - base.Cmd("exec", containerName, "cmd", "/c", "echo -n str4 > C:/mnt4/file4").AssertFail() - base.Cmd("rm", "-f", containerName).AssertOK() - - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:C:/mnt1", rwDir), - "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), - testutil.CommonImage, - "cat", "C:/mnt1/file1", "C:/mnt3/file3", - ).AssertOutContainsAll("str1", "str3") - base.Cmd("run", - "--rm", - "-v", fmt.Sprintf("%s:C:/mnt3/mnt1", rwDir), - "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), - testutil.CommonImage, - "cat", "C:/mnt3/mnt1/file1", "C:/mnt3/file3", - ).AssertOutContainsAll("str1", "str3") -} + testCase := nerdtest.Setup() -func TestRunMountVolumeInspect(t *testing.T) { - base := testutil.NewBase(t) - testContainer := testutil.Identifier(t) - testVolume := testutil.Identifier(t) - - defer base.Cmd("volume", "rm", "-f", testVolume).Run() - base.Cmd("volume", "create", testVolume).AssertOK() - inspectVolume := base.InspectVolume(testVolume) - namedVolumeSource := inspectVolume.Mountpoint - - base.Cmd( - "run", "-d", "--name", testContainer, - "-v", "C:/mnt1", - "-v", "C:/mnt2:C:/mnt2", - "-v", "\\\\.\\pipe\\containerd-containerd:\\\\.\\pipe\\containerd-containerd", - "-v", fmt.Sprintf("%s:C:/mnt3", testVolume), - testutil.CommonImage, - ).AssertOK() - - inspect := base.InspectContainer(testContainer) - // convert array to map to get by key of Destination - actual := make(map[string]dockercompat.MountPoint) - for i := range inspect.Mounts { - actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] + testCase.Setup = func(data test.Data, helpers test.Helpers) { + rwDir := data.Temp().Dir("rw") + roDir := data.Temp().Dir("ro") + rwVolName := data.Identifier("rw") + roVolName := data.Identifier("ro") + + helpers.Ensure("volume", "create", rwVolName) + helpers.Ensure("volume", "create", roVolName) + + helpers.Ensure("run", + "-d", + "--name", data.Identifier(), + "-v", fmt.Sprintf("%s:C:/mnt1", rwDir), + "-v", fmt.Sprintf("%s:C:/mnt2:ro", roDir), + "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName), + "-v", fmt.Sprintf("%s:C:/mnt4:ro", roVolName), + testutil.CommonImage, + "ping localhost -t", + ) + + // Verify rw mounts are writable + helpers.Ensure("exec", data.Identifier(), "cmd", "/c", "echo -n str1 > C:/mnt1/file1") + helpers.Ensure("exec", data.Identifier(), "cmd", "/c", "echo -n str3 > C:/mnt3/file3") + // Verify ro mounts are NOT writable + helpers.Fail("exec", data.Identifier(), "cmd", "/c", "echo -n str2 > C:/mnt2/file2") + helpers.Fail("exec", data.Identifier(), "cmd", "/c", "echo -n str4 > C:/mnt4/file4") + + helpers.Ensure("rm", "-f", data.Identifier()) + + data.Labels().Set("rwDir", rwDir) + data.Labels().Set("rwVolName", rwVolName) } - expected := []struct { - dest string - mountPoint dockercompat.MountPoint - }{ - // anonymous volume - { - dest: "C:\\mnt1", - mountPoint: dockercompat.MountPoint{ - Type: "volume", - Source: "", // source of anonymous volume is a generated path, so here will not check it. - Destination: "C:\\mnt1", - }, - }, - - // bind + testCase.SubTests = []*test.Case{ { - dest: "C:\\mnt2", - mountPoint: dockercompat.MountPoint{ - Type: "bind", - Source: "C:\\mnt2", - Destination: "C:\\mnt2", + Description: "data persists across container removal", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:C:/mnt1", data.Labels().Get("rwDir")), + "-v", fmt.Sprintf("%s:C:/mnt3", data.Labels().Get("rwVolName")), + testutil.CommonImage, + "cat", "C:/mnt1/file1", "C:/mnt3/file3", + ) }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("str1", "str3")), }, - - // named pipe { - dest: "\\\\.\\pipe\\containerd-containerd", - mountPoint: dockercompat.MountPoint{ - Type: "npipe", - Source: "\\\\.\\pipe\\containerd-containerd", - Destination: "\\\\.\\pipe\\containerd-containerd", + Description: "nested mount ordering", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", + "--rm", + "-v", fmt.Sprintf("%s:C:/mnt3/mnt1", data.Labels().Get("rwDir")), + "-v", fmt.Sprintf("%s:C:/mnt3", data.Labels().Get("rwVolName")), + testutil.CommonImage, + "cat", "C:/mnt3/mnt1/file1", "C:/mnt3/file3", + ) }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("str1", "str3")), }, + } - // named volume - { - dest: "C:\\mnt3", - mountPoint: dockercompat.MountPoint{ - Type: "volume", - Name: testVolume, - Source: namedVolumeSource, - Destination: "C:\\mnt3", - }, - }, + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("rw")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("ro")) } - for i := range expected { - testCase := expected[i] - t.Logf("test volume[dest=%q]", testCase.dest) + testCase.Run(t) +} - mountPoint, ok := actual[testCase.dest] - assert.Assert(base.T, ok) +func TestRunMountVolumeInspect(t *testing.T) { + testCase := nerdtest.Setup() - assert.Equal(base.T, testCase.mountPoint.Type, mountPoint.Type) - assert.Equal(base.T, testCase.mountPoint.Destination, mountPoint.Destination) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testVolume := data.Identifier("vol") + + helpers.Ensure("volume", "create", testVolume) + inspectVolume := nerdtest.InspectVolume(helpers, testVolume) + data.Labels().Set("namedVolumeSource", inspectVolume.Mountpoint) + data.Labels().Set("testVolume", testVolume) + + helpers.Ensure( + "run", "-d", "--name", data.Identifier(), + "-v", "C:/mnt1", + "-v", "C:/mnt2:C:/mnt2", + "-v", "\\\\.\\pipe\\containerd-containerd:\\\\.\\pipe\\containerd-containerd", + "-v", fmt.Sprintf("%s:C:/mnt3", testVolume), + testutil.CommonImage, + ) + } - if testCase.mountPoint.Source == "" { - // for anonymous volumes, we want to make sure that the source is not the same as the destination - assert.Assert(base.T, mountPoint.Source != testCase.mountPoint.Destination) - } else { - assert.Equal(base.T, testCase.mountPoint.Source, mountPoint.Source) - } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("vol")) + } - if testCase.mountPoint.Name != "" { - assert.Equal(base.T, testCase.mountPoint.Name, mountPoint.Name) + 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: func(stdout string, t tig.T) { + var dc []dockercompat.Container + + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err) + assert.Equal(t, 1, len(dc)) + + inspect := dc[0] + // convert array to map to get by key of Destination + actual := make(map[string]dockercompat.MountPoint) + for i := range inspect.Mounts { + actual[inspect.Mounts[i].Destination] = inspect.Mounts[i] + } + + expected := []struct { + dest string + mountPoint dockercompat.MountPoint + }{ + // anonymous volume + { + dest: "C:\\mnt1", + mountPoint: dockercompat.MountPoint{ + Type: "volume", + Source: "", + Destination: "C:\\mnt1", + }, + }, + + // bind + { + dest: "C:\\mnt2", + mountPoint: dockercompat.MountPoint{ + Type: "bind", + Source: "C:\\mnt2", + Destination: "C:\\mnt2", + }, + }, + + // named pipe + { + dest: "\\\\.\\pipe\\containerd-containerd", + mountPoint: dockercompat.MountPoint{ + Type: "npipe", + Source: "\\\\.\\pipe\\containerd-containerd", + Destination: "\\\\.\\pipe\\containerd-containerd", + }, + }, + + // named volume + { + dest: "C:\\mnt3", + mountPoint: dockercompat.MountPoint{ + Type: "volume", + Name: data.Labels().Get("testVolume"), + Source: data.Labels().Get("namedVolumeSource"), + Destination: "C:\\mnt3", + }, + }, + } + + for i := range expected { + tc := expected[i] + + mountPoint, ok := actual[tc.dest] + assert.Assert(t, ok, "mount point not found for dest=%q", tc.dest) + + assert.Equal(t, tc.mountPoint.Type, mountPoint.Type) + assert.Equal(t, tc.mountPoint.Destination, mountPoint.Destination) + + if tc.mountPoint.Source == "" { + // for anonymous volumes, we want to make sure that the source is not the same as the destination + assert.Assert(t, mountPoint.Source != tc.mountPoint.Destination) + } else { + assert.Equal(t, tc.mountPoint.Source, mountPoint.Source) + } + + if tc.mountPoint.Name != "" { + assert.Equal(t, tc.mountPoint.Name, mountPoint.Name) + } + } + }, } } + + testCase.Run(t) } func TestRunMountAnonymousVolume(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "-v", "TestVolume:C:/mnt", testutil.CommonImage).AssertOK() + testCase := nerdtest.Setup() - // For docker-campatibility, Unrecognised volume spec: invalid volume specification: 'TestVolume' - base.Cmd("run", "--rm", "-v", "TestVolume", testutil.CommonImage).AssertFail() + testCase.SubTests = []*test.Case{ + { + Description: "named volume with mount path", + Command: test.Command("run", "--rm", "-v", "TestVolume:C:/mnt", testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + // For docker-compatibility, Unrecognised volume spec: invalid volume specification: 'TestVolume' + Description: "volume name only fails", + Command: test.Command("run", "--rm", "-v", "TestVolume", testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "non-absolute destination fails", + Command: test.Command("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } - // Destination must be an absolute path not named volume - base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.CommonImage).AssertFail() + testCase.Run(t) } func TestRunMountRelativePath(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "-v", "./mnt:C:/mnt1", testutil.CommonImage, "cmd").AssertOK() + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "relative source with absolute destination", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./mnt:C:/mnt1", testutil.CommonImage, "cmd") + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + // Destination cannot be a relative path + Description: "relative source only fails", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./mnt", testutil.CommonImage) + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "relative source and relative destination fails", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-v", "./mnt:./mnt1", testutil.CommonImage, "cmd") + cmd.WithCwd(data.Temp().Dir()) + return cmd + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } - // Destination cannot be a relative path - base.Cmd("run", "--rm", "-v", "./mnt", testutil.CommonImage).AssertFail() - base.Cmd("run", "--rm", "-v", "./mnt:./mnt1", testutil.CommonImage, "cmd").AssertFail() + testCase.Run(t) } func TestRunMountNamedPipeVolume(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "-v", `\\.\pipe\containerd-containerd`, testutil.CommonImage).AssertFail() + testCase := nerdtest.Setup() + + testCase.Command = test.Command("run", "--rm", "-v", `\\.\pipe\containerd-containerd`, testutil.CommonImage) + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) + + testCase.Run(t) } func TestRunMountVolumeSpec(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "-v", `InvalidPathC:\TestVolume:C:\Mount`, testutil.CommonImage).AssertFail() - base.Cmd("run", "--rm", "-v", `C:\TestVolume:C:\Mount:ro,rw:boot`, testutil.CommonImage).AssertFail() + testCase := nerdtest.Setup() - // If -v is an empty string, it will be ignored - base.Cmd("run", "--rm", "-v", "", testutil.CommonImage).AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "invalid source path", + Command: test.Command("run", "--rm", "-v", `InvalidPathC:\TestVolume:C:\Mount`, testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "invalid mount options", + Command: test.Command("run", "--rm", "-v", `C:\TestVolume:C:\Mount:ro,rw:boot`, testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + // If -v is an empty string, it will be ignored + Description: "empty volume string ignored", + Command: test.Command("run", "--rm", "-v", "", testutil.CommonImage), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } + + testCase.Run(t) } func TestRunVolumeWithDriveRootDestination(t *testing.T) { From 524c921d5d6a8e3f3f712c042275217c63776741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:32:23 +0000 Subject: [PATCH 697/868] bump actions/setup-go from 6.5.0 to 7.0.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/924ae3a1cded613372ab5595356fb5720e22ba16...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 41fb102692c..2bcb30d7894 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -53,7 +53,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index c16f5ffb5f9..baf7e1af013 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -56,7 +56,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index c74d3bab499..867617afd18 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: "Init: install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ inputs.go-version }} check-latest: true diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 50e5a0c6f00..3ef985dca5f 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -151,7 +151,7 @@ jobs: - if: ${{ env.SHOULD_RUN == 'yes' }} name: "Init: install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 0e1eaf8c835..30d619b8388 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -64,7 +64,7 @@ jobs: - if: ${{ env.GO_VERSION != '' }} name: "Init: install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f69e8230ee..716cce679d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: - name: "Set up QEMU" uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: "Install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "1.26" check-latest: true diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index f5b09edbd3a..42dbbe8609f 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -51,7 +51,7 @@ jobs: echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." - if: ${{ env.GO_VERSION != '' }} name: "Install go" - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true From fe792d5e184c9c97f5c1f61b1ce74618cbff21c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:33:00 +0000 Subject: [PATCH 698/868] bump github.com/mattn/go-isatty from 0.0.22 to 0.0.23 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.22 to 0.0.23. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.22...v0.0.23) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-version: 0.0.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0965b568bb6..caecc121120 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.0 - github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined + github.com/mattn/go-isatty v0.0.23 //gomodjail:unconfined 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 diff --git a/go.sum b/go.sum index b2c52e152b2..157a7dbd9d7 100644 --- a/go.sum +++ b/go.sum @@ -186,8 +186,8 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= +github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= From 7af48b01516553e462e49310e21a55a9bd235de1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 17 Jul 2026 19:28:44 +0900 Subject: [PATCH 699/868] update BuildKit (0.31.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 diff --git a/Dockerfile b/Dockerfile index 80f38e4d85d..89281cedb9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.5.0@c4bb59526d0c9cf3a3a46a04d08ca031749a2119 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.31.1@BINARY +ARG BUILDKIT_VERSION=v0.31.2@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 deleted file mode 100644 index 798584ad2f5..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.1 +++ /dev/null @@ -1,2 +0,0 @@ -1fc78750d0c96bdc18799a3c0b551d6807bd4939e8cd79e357823e467451e16e buildkit-v0.31.1.linux-amd64.tar.gz -a4c9bef205f61c1e4253bdfd196e1db303aca25e8756105d75c6fddfbab7903f buildkit-v0.31.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 new file mode 100644 index 00000000000..83bd5d424c4 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 @@ -0,0 +1,2 @@ +fbabdb72433a35f5bb646e4cd424bf8567e5d055710cf55840f7af2020640791 buildkit-v0.31.2.linux-amd64.tar.gz +41fba1eed480376934fa4c8177ddd7021036b5168a0eb8e7ab5eccdf75d47a05 buildkit-v0.31.2.linux-arm64.tar.gz From bc874f3778002528b3493cb815d3c2fdc50aa3ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:34:21 +0000 Subject: [PATCH 700/868] build(deps): bump github.com/docker/cli Bumps the docker group with 1 update in the / directory: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.6.1+incompatible to 29.6.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.6.1...v29.6.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.6.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7b1ed6cc5c7..0b667bf3c09 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.6.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.6.2+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 diff --git a/go.sum b/go.sum index 157a7dbd9d7..91a1db1c75e 100644 --- a/go.sum +++ b/go.sum @@ -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.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= -github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= +github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= From aca6dd0d63570308fbc831733e7c833b7955b89e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 19 Jul 2026 08:11:36 +0900 Subject: [PATCH 701/868] update runc (1.5.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 89281cedb9c..d3bd3b66493 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.3.3@aad11006b869517fcd3009450b6f82da282e1a9b -ARG RUNC_VERSION=v1.5.0@c4bb59526d0c9cf3a3a46a04d08ca031749a2119 +ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From 67ea5faad202508b547fcedbdfbd791c75cfd035 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 19 Jul 2026 08:16:07 +0900 Subject: [PATCH 702/868] CI: ipv6: consistently use Ubuntu 26.04 The rootless variant was using Ubuntu 24.04 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 6a294cdded6..227a81e6df9 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -88,7 +88,7 @@ jobs: target: rootless binary: "nerdctl.gomodjail" # ipv6 - - runner: ubuntu-24.04 + - runner: ubuntu-26.04 target: rootless ipv6: true skip-flaky: true From d4f4231fbd1af6b404d070ce1b76497b7dfdeafd Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Sun, 19 Jul 2026 00:32:37 +0100 Subject: [PATCH 703/868] test: refactor container_run_linux_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container/container_run_linux_test.go | 870 +++++++++++------- 1 file changed, 525 insertions(+), 345 deletions(-) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index fe18f1a46ce..ff0cc0887f4 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -39,7 +40,6 @@ import ( "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/strutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -82,234 +82,344 @@ func prepareCustomRootfs(base *testutil.Base, imageName string) string { } func TestRunShmSize(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) const shmSize = "32m" - - base.Cmd("run", "--rm", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") + testCase := nerdtest.Setup() + testCase.Command = test.Command("run", "--rm", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("size=32768k")) + testCase.Run(t) } func TestRunShmSizeIPCShareable(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) const shmSize = "32m" - - container := testutil.Identifier(t) - base.Cmd("run", "--rm", "--name", container, "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") - defer base.Cmd("rm", "-f", container) + testCase := nerdtest.Setup() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--name", data.Identifier(), "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("size=32768k")) + testCase.Run(t) } func TestRunIPCShareableRemoveMount(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - container := testutil.Identifier(t) - - base.Cmd("run", "--name", container, "--ipc", "shareable", testutil.AlpineImage, "sleep", "0").AssertOK() - base.Cmd("rm", container).AssertOK() + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), "--ipc", "shareable", testutil.AlpineImage, "sleep", "0") + } + 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("rm", data.Identifier()) + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + testCase.Run(t) } func TestRunIPCContainerNotExists(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - container := testutil.Identifier(t) - result := base.Cmd("run", "--name", container, "--ipc", "container:abcd1234", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() - defer base.Cmd("rm", "-f", container) - combined := result.Combined() - if !strings.Contains(strings.ToLower(combined), "no such container: abcd1234") { - t.Fatalf("unexpected output: %s", combined) + 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", "--name", data.Identifier(), "--ipc", "container:abcd1234", testutil.AlpineImage, "sleep", nerdtest.Infinity) } + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such container: abcd1234")}, nil) + testCase.Run(t) } func TestRunShmSizeIPCContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - const shmSize = "32m" - sharedContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() - baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout()) - defer base.Cmd("rm", "-f", baseContainerID).Run() - - base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", baseContainerID), - testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("shared"), "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("shared")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--ipc=container:"+data.Identifier("shared"), testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("size=32768k")) + testCase.Run(t) } func TestRunIPCContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - const shmSize = "32m" - victimContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() - victimContainerID := strings.TrimSpace(victimContainerResult.Stdout()) - defer base.Cmd("rm", "-f", victimContainerID).Run() - - base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", victimContainerID), - testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k") + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("victim"), "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("victim")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--ipc=container:"+data.Identifier("victim"), testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("size=32768k")) + testCase.Run(t) } func TestRunPidHost(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) pid := os.Getpid() - - base.Cmd("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw").AssertOutContains(strconv.Itoa(pid)) + testCase := nerdtest.Setup() + testCase.Command = test.Command("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(strconv.Itoa(pid))) + testCase.Run(t) } func TestRunUtsHost(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - // Was thinking of os.ReadLink("/proc/1/ns/uts") // but you'd get EPERM for rootless. Just validate the // hostname is the same. - hostName, err := os.Hostname() - assert.NilError(base.T, err) - - base.Cmd("run", "--rm", "--uts=host", testutil.AlpineImage, "hostname").AssertOutContains(hostName) - // Validate we can't provide a hostname with uts=host - base.Cmd("run", "--rm", "--uts=host", "--hostname=foobar", testutil.AlpineImage, "hostname").AssertFail() - // Validate we can't provide a domainname with uts=host - base.Cmd("run", "--rm", "--uts=host", "--domainname=example.com", testutil.AlpineImage, "hostname").AssertFail() + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostName, err := os.Hostname() + assert.NilError(helpers.T(), err) + data.Labels().Set("hostName", hostName) + } + testCase.SubTests = []*test.Case{ + { + Description: "hostname matches host uts", + Command: test.Command("run", "--rm", "--uts=host", testutil.AlpineImage, "hostname"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ExitCode: expect.ExitCodeSuccess, Output: expect.Contains(data.Labels().Get("hostName"))} + }, + }, + { + Description: "hostname flag rejected with host uts", + Command: test.Command("run", "--rm", "--uts=host", "--hostname=foobar", testutil.AlpineImage, "hostname"), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "domainname flag rejected with host uts", + Command: test.Command("run", "--rm", "--uts=host", "--domainname=example.com", testutil.AlpineImage, "hostname"), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + testCase.Run(t) } func TestRunPidContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run() - baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout()) - defer base.Cmd("rm", "-f", baseContainerID).Run() - - base.Cmd("run", "--rm", fmt.Sprintf("--pid=container:%s", baseContainerID), - testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep " + nerdtest.Infinity) + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("shared"), testutil.AlpineImage, "sleep", nerdtest.Infinity) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("shared")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--pid=container:"+data.Identifier("shared"), testutil.AlpineImage, "ps", "ax") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("sleep "+nerdtest.Infinity)) + testCase.Run(t) } func TestRunIpcHost(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - testFilePath := filepath.Join("/dev/shm", - fmt.Sprintf("%s-%d-%s", testutil.Identifier(t), os.Geteuid(), base.Target)) - err := os.WriteFile(testFilePath, []byte(""), 0o644) - assert.NilError(base.T, err) - defer os.Remove(testFilePath) - - base.Cmd("run", "--rm", "--ipc=host", testutil.AlpineImage, "ls", testFilePath).AssertOK() + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + testFilePath := filepath.Join("/dev/shm", fmt.Sprintf("%s-%d", data.Identifier(), os.Geteuid())) + err := os.WriteFile(testFilePath, []byte(""), 0o644) + assert.NilError(helpers.T(), err) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + testFilePath := filepath.Join("/dev/shm", fmt.Sprintf("%s-%d", data.Identifier(), os.Geteuid())) + _ = os.Remove(testFilePath) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + testFilePath := filepath.Join("/dev/shm", fmt.Sprintf("%s-%d", data.Identifier(), os.Geteuid())) + return helpers.Command("run", "--rm", "--ipc=host", testutil.AlpineImage, "ls", testFilePath) + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + testCase.Run(t) } func TestRunAddHost(t *testing.T) { // Not parallelizable (https://github.com/containerd/nerdctl/issues/1127) - base := testutil.NewBase(t) - base.Cmd("run", "--rm", "--add-host", "testing.example.com:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { - var found bool - sc := bufio.NewScanner(bytes.NewBufferString(stdout)) - for sc.Scan() { - // removing spaces and tabs separating items - line := strings.ReplaceAll(sc.Text(), " ", "") - line = strings.ReplaceAll(line, "\t", "") - if strings.Contains(line, "10.0.0.1testing.example.com") { - found = true + response := "This is the expected response for --add-host special IP test." + const hostPort = 8081 + var server *http.Server + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = io.WriteString(w, response) + }) + server = &http.Server{Addr: fmt.Sprintf(":%d", hostPort), Handler: mux, ReadTimeout: 30 * time.Second} + go func() { + err := server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return } - } - if !found { - return errors.New("host was not added") - } - return nil - }) - base.Cmd("run", "--rm", "--add-host", "test:10.0.0.1", "--add-host", "test1:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { - var found int - sc := bufio.NewScanner(bytes.NewBufferString(stdout)) - for sc.Scan() { - // removing spaces and tabs separating items - line := strings.ReplaceAll(sc.Text(), " ", "") - line = strings.ReplaceAll(line, "\t", "") - if strutil.InStringSlice([]string{"10.0.0.1test", "10.0.0.1test1"}, line) { - found++ + }() + var err error + for i := 0; i < 50; i++ { + var resp *http.Response + resp, err = http.Get(fmt.Sprintf("http://127.0.0.1:%d", hostPort)) + if err == nil { + _ = resp.Body.Close() + return } + time.Sleep(100 * time.Millisecond) } - if found != 2 { - return fmt.Errorf("host was not added, found %d", found) + assert.NilError(helpers.T(), err) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if server == nil { + return } - return nil - }) - base.Cmd("run", "--rm", "--add-host", "10.0.0.1:testing.example.com", testutil.AlpineImage, "cat", "/etc/hosts").AssertFail() - - response := "This is the expected response for --add-host special IP test." - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, response) - }) - const hostPort = 8081 - s := http.Server{Addr: fmt.Sprintf(":%d", hostPort), Handler: nil, ReadTimeout: 30 * time.Second} - go s.ListenAndServe() - defer s.Shutdown(context.Background()) - base.Cmd("run", "--rm", "--add-host", "test:host-gateway", testutil.NginxAlpineImage, "curl", fmt.Sprintf("test:%d", hostPort)).AssertOutExactly(response) + err := server.Shutdown(context.Background()) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + assert.NilError(helpers.T(), err) + } + } + testCase.SubTests = []*test.Case{ + { + Description: "single add-host entry is written", + NoParallel: true, + Command: test.Command("run", "--rm", "--add-host", "testing.example.com:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.NilError(t, assertAddHostEntries(stdout, []string{"10.0.0.1testing.example.com"})) + }), + }, + { + Description: "multiple add-host entries are written", + NoParallel: true, + Command: test.Command("run", "--rm", "--add-host", "test:10.0.0.1", "--add-host", "test1:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.NilError(t, assertAddHostEntries(stdout, []string{"10.0.0.1test", "10.0.0.1test1"})) + }), + }, + { + Description: "invalid add-host input fails", + NoParallel: true, + Command: test.Command("run", "--rm", "--add-host", "10.0.0.1:testing.example.com", testutil.AlpineImage, "cat", "/etc/hosts"), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "host-gateway resolves host service", + NoParallel: true, + Command: test.Command("run", "--rm", "--add-host", "test:host-gateway", testutil.NginxAlpineImage, "curl", fmt.Sprintf("test:%d", hostPort)), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(response)), + }, + } + testCase.Run(t) } func TestRunAddHostWithCustomHostGatewayIP(t *testing.T) { // Not parallelizable (https://github.com/containerd/nerdctl/issues/1127) - base := testutil.NewBase(t) - testutil.DockerIncompatible(t) - base.Cmd("run", "--rm", "--host-gateway-ip", "192.168.5.2", "--add-host", "test:host-gateway", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error { - var found bool - sc := bufio.NewScanner(bytes.NewBufferString(stdout)) - for sc.Scan() { - // removing spaces and tabs separating items - line := strings.ReplaceAll(sc.Text(), " ", "") - line = strings.ReplaceAll(line, "\t", "") - if strings.Contains(line, "192.168.5.2test") { - found = true - } - } - if !found { - return errors.New("host was not added") - } - return nil + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.NoParallel = true + testCase.Command = test.Command("run", "--rm", "--host-gateway-ip", "192.168.5.2", "--add-host", "test:host-gateway", testutil.AlpineImage, "cat", "/etc/hosts") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.NilError(t, assertAddHostEntries(stdout, []string{"192.168.5.2test"})) }) + testCase.Run(t) } func TestRunUlimit(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) ulimit := "nofile=622:622" ulimit2 := "nofile=622:722" - - base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n") - base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("622\n") - - base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n") - base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("722\n") + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "soft limit matches identical hard limit", + Command: test.Command("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Sn"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("622\n")), + }, + { + Description: "hard limit matches identical hard limit", + Command: test.Command("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Hn"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("622\n")), + }, + { + Description: "soft limit uses first value", + Command: test.Command("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Sn"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("622\n")), + }, + { + Description: "hard limit uses second value", + Command: test.Command("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Hn"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("722\n")), + }, + } + testCase.Run(t) } func TestRunWithInit(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - testutil.RequireExecutable(t, "tini-custom") - base := testutil.NewBase(t) - - container := testutil.Identifier(t) - base.Cmd("run", "-d", "--name", container, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", container).Run() - - base.Cmd("stop", "--time=3", container).AssertOK() - // Unable to handle TERM signal, be killed when timeout - assert.Equal(t, base.InspectContainer(container).State.ExitCode, 137) - - // Test with --init-path - container1 := container + "-1" - base.Cmd("run", "-d", "--name", container1, "--init-binary", "tini-custom", - testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", container1).Run() - - base.Cmd("stop", "--time=3", container1).AssertOK() - assert.Equal(t, base.InspectContainer(container1).State.ExitCode, 143) - - // Test with --init - container2 := container + "-2" - base.Cmd("run", "-d", "--name", container2, "--init", - testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", container2).Run() - - base.Cmd("stop", "--time=3", container2).AssertOK() - assert.Equal(t, base.InspectContainer(container2).State.ExitCode, 143) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + if _, err := exec.LookPath("tini-custom"); err != nil { + helpers.T().Skip("required executable doesn't exist in PATH: tini-custom") + } + } + testCase.SubTests = []*test.Case{ + { + // Unable to handle TERM signal, be killed when timeout + Description: "without init exits with SIGKILL timeout status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("plain"), testutil.AlpineImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("plain")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", "--time=3", data.Identifier("plain")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assert.Equal(t, "137", strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.State.ExitCode}}", data.Identifier("plain")))) + }, + } + }, + }, + { + // Test with --init-binary + Description: "custom init binary exits with SIGTERM status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("custom"), "--init-binary", "tini-custom", testutil.AlpineImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("custom")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", "--time=3", data.Identifier("custom")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assert.Equal(t, "143", strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.State.ExitCode}}", data.Identifier("custom")))) + }, + } + }, + }, + { + // Test with --init + Description: "default init exits with SIGTERM status", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("default"), "--init", testutil.AlpineImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("default")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("stop", "--time=3", data.Identifier("default")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assert.Equal(t, "143", strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.State.ExitCode}}", data.Identifier("default")))) + }, + } + }, + }, + } + testCase.Run(t) } func TestRunTTY(t *testing.T) { @@ -324,7 +434,7 @@ func TestRunTTY(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("run", "-it", data.Identifier(), "stty") + cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage, "stty") cmd.WithPseudoTTY() return cmd }, @@ -336,7 +446,7 @@ func TestRunTTY(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("run", "-t", data.Identifier(), "stty") + cmd := helpers.Command("run", "-t", "--name", data.Identifier(), testutil.CommonImage, "stty") cmd.WithPseudoTTY() return cmd }, @@ -348,7 +458,7 @@ func TestRunTTY(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("run", "-i", data.Identifier(), "stty") + cmd := helpers.Command("run", "-i", "--name", data.Identifier(), testutil.CommonImage, "stty") cmd.WithPseudoTTY() return cmd }, @@ -360,7 +470,7 @@ func TestRunTTY(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("run", data.Identifier(), "stty") + cmd := helpers.Command("run", "--name", data.Identifier(), testutil.CommonImage, "stty") cmd.WithPseudoTTY() return cmd }, @@ -372,13 +482,14 @@ func TestRunTTY(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("run", "-td", data.Identifier(), "stty") + cmd := helpers.Command("run", "-td", "--name", data.Identifier(), testutil.CommonImage, "stty") cmd.WithPseudoTTY() return cmd }, Expected: test.Expects(0, nil, nil), }, } + testCase.Run(t) } func TestRunSigProxy(t *testing.T) { @@ -447,72 +558,86 @@ func TestRunSigProxy(t *testing.T) { } func TestRunWithFluentdLogDriver(t *testing.T) { - base := testutil.NewBase(t) - tempDirectory := t.TempDir() - err := os.Chmod(tempDirectory, 0o777) - assert.NilError(t, err) - - containerName := testutil.Identifier(t) - base.Cmd("run", "-d", "--name", containerName, "-p", "24224:24224", - "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK() - defer base.Cmd("rm", "-f", containerName).AssertOK() - time.Sleep(3 * time.Second) - - testContainerName := containerName + "test" - base.Cmd("run", "-d", "--log-driver", "fluentd", "--name", testContainerName, testutil.CommonImage, - "sh", "-c", "echo test").AssertOK() - defer base.Cmd("rm", "-f", testContainerName).AssertOK() - - inspectedContainer := base.InspectContainer(testContainerName) - matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log") - assert.NilError(t, err) - assert.Equal(t, 1, len(matches)) - - data, err := os.ReadFile(matches[0]) - assert.NilError(t, err) - logData := string(data) - assert.Equal(t, true, strings.Contains(logData, "test")) - assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID)) + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tempDirectory := data.Temp().Dir("fluentd") + err := os.Chmod(tempDirectory, 0o777) + assert.NilError(helpers.T(), err) + data.Labels().Set("tempDirectory", tempDirectory) + helpers.Ensure("run", "-d", "--name", data.Identifier("fluentd"), "-p", "24224:24224", "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage) + time.Sleep(3 * time.Second) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("test")) + helpers.Anyhow("rm", "-f", data.Identifier("fluentd")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--log-driver", "fluentd", "--name", data.Identifier("test"), testutil.CommonImage, "sh", "-c", "echo test") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + inspectedContainerID := strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.Id}}", data.Identifier("test"))) + matches, err := filepath.Glob(filepath.Join(data.Labels().Get("tempDirectory"), "data.*.log")) + assert.NilError(t, err) + assert.Equal(t, 1, len(matches)) + content, err := os.ReadFile(matches[0]) + assert.NilError(t, err) + logData := string(content) + assert.Assert(t, strings.Contains(logData, "test")) + assert.Assert(t, strings.Contains(logData, inspectedContainerID)) + }, + } + } + testCase.Run(t) } func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) { - base := testutil.NewBase(t) - tempDirectory := t.TempDir() - err := os.Chmod(tempDirectory, 0o777) - assert.NilError(t, err) - - containerName := testutil.Identifier(t) - base.Cmd("run", "-d", "--name", containerName, "-p", "24225:24224", - "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK() - defer base.Cmd("rm", "-f", containerName).AssertOK() - time.Sleep(3 * time.Second) - - testContainerName := containerName + "test" - base.Cmd("run", "-d", "--log-driver", "fluentd", "--log-opt", "fluentd-address=127.0.0.1:24225", - "--name", testContainerName, testutil.CommonImage, "sh", "-c", "echo test2").AssertOK() - defer base.Cmd("rm", "-f", testContainerName).AssertOK() - - inspectedContainer := base.InspectContainer(testContainerName) - matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log") - assert.NilError(t, err) - assert.Equal(t, 1, len(matches)) - - data, err := os.ReadFile(matches[0]) - assert.NilError(t, err) - logData := string(data) - assert.Equal(t, true, strings.Contains(logData, "test2")) - assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID)) + testCase := nerdtest.Setup() + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tempDirectory := data.Temp().Dir("fluentd") + err := os.Chmod(tempDirectory, 0o777) + assert.NilError(helpers.T(), err) + data.Labels().Set("tempDirectory", tempDirectory) + helpers.Ensure("run", "-d", "--name", data.Identifier("fluentd"), "-p", "24225:24224", "-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage) + time.Sleep(3 * time.Second) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("test")) + helpers.Anyhow("rm", "-f", data.Identifier("fluentd")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--log-driver", "fluentd", "--log-opt", "fluentd-address=127.0.0.1:24225", "--name", data.Identifier("test"), testutil.CommonImage, "sh", "-c", "echo test2") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + inspectedContainerID := strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.Id}}", data.Identifier("test"))) + matches, err := filepath.Glob(filepath.Join(data.Labels().Get("tempDirectory"), "data.*.log")) + assert.NilError(t, err) + assert.Equal(t, 1, len(matches)) + content, err := os.ReadFile(matches[0]) + assert.NilError(t, err) + logData := string(content) + assert.Assert(t, strings.Contains(logData, "test2")) + assert.Assert(t, strings.Contains(logData, inspectedContainerID)) + }, + } + } + testCase.Run(t) } func TestRunWithOOMScoreAdj(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("test skipped for rootless containers.") - } - t.Parallel() - base := testutil.NewBase(t) score := "-42" - - base.Cmd("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj").AssertOutContains(score) + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Rootful + testCase.Command = test.Command("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj") + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(score)) + testCase.Run(t) } func TestRunWithDetachKeys(t *testing.T) { @@ -560,24 +685,55 @@ func TestRunWithDetachKeys(t *testing.T) { } func TestRunWithTtyAndDetached(t *testing.T) { - base := testutil.NewBase(t) imageName := testutil.CommonImage - withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) - withTtyContainerName := "with-terminal-" + testutil.Identifier(t) - - // without -t, fail - base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() - base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") - withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) - assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) - - // with -t, success - base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() - base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") - withTtyContainer := base.InspectContainer(withTtyContainerName) - assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + // without -t, fail + Description: "without tty logs not-a-tty error", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier("without-terminal"), imageName, "stty") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier("without-terminal")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier("without-terminal")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Errors: []error{errors.New("stty: standard input: Not a tty")}, + Output: func(stdout string, t tig.T) { + assert.Equal(t, "1", strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.State.ExitCode}}", data.Identifier("without-terminal")))) + }, + } + }, + }, + { + // with -t, success + Description: "with tty logs stty output", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "-t", "--name", data.Identifier("with-terminal"), imageName, "stty") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier("with-terminal")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier("with-terminal")) + }, + 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, "speed 38400 baud; line = 0;")) + assert.Equal(t, "0", strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.State.ExitCode}}", data.Identifier("with-terminal")))) + }, + } + }, + }, + } + testCase.Run(t) } // TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568 @@ -670,10 +826,6 @@ func TestPortBindingWithCustomHost(t *testing.T) { } func TestRunDeviceCDI(t *testing.T) { - t.Parallel() - // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. - testutil.DockerIncompatible(t) - cdiSpecDir := filepath.Join(t.TempDir(), "cdi") const testCDIVendor1 = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" @@ -683,21 +835,22 @@ devices: env: - FOO=injected ` - writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) - - base := testutil.NewBase(t) - base.Cmd("--cdi-spec-dirs", cdiSpecDir, "run", - "--rm", - "--device", "vendor1.com/device=foo", - testutil.AlpineImage, "env", - ).AssertOutContains("FOO=injected") + testCase := nerdtest.Setup() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cdiSpecDir := data.Temp().Dir("cdi") + writeTestCDISpecTigron(helpers.T(), testCDIVendor1, "vendor1.yaml", cdiSpecDir) + data.Labels().Set("cdiSpecDir", cdiSpecDir) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--cdi-spec-dirs", data.Labels().Get("cdiSpecDir"), "run", "--rm", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("FOO=injected")) + testCase.Run(t) } func TestRunDeviceCDIWithNerdctlConfig(t *testing.T) { - t.Parallel() - // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. - testutil.DockerIncompatible(t) - cdiSpecDir := filepath.Join(t.TempDir(), "cdi") const testCDIVendor1 = ` cdiVersion: "0.3.0" kind: "vendor1.com/device" @@ -707,28 +860,28 @@ devices: env: - FOO=injected ` - writeTestCDISpec(t, testCDIVendor1, "vendor1.yaml", cdiSpecDir) - - tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") - err := os.WriteFile(tomlPath, []byte(fmt.Sprintf(` -cdi_spec_dirs = ["%s"] -`, cdiSpecDir)), 0o400) - assert.NilError(t, err) - - base := testutil.NewBase(t) - base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) - base.Cmd("run", - "--rm", - "--device", "vendor1.com/device=foo", - testutil.AlpineImage, "env", - ).AssertOutContains("FOO=injected") + testCase := nerdtest.Setup() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cdiSpecDir := data.Temp().Dir("cdi") + writeTestCDISpecTigron(helpers.T(), testCDIVendor1, "vendor1.yaml", cdiSpecDir) + tomlPath := filepath.Join(data.Temp().Path(), "nerdctl.toml") + err := os.WriteFile(tomlPath, []byte(fmt.Sprintf("\ncdi_spec_dirs = [\"%s\"]\n", cdiSpecDir)), 0o400) + assert.NilError(helpers.T(), err) + data.Labels().Set("tomlPath", tomlPath) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env") + cmd.Setenv("NERDCTL_TOML", data.Labels().Get("tomlPath")) + return cmd + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("FOO=injected")) + testCase.Run(t) } // TestRunGPU tests GPU injection using the --gpus flag. func TestRunGPU(t *testing.T) { - t.Parallel() - // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. - testutil.DockerIncompatible(t) const nvidiaSpec = ` cdiVersion: "0.5.0" kind: "nvidia.com/gpu" @@ -765,13 +918,7 @@ devices: - UNKNOWN_GPU_0=injected ` - testCases := []struct { - name string - specs map[string]string - gpuFlags []string - expectedEnvs []string - expectFail bool - }{ + testCases := []runGPUTestCase{ { name: "nvidia device injection", specs: map[string]string{"nvidia.yaml": nvidiaSpec}, @@ -797,41 +944,15 @@ devices: expectFail: true, }, } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - tmpDir := t.TempDir() - for fileName, spec := range tc.specs { - writeTestCDISpec(t, spec, fileName, tmpDir) - } - - base := testutil.NewBase(t) - args := []string{"--cdi-spec-dirs", tmpDir, "run", "--rm"} - args = append(args, tc.gpuFlags...) - args = append(args, testutil.AlpineImage, "env") - - if tc.expectFail { - base.Cmd(args...).AssertFail() - } else { - base.Cmd(args...).AssertOutWithFunc(func(stdout string) error { - for _, expectedEnv := range tc.expectedEnvs { - if !strings.Contains(stdout, expectedEnv) { - return fmt.Errorf("%s not found", expectedEnv) - } - } - return nil - }) - } - }) - } + testCase := nerdtest.Setup() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testCase.Require = require.Not(nerdtest.Docker) + testCase.SubTests = runGPUCases(testCases) + testCase.Run(t) } // TestRunGPUWithOtherCDIDevices tests GPU CDI injection along with other CDI devices. func TestRunGPUWithOtherCDIDevices(t *testing.T) { - t.Parallel() - // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. - testutil.DockerIncompatible(t) const amdSpec = ` cdiVersion: "0.5.0" kind: "amd.com/gpu" @@ -854,31 +975,90 @@ devices: env: - FOO=injected ` + testCase := nerdtest.Setup() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cdiSpecDir := data.Temp().Dir("cdi") + writeTestCDISpecTigron(helpers.T(), amdSpec, "amd.yaml", cdiSpecDir) + writeTestCDISpecTigron(helpers.T(), vendor1Spec, "vendor1.yaml", cdiSpecDir) + data.Labels().Set("cdiSpecDir", cdiSpecDir) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--cdi-spec-dirs", data.Labels().Get("cdiSpecDir"), "run", "--rm", "--gpus", "2", "--device", "vendor1.com/device=foo", testutil.AlpineImage, "env") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "AMD_GPU_0=injected")) + assert.Assert(t, strings.Contains(stdout, "AMD_GPU_1=injected")) + assert.Assert(t, strings.Contains(stdout, "FOO=injected")) + }) + testCase.Run(t) +} - tmpDir := t.TempDir() - writeTestCDISpec(t, amdSpec, "amd.yaml", tmpDir) - writeTestCDISpec(t, vendor1Spec, "vendor1.yaml", tmpDir) - - base := testutil.NewBase(t) - base.Cmd("--cdi-spec-dirs", tmpDir, "run", "--rm", - "--gpus", "2", - "--device", "vendor1.com/device=foo", - testutil.AlpineImage, "env", - ).AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "AMD_GPU_0=injected") { - return errors.New("AMD_GPU_0=injected not found") - } - if !strings.Contains(stdout, "AMD_GPU_1=injected") { - return errors.New("AMD_GPU_1=injected not found") +type runGPUTestCase struct { + name string + specs map[string]string + gpuFlags []string + expectedEnvs []string + expectFail bool +} + +func runGPUCases(cases []runGPUTestCase) []*test.Case { + subTests := make([]*test.Case, len(cases)) + for i, tc := range cases { + i, tc := i, tc + subTests[i] = &test.Case{ + Description: tc.name, + Setup: func(data test.Data, helpers test.Helpers) { + cdiSpecDir := data.Temp().Dir("cdi") + for fileName, spec := range tc.specs { + writeTestCDISpecTigron(helpers.T(), spec, fileName, cdiSpecDir) + } + data.Labels().Set("cdiSpecDir", cdiSpecDir) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + args := []string{"--cdi-spec-dirs", data.Labels().Get("cdiSpecDir"), "run", "--rm"} + args = append(args, tc.gpuFlags...) + args = append(args, testutil.AlpineImage, "env") + return helpers.Command(args...) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if tc.expectFail { + return &test.Expected{ExitCode: expect.ExitCodeGenericFail} + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + for _, expectedEnv := range tc.expectedEnvs { + assert.Assert(t, strings.Contains(stdout, expectedEnv), expectedEnv+" not found") + } + }, + } + }, } - if !strings.Contains(stdout, "FOO=injected") { - return errors.New("FOO=injected not found") + } + return subTests +} + +// assertAddHostEntries checks that each expected entry appears in stdout +// after removing spaces and tabs separating items. +func assertAddHostEntries(stdout string, expected []string) error { + var found int + sc := bufio.NewScanner(bytes.NewBufferString(stdout)) + for sc.Scan() { + line := strings.ReplaceAll(sc.Text(), " ", "") + line = strings.ReplaceAll(line, "\t", "") + if strutil.InStringSlice(expected, line) { + found++ } - return nil - }) + } + if found != len(expected) { + return fmt.Errorf("host was not added, found %d", found) + } + return nil } -func writeTestCDISpec(t *testing.T, spec string, fileName string, cdiSpecDir string) { +func writeTestCDISpecTigron(t tig.T, spec string, fileName string, cdiSpecDir string) { err := os.MkdirAll(cdiSpecDir, 0o700) assert.NilError(t, err) cdiSpecPath := filepath.Join(cdiSpecDir, fileName) From ae44c500297a07212b4d6d43a64a98f07d971a8f Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Sun, 19 Jul 2026 01:41:28 +0100 Subject: [PATCH 704/868] test: refactor container_run_cgroup_linux_test.go to use Tigron Signed-off-by: Daniel Benjamin --- .../container_run_cgroup_linux_test.go | 583 +++++++++++------- 1 file changed, 365 insertions(+), 218 deletions(-) diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index 5b8807c1227..1203e71e32a 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -29,8 +29,8 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/cgroups/v3" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/defaults" "github.com/containerd/continuity/testutil/loopback" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" @@ -38,36 +38,36 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/cmd/container" - "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunCgroupV2(t *testing.T) { - t.Parallel() - if cgroups.Mode() != cgroups.Unified { - t.Skip("test requires cgroup v2") - } - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") - } + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.CGroupV2, + nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") + } + if !info.MemoryLimit { + return fmt.Errorf("test requires MemoryLimit") + } + if !info.SwapLimit { + return fmt.Errorf("test requires SwapLimit") + } + if !info.CPUSet { + return fmt.Errorf("test requires CPUSet") + } + if !info.PidsLimit { + return fmt.Errorf("test requires PidsLimit") + } + return nil + }), + ) - if !info.MemoryLimit { - t.Skip("test requires MemoryLimit") - } - if !info.SwapLimit { - t.Skip("test requires SwapLimit") - } - if !info.CPUSet { - t.Skip("test requires CPUSet") - } - if !info.PidsLimit { - t.Skip("test requires PidsLimit") - } const expected1 = `42000 100000 44040192 44040192 @@ -84,82 +84,149 @@ func TestRunCgroupV2(t *testing.T) { 0 ` - base.Cmd("run", "--rm", - "--cpus", "0.42", "--cpuset-mems", "0", - "--memory", "42m", - "--pids-limit", "42", - "--cpuset-cpus", "0-1", - "-w", "/sys/fs/cgroup", testutil.AlpineImage, - "cat", "cpu.max", "memory.max", "memory.swap.max", - "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) - base.Cmd("run", "--rm", - "--cpu-quota", "42000", "--cpuset-mems", "0", - "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", - "--pids-limit", "42", "--cpuset-cpus", "0-1", - "-w", "/sys/fs/cgroup", testutil.AlpineImage, - "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", - "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) - - base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate1", "-w", "/sys/fs/cgroup", "-d", - testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate1").Run() - update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", - "--memory", "42m", - "--pids-limit", "42", "--cpuset-cpus", "0-1"} - if nerdtest.IsDocker() && info.CgroupVersion == "2" && info.SwapLimit { - // Workaround for Docker with cgroup v2: - // > Error response from daemon: Cannot update container 67c13276a13dd6a091cdfdebb355aa4e1ecb15fbf39c2b5c9abee89053e88fce: - // > Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time - update = append(update, "--memory-swap=84m") + testCase.SubTests = []*test.Case{ + { + Description: "cpus and memory", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--cpus", "0.42", "--cpuset-mems", "0", + "--memory", "42m", + "--pids-limit", "42", + "--cpuset-cpus", "0-1", + "-w", "/sys/fs/cgroup", testutil.AlpineImage, + "cat", "cpu.max", "memory.max", "memory.swap.max", + "pids.max", "cpuset.cpus", "cpuset.mems") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected1), + } + }, + }, + { + Description: "explicit quota and period", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--cpu-quota", "42000", "--cpuset-mems", "0", + "--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", + "--pids-limit", "42", "--cpuset-cpus", "0-1", + "-w", "/sys/fs/cgroup", testutil.AlpineImage, + "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", + "cpuset.cpus", "cpuset.mems") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected2), + } + }, + }, + { + Description: "update basic", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), "-w", "/sys/fs/cgroup", "-d", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", + "--memory", "42m", + "--pids-limit", "42", "--cpuset-cpus", "0-1"} + if nerdtest.IsDocker() { + // Workaround for Docker with cgroup v2: + // > Error response from daemon: Cannot update container ...: + // > Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time + update = append(update, "--memory-swap=84m") + } + update = append(update, data.Identifier()) + helpers.Ensure(update...) + }, + 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("exec", data.Identifier(), + "cat", "cpu.max", "memory.max", "memory.swap.max", + "pids.max", "cpuset.cpus", "cpuset.mems") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected1), + } + }, + }, + { + Description: "update with reservation and swap", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), "-w", "/sys/fs/cgroup", "-d", + testutil.AlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", + "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", + "--pids-limit", "42", "--cpuset-cpus", "0-1", + data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier(), + "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", + "pids.max", "cpuset.cpus", "cpuset.mems") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected2), + } + }, + }, + { + Description: "writable-cgroups true", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "writable-cgroups false", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "writable-cgroups default", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, } - update = append(update, testutil.Identifier(t)+"-testUpdate1") - base.Cmd(update...).AssertOK() - base.Cmd("exec", testutil.Identifier(t)+"-testUpdate1", - "cat", "cpu.max", "memory.max", "memory.swap.max", - "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1) - - defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate2").Run() - base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate2", "-w", "/sys/fs/cgroup", "-d", - testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - base.EnsureContainerStarted(testutil.Identifier(t) + "-testUpdate2") - - base.Cmd("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", - "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", - "--pids-limit", "42", "--cpuset-cpus", "0-1", - testutil.Identifier(t)+"-testUpdate2").AssertOK() - base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2", - "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", - "pids.max", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) - base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertOK() - base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() - base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() + testCase.Run(t) } func TestRunCgroupV1(t *testing.T) { - t.Parallel() - switch cgroups.Mode() { - case cgroups.Legacy, cgroups.Hybrid: - default: - t.Skip("test requires cgroup v1") - } - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") - } - if !info.MemoryLimit { - t.Skip("test requires MemoryLimit") - } - if !info.CPUShares { - t.Skip("test requires CPUShares") - } - if !info.CPUSet { - t.Skip("test requires CPUSet") - } - if !info.PidsLimit { - t.Skip("test requires PidsLimit") - } + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.CGroupV2), + nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") + } + if !info.MemoryLimit { + return fmt.Errorf("test requires MemoryLimit") + } + if !info.CPUShares { + return fmt.Errorf("test requires CPUShares") + } + if !info.CPUSet { + return fmt.Errorf("test requires CPUSet") + } + if !info.PidsLimit { + return fmt.Errorf("test requires PidsLimit") + } + return nil + }), + ) + quota := "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" period := "/sys/fs/cgroup/cpu/cpu.cfs_period_us" cpusetMems := "/sys/fs/cgroup/cpuset/cpuset.mems" @@ -170,60 +237,103 @@ func TestRunCgroupV1(t *testing.T) { pidsLimit := "/sys/fs/cgroup/pids/pids.max" cpuShare := "/sys/fs/cgroup/cpu/cpu.shares" cpusetCpus := "/sys/fs/cgroup/cpuset/cpuset.cpus" - const expected = "42000\n100000\n0\n44040192\n6291456\n104857600\n0\n42\n2000\n0-1\n" - base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) - base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected) - base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertOK() - base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() - base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() + + testCase.SubTests = []*test.Case{ + { + Description: "cpus and memory", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected), + } + }, + }, + { + Description: "explicit quota and period", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(expected), + } + }, + }, + { + Description: "writable-cgroups true", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "writable-cgroups false", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "writable-cgroups default", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + testCase.Run(t) } // TestIssue3781 tests https://github.com/containerd/nerdctl/issues/3781 func TestIssue3781(t *testing.T) { - t.Parallel() testCase := nerdtest.Setup() - testCase.Require = require.Not(nerdtest.Docker) - - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") - } - containerName := testutil.Identifier(t) - base.Cmd("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", "infinity").AssertOK() - defer func() { - base.Cmd("rm", "-f", containerName) - }() - base.Cmd("update", "--cpuset-cpus", "0-1", containerName).AssertOK() - addr := base.ContainerdAddress() - client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) - assert.NilError(base.T, err) - ctx := context.Background() - - // get container id by container name. - var cid string - var args []string - args = append(args, containerName) - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") } - cid = found.Container.ID() return nil - }, + }), + ) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", "infinity") + helpers.Ensure("update", "--cpuset-cpus", "0-1", 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 { + return helpers.Command("inspect", "--format", "{{.Id}}", data.Identifier()) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + cid := strings.TrimSpace(stdout) + addr := defaults.DefaultAddress + if rootlessutil.IsRootless() { + stateDir, err := rootlessutil.RootlessKitStateDir() + assert.NilError(t, err) + childPid, err := rootlessutil.RootlessKitChildPid(stateDir) + assert.NilError(t, err) + addr = filepath.Join("/proc", fmt.Sprintf("%d", childPid), "root", defaults.DefaultAddress) + } + client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) + assert.NilError(t, err) + defer client.Close() + ctx := context.Background() + cntr, err := client.LoadContainer(ctx, cid) + assert.NilError(t, err) + spec, err := cntr.Spec(ctx) + assert.NilError(t, err) + assert.Assert(t, spec.Linux.Resources.Pids == nil) + }, + } } - err = walker.WalkAll(ctx, args, true) - assert.NilError(base.T, err) - - container, err := client.LoadContainer(ctx, cid) - assert.NilError(base.T, err) - spec, err := container.Spec(ctx) - assert.NilError(base.T, err) - assert.Equal(t, spec.Linux.Resources.Pids == nil, true) + testCase.Run(t) } func TestRunDevice(t *testing.T) { @@ -387,98 +497,135 @@ func TestParseDevice(t *testing.T) { } func TestRunCgroupConf(t *testing.T) { - t.Parallel() - if cgroups.Mode() != cgroups.Unified { - t.Skip("test requires cgroup v2") - } - testutil.DockerIncompatible(t) // Docker lacks --cgroup-conf - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") + testCase := nerdtest.Setup() + testCase.Require = require.All( + // Docker lacks --cgroup-conf + require.Not(nerdtest.Docker), + nerdtest.CGroupV2, + nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") + } + if !info.MemoryLimit { + return fmt.Errorf("test requires MemoryLimit") + } + return nil + }), + ) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--cgroup-conf", "memory.high=33554432", "-w", "/sys/fs/cgroup", testutil.AlpineImage, + "cat", "memory.high") } - if !info.MemoryLimit { - t.Skip("test requires MemoryLimit") + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals("33554432\n"), + } } - base.Cmd("run", "--rm", "--cgroup-conf", "memory.high=33554432", "-w", "/sys/fs/cgroup", testutil.AlpineImage, - "cat", "memory.high").AssertOutExactly("33554432\n") + testCase.Run(t) } func TestRunCgroupParent(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") - } - - containerName := testutil.Identifier(t) - t.Logf("Using %q cgroup driver", info.CgroupDriver) - - parent := "/foobarbaz" - if info.CgroupDriver == "systemd" { - // Path separators aren't allowed in systemd path. runc - // explicitly checks for this. - // https://github.com/opencontainers/runc/blob/016a0d29d1750180b2a619fc70d6fe0d80111be0/libcontainer/cgroups/systemd/common.go#L65-L68 - parent = "foobarbaz.slice" + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") + } + return nil + }) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cgroupDriver := strings.TrimSpace(helpers.Capture("info", "--format", "{{.CgroupDriver}}")) + parent := "/foobarbaz" + if cgroupDriver == "systemd" { + // Path separators aren't allowed in systemd path. runc + // explicitly checks for this. + // https://github.com/opencontainers/runc/blob/016a0d29d1750180b2a619fc70d6fe0d80111be0/libcontainer/cgroups/systemd/common.go#L65-L68 + parent = "foobarbaz.slice" + } + data.Labels().Set("parent", parent) + data.Labels().Set("cgroupDriver", cgroupDriver) + // cgroup2 without host cgroup ns will just output 0::/ which doesn't help much to verify + // we got our expected path. This approach should work for both cgroup1 and 2, there will + // just be many more entries for cgroup1 as there'll be an entry per controller. + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--cgroupns=host", "--cgroup-parent", parent, + testutil.AlpineImage, "sleep", "infinity") } - - tearDown := func() { - base.Cmd("rm", "-f", containerName).Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - - tearDown() - t.Cleanup(tearDown) - - // cgroup2 without host cgroup ns will just output 0::/ which doesn't help much to verify - // we got our expected path. This approach should work for both cgroup1 and 2, there will - // just be many more entries for cgroup1 as there'll be an entry per controller. - base.Cmd( - "run", - "-d", - "--name", - containerName, - "--cgroupns=host", - "--cgroup-parent", parent, - testutil.AlpineImage, - "sleep", - "infinity", - ).AssertOK() - - id := base.InspectContainer(containerName).ID - expected := filepath.Join(parent, id) - if info.CgroupDriver == "systemd" { - expected = filepath.Join(parent, fmt.Sprintf("nerdctl-%s", id)) - if nerdtest.IsDocker() { - expected = filepath.Join(parent, fmt.Sprintf("docker-%s", id)) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Identifier(), "cat", "/proc/self/cgroup") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + parent := data.Labels().Get("parent") + cgroupDriver := data.Labels().Get("cgroupDriver") + id := strings.TrimSpace(helpers.Capture("inspect", "--format", "{{.Id}}", data.Identifier())) + expected := filepath.Join(parent, id) + if cgroupDriver == "systemd" { + expected = filepath.Join(parent, fmt.Sprintf("nerdctl-%s", id)) + if nerdtest.IsDocker() { + expected = filepath.Join(parent, fmt.Sprintf("docker-%s", id)) + } + } + return &test.Expected{ + Output: expect.Contains(expected), } } - base.Cmd("exec", containerName, "cat", "/proc/self/cgroup").AssertOutContains(expected) + testCase.Run(t) } func TestRunBlkioWeightCgroupV2(t *testing.T) { - t.Parallel() - if cgroups.Mode() != cgroups.Unified { - t.Skip("test requires cgroup v2") + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.CGroupV2, + nerdtest.Info(func(info dockercompat.Info) error { + if info.CgroupDriver == "none" || info.CgroupDriver == "" { + return fmt.Errorf("test requires cgroup driver") + } + return nil + }), + ) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + if _, err := os.Stat("/sys/module/bfq"); err != nil { + helpers.T().Skip(fmt.Sprintf("test requires \"bfq\" module to be loaded: %v", err)) + } + // when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight + helpers.Ensure("run", "--name", data.Identifier(), "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", nerdtest.Infinity) + data.Labels().Set("container", data.Identifier()) } - if _, err := os.Stat("/sys/module/bfq"); err != nil { - t.Skipf("test requires \"bfq\" module to be loaded: %v", err) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - base := testutil.NewBase(t) - info := base.Info() - switch info.CgroupDriver { - case "none", "": - t.Skip("test requires cgroup driver") + testCase.SubTests = []*test.Case{ + { + Description: "initial weight", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container"), "cat", "io.bfq.weight") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals("default 300\n"), + } + }, + }, + { + Description: "update weight", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("update", data.Labels().Get("container"), "--blkio-weight", "400") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container"), "cat", "io.bfq.weight") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals("default 400\n"), + } + }, + }, } - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() - // when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight - base.Cmd("run", "--name", containerName, "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK() - base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 300\n") - base.Cmd("update", containerName, "--blkio-weight", "400").AssertOK() - base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 400\n") + testCase.Run(t) } func TestRunBlkioSettingCgroupV2(t *testing.T) { From bd9c6e24aa4bcc04f6259e0edeef0ab4dfe1302d Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Mon, 20 Jul 2026 09:52:45 +0100 Subject: [PATCH 705/868] test: convert TestRunCustomRootfs to Tigron framework Signed-off-by: Daniel Benjamin --- .../container/container_run_linux_test.go | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index ff0cc0887f4..cc5c381d657 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -47,7 +47,7 @@ import ( ) func TestRunCustomRootfs(t *testing.T) { - testutil.DockerIncompatible(t) + testCase := nerdtest.Setup() // FIXME: root issue is undiagnosed and this is very likely a containerd bug // It appears that in certain conditions, the proxy content store info method will fail on the layer of the image // Search for func (pcs *proxyContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) { @@ -57,27 +57,46 @@ func TestRunCustomRootfs(t *testing.T) { // - this test is not parallelized - but the fact that namespacing it solves the problem suggest that something // happening in the default namespace BEFORE this test is run is SOMETIMES setting conditions that will make this fail // Possible suspects would be concurrent pulls somehow effing things up w. namespaces. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - rootfs := prepareCustomRootfs(base, testutil.AlpineImage) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - defer os.RemoveAll(rootfs) - base.Cmd("run", "--rm", "--rootfs", rootfs, "/bin/cat", "/proc/self/environ").AssertOutContains("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") - base.Cmd("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", rootfs, "echo", "foo").AssertOutExactly("echo foo\n") + testCase.Require = require.Not(nerdtest.Docker) + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Write(nerdtest.Namespace, test.ConfigValue(data.Identifier())) + rootfs := prepareCustomRootfs(data, helpers, testutil.AlpineImage) + data.Labels().Set("rootfs", rootfs) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("namespace", "remove", data.Identifier()) + if rootfs := data.Labels().Get("rootfs"); rootfs != "" { + os.RemoveAll(rootfs) + } + } + testCase.SubTests = []*test.Case{ + { + Description: "cat environ shows PATH", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--rootfs", data.Labels().Get("rootfs"), "/bin/cat", "/proc/self/environ") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")), + }, + { + Description: "echo with entrypoint", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", data.Labels().Get("rootfs"), "echo", "foo") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("echo foo\n")), + }, + } + testCase.Run(t) } -func prepareCustomRootfs(base *testutil.Base, imageName string) string { - base.Cmd("pull", "--quiet", imageName).AssertOK() - tmpDir, err := os.MkdirTemp(base.T.TempDir(), "test-save") - assert.NilError(base.T, err) - defer os.RemoveAll(tmpDir) +func prepareCustomRootfs(data test.Data, h test.Helpers, imageName string) string { + h.Ensure("pull", "--quiet", imageName) + tmpDir := data.Temp().Dir("test-save") archiveTarPath := filepath.Join(tmpDir, "a.tar") - base.Cmd("save", "-o", archiveTarPath, imageName).AssertOK() - rootfs, err := os.MkdirTemp(base.T.TempDir(), "rootfs") - assert.NilError(base.T, err) - err = helpers.ExtractDockerArchive(archiveTarPath, rootfs) - assert.NilError(base.T, err) + h.Ensure("save", "-o", archiveTarPath, imageName) + rootfs := data.Temp().Dir("rootfs") + err := helpers.ExtractDockerArchive(archiveTarPath, rootfs) + assert.NilError(h.T(), err) return rootfs } From 928ca302c4cfea7a6023e8d412496871c4c23e4f Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Sun, 19 Jul 2026 23:42:26 +0530 Subject: [PATCH 706/868] Refactor container_kill_linux_test.go to use Tigro Signed-off-by: Mujib Ahasan Co-authored-by: microness --- .../container/container_kill_linux_test.go | 150 +++++++++++++----- pkg/testutil/iptables/iptables_linux.go | 26 +++ 2 files changed, 133 insertions(+), 43 deletions(-) diff --git a/cmd/nerdctl/container/container_kill_linux_test.go b/cmd/nerdctl/container/container_kill_linux_test.go index 6372d80ee33..6827cdce8d7 100644 --- a/cmd/nerdctl/container/container_kill_linux_test.go +++ b/cmd/nerdctl/container/container_kill_linux_test.go @@ -18,63 +18,127 @@ package container import ( "fmt" + "strconv" "strings" "testing" "github.com/coreos/go-iptables/iptables" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "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/testutil" iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) -// TestKillCleanupForwards runs a container that exposes a port and then kill it. -// The test checks that the kill command effectively clean up -// the iptables forwards creted from the run. +// TestKillCleanupForwards runs a container that exposes a port and then kills it. +// The test checks that the kill command effectively cleans up +// the iptables forwards created from the run. func TestKillCleanupForwards(t *testing.T) { - const ( - hostPort = 9999 - testContainerName = "ngx" - ) - base := testutil.NewBase(t) - defer func() { - base.Cmd("rm", "-f", testContainerName).Run() - }() - - // skip if rootless - if rootlessutil.IsRootless() { - t.Skip("pkg/testutil/iptables does not support rootless") + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Rootful // pkg/testutil/iptables does not support rootless + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + port, err := portlock.Acquire(0) + assert.NilError(helpers.T(), err) + data.Labels().Set("hostPort", strconv.Itoa(port)) + + containerID := helpers.Capture("run", "-d", + "--restart=no", + "--name", data.Identifier(), + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + testutil.NginxAlpineImage) + containerID = strings.TrimSuffix(containerID, "\n") + + containerIP := helpers.Capture("inspect", + "-f", + "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", + data.Identifier()) + containerIP = strings.ReplaceAll(containerIP, "'", "") + containerIP = strings.TrimSuffix(containerIP, "\n") + + // define iptables chain name depending on the target (docker/nerdctl) + ipt, err := iptables.New() + assert.NilError(helpers.T(), err) + + var chain string + if nerdtest.IsDocker() { + chain = "DOCKER" + } else { + redirectChain := "CNI-HOSTPORT-DNAT" + chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) + } + + data.Labels().Set("chain", chain) + data.Labels().Set("containerIP", containerIP) + data.Labels().Set("containerName", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) + if portStr := data.Labels().Get("hostPort"); portStr != "" { + port, err := strconv.Atoi(portStr) + if err == nil { + _ = portlock.Release(port) + } + } } - ipt, err := iptables.New() - assert.NilError(t, err) - - containerID := base.Cmd("run", "-d", - "--restart=no", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).Run().Stdout() - containerID = strings.TrimSuffix(containerID, "\n") - - containerIP := base.Cmd("inspect", - "-f", - "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", - testContainerName).Run().Stdout() - containerIP = strings.ReplaceAll(containerIP, "'", "") - containerIP = strings.TrimSuffix(containerIP, "\n") - - // define iptables chain name depending on the target (docker/nerdctl) - var chain string - if nerdtest.IsDocker() { - chain = "DOCKER" - } else { - redirectChain := "CNI-HOSTPORT-DNAT" - chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) + testCase.SubTests = []*test.Case{ + { + Description: "iptables forwarding rule should exist before container is killed", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("iptables", "-t", "nat", "-S", data.Labels().Get("chain")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + rules := strings.Split(stdout, "\n") + port, _ := strconv.Atoi(data.Labels().Get("hostPort")) + found, err := iptablesutil.ForwardExistsFromRules(rules, data.Labels().Get("containerIP"), port) + assert.NilError(t, err) + assert.Assert(t, found, "iptables forwarding rule should exist before kill") + }, + } + }, + }, + { + Description: "kill container", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("kill", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "iptables forwarding rule should be removed after container is killed", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("iptables", "-t", "nat", "-S", data.Labels().Get("chain")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + rules := strings.Split(stdout, "\n") + port, _ := strconv.Atoi(data.Labels().Get("hostPort")) + found, err := iptablesutil.ForwardExistsFromRules(rules, data.Labels().Get("containerIP"), port) + if err != nil { + // chain may have been removed entirely after kill — that's fine + return + } + assert.Assert(t, !found, "iptables forwarding rule should be removed after kill") + }, + } + }, + }, } - assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true) - base.Cmd("kill", testContainerName).AssertOK() - assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false) + testCase.Run(t) } diff --git a/pkg/testutil/iptables/iptables_linux.go b/pkg/testutil/iptables/iptables_linux.go index c0dcea4d42d..6b2dcb7a6fd 100644 --- a/pkg/testutil/iptables/iptables_linux.go +++ b/pkg/testutil/iptables/iptables_linux.go @@ -55,6 +55,32 @@ func ForwardExists(t *testing.T, ipt *iptables.IPTables, chain, containerIP stri return found } +// ForwardExistsFromRules checks whether an iptables forwarding rule exists +// in the provided list of rule strings. Unlike ForwardExists, this function +// does not query iptables directly, making it suitable for use with +// pre-captured command output (e.g., from helpers.Custom("iptables", ...)). +func ForwardExistsFromRules(rules []string, containerIP string, port int) (bool, error) { + if len(rules) < 1 { + return false, fmt.Errorf("not enough rules: %d", len(rules)) + } + + found := false + matchRule := `--dport ` + fmt.Sprintf("%d", port) + ` .+ --to-destination ` + containerIP + + for _, rule := range rules { + foundInRule, err := regexp.MatchString(matchRule, rule) + if err != nil { + return false, fmt.Errorf("error in match string: %q", err) + } + + if foundInRule { + found = foundInRule + } + } + + return found, nil +} + // GetRedirectedChain returns the chain where the traffic is being redirected. // This is how libcni manage its port maps. // Suppose you have the following rule: From 62733037b8694a51e3b1db4770af28730d1c7933 Mon Sep 17 00:00:00 2001 From: Mujib Ahasan Date: Sun, 12 Jul 2026 21:26:59 +0530 Subject: [PATCH 707/868] Refactor container_run_network_linux_test.go to use Tigron Signed-off-by: Mujib Ahasan --- .../container_run_network_linux_test.go | 1239 +++++++++-------- 1 file changed, 687 insertions(+), 552 deletions(-) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 26ec94d7089..6584fb1784e 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -32,7 +32,6 @@ import ( "github.com/opencontainers/go-digest" "github.com/vishvananda/netlink" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" "github.com/containerd/containerd/v2/defaults" "github.com/containerd/containerd/v2/pkg/netns" @@ -64,344 +63,320 @@ func extractHostPort(portMapping string, port string) (string, error) { return "", fmt.Errorf("could not extract host port from port mapping: %s", portMapping) } -func valuesOfMapStringString(m map[string]string) map[string]struct{} { - res := make(map[string]struct{}) - for _, v := range m { - res[v] = struct{}{} - } - return res -} - // TestRunInternetConnectivity tests Internet connectivity with `apk update` func TestRunInternetConnectivity(t *testing.T) { - base := testutil.NewBase(t) - customNet := testutil.Identifier(t) - base.Cmd("network", "create", customNet).AssertOK() - defer base.Cmd("network", "rm", customNet).Run() + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("customnet")) + netw := nerdtest.InspectNetwork(helpers, data.Identifier("customnet")) + data.Labels().Set("customNet", data.Identifier("customnet")) + data.Labels().Set("customNetID", netw.ID) + } - type testCase struct { - args []string + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("customnet")) } - customNetID := base.InspectNetwork(customNet).ID - testCases := []testCase{ + + testCase.SubTests = []*test.Case{ { - args: []string{"--net", "bridge"}, + Description: "--net bridge", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", "bridge", testutil.AlpineImage, "apk", "update") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("OK")), }, { - args: []string{"--net", customNet}, + Description: "--net customNet", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("customNet"), testutil.AlpineImage, "apk", "update") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("OK")), }, { - args: []string{"--net", customNetID}, + Description: "--net customNetID (full)", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("customNetID"), testutil.AlpineImage, "apk", "update") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("OK")), }, { - args: []string{"--net", customNetID[:12]}, + Description: "--net customNetID (short)", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("customNetID")[:12], testutil.AlpineImage, "apk", "update") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("OK")), }, { - args: []string{"--net", "host"}, + Description: "--net host", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", "host", testutil.AlpineImage, "apk", "update") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("OK")), }, } - for _, tc := range testCases { - tc := tc // IMPORTANT - name := "default" - if len(tc.args) > 0 { - name = strings.Join(tc.args, "_") - } - t.Run(name, func(t *testing.T) { - args := []string{"run", "--rm"} - args = append(args, tc.args...) - args = append(args, testutil.AlpineImage, "apk", "update") - cmd := base.Cmd(args...) - cmd.AssertOutContains("OK") - }) - } + + testCase.Run(t) } // TestRunHostLookup tests hostname lookup func TestRunHostLookup(t *testing.T) { - base := testutil.NewBase(t) - // key: container name, val: network name - m := map[string]string{ - "c0-in-n0": "n0", - "c1-in-n0": "n0", - "c2-in-n1": "n1", - "c3-in-bridge": "bridge", - } - customNets := valuesOfMapStringString(m) - defer func() { - for name := range m { - base.Cmd("rm", "-f", name).Run() + testCase := nerdtest.Setup() + + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // key: container name suffix, val: network name + m := map[string]string{ + "c0-in-n0": data.Identifier("n0"), + "c1-in-n0": data.Identifier("n0"), + "c2-in-n1": data.Identifier("n1"), + "c3-in-bridge": "bridge", } - for netName := range customNets { - if netName == "bridge" { - continue - } - base.Cmd("network", "rm", netName).Run() + + // Create networks + helpers.Ensure("network", "create", data.Identifier("n0")) + helpers.Ensure("network", "create", data.Identifier("n1")) + + // Store network and container names in labels + data.Labels().Set("net-n0", data.Identifier("n0")) + data.Labels().Set("net-n1", data.Identifier("n1")) + + // Create nginx containers + for name, netName := range m { + containerName := data.Identifier(name) + data.Labels().Set(name, containerName) + helpers.Ensure("run", "-d", "--name", containerName, "--hostname", name+"-foobar", "--net", netName, testutil.NginxAlpineImage) } - }() + } - // Create networks - for netName := range customNets { - if netName == "bridge" { - continue + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + for _, name := range []string{"c0-in-n0", "c1-in-n0", "c2-in-n1", "c3-in-bridge"} { + helpers.Anyhow("rm", "-f", data.Identifier(name)) } - base.Cmd("network", "create", netName).AssertOK() + helpers.Anyhow("network", "rm", data.Identifier("n0")) + helpers.Anyhow("network", "rm", data.Identifier("n1")) + } + + type wgetCase struct { + srcSuffix string + buildTarget func(data test.Data) string + desc string + shouldSucceed bool } - // Create nginx containers - for name, netName := range m { - cmd := base.Cmd("run", - "-d", - "--name", name, - "--hostname", name+"-foobar", - "--net", netName, - testutil.NginxAlpineImage, - ) - t.Logf("creating host lookup testing container with command: %q", strings.Join(cmd.Command, " ")) - cmd.AssertOK() + wgetCases := []wgetCase{ + {"c0-in-n0", func(d test.Data) string { return d.Labels().Get("c1-in-n0") }, "container name", true}, + {"c0-in-n0", func(d test.Data) string { return d.Labels().Get("c1-in-n0") + "." + d.Labels().Get("net-n0") }, "container FQDN", true}, + {"c0-in-n0", func(d test.Data) string { return "c1-in-n0-foobar" }, "hostname", true}, + {"c0-in-n0", func(d test.Data) string { return "c1-in-n0-foobar." + d.Labels().Get("net-n0") }, "hostname FQDN", true}, + {"c0-in-n0", func(d test.Data) string { return d.Labels().Get("c2-in-n1") }, "cross-network name", false}, + {"c0-in-n0", func(d test.Data) string { return d.Labels().Get("c2-in-n1") + "." + d.Labels().Get("net-n1") }, "cross-network FQDN", false}, + {"c0-in-n0", func(d test.Data) string { return d.Labels().Get("c3-in-bridge") }, "bridge container", false}, + {"c1-in-n0", func(d test.Data) string { return d.Labels().Get("c0-in-n0") }, "reverse container name", true}, + {"c1-in-n0", func(d test.Data) string { return d.Labels().Get("c0-in-n0") + "." + d.Labels().Get("net-n0") }, "reverse FQDN", true}, + {"c1-in-n0", func(d test.Data) string { return "c0-in-n0-foobar" }, "reverse hostname", true}, + {"c1-in-n0", func(d test.Data) string { return "c0-in-n0-foobar." + d.Labels().Get("net-n0") }, "reverse hostname FQDN", true}, } - testWget := func(srcContainer, targetHostname string, expected bool) { - t.Logf("resolving %q in container %q (should success: %+v)", targetHostname, srcContainer, expected) - cmd := base.Cmd("exec", srcContainer, "wget", "-qO-", "http://"+targetHostname) - if expected { - cmd.AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) + testCase.SubTests = make([]*test.Case, 0, len(wgetCases)) + for _, wc := range wgetCases { + wc := wc + desc := fmt.Sprintf("%s from %s (expect %v)", wc.desc, wc.srcSuffix, wc.shouldSucceed) + var expected test.Manager + if wc.shouldSucceed { + expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)) } else { - cmd.AssertFail() + expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) } + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: desc, + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + target := wc.buildTarget(data) + return helpers.Command("exec", data.Labels().Get(wc.srcSuffix), "wget", "-qO-", "http://"+target) + }, + Expected: expected, + }) } - // Tests begin - testWget("c0-in-n0", "c1-in-n0", true) - testWget("c0-in-n0", "c1-in-n0.n0", true) - testWget("c0-in-n0", "c1-in-n0-foobar", true) - testWget("c0-in-n0", "c1-in-n0-foobar.n0", true) - testWget("c0-in-n0", "c2-in-n1", false) - testWget("c0-in-n0", "c2-in-n1.n1", false) - testWget("c0-in-n0", "c3-in-bridge", false) - testWget("c1-in-n0", "c0-in-n0", true) - testWget("c1-in-n0", "c0-in-n0.n0", true) - testWget("c1-in-n0", "c0-in-n0-foobar", true) - testWget("c1-in-n0", "c0-in-n0-foobar.n0", true) + testCase.Run(t) } func TestRunPortWithNoHostPort(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Auto port assign is not supported rootless mode yet") - } + testCase := nerdtest.Setup() - type testCase struct { - containerPort string - runShouldSuccess bool + testCase.Require = nerdtest.Rootful // Auto port assign is not supported rootless mode yet + + type portTestCase struct { + containerPort string } - testCases := []testCase{ - { - containerPort: "80", - runShouldSuccess: true, - }, - { - containerPort: "80-81", - runShouldSuccess: true, - }, - { - containerPort: "80-81/tcp", - runShouldSuccess: true, - }, + testCases := []portTestCase{ + {containerPort: "80"}, + {containerPort: "80-81"}, + {containerPort: "80-81/tcp"}, } - tID := testutil.Identifier(t) + for i, tc := range testCases { - i := i tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName := fmt.Sprintf("%s-%d", tID, i) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName).Run() - pFlag := tc.containerPort - cmd := base.Cmd("run", "-d", - "--name", testContainerName, - "-p", pFlag, - testutil.NginxAlpineImage) - var result *icmd.Result - stdoutContent := "" - if tc.runShouldSuccess { - cmd.AssertOK() - } else { - cmd.AssertFail() - return - } - portCmd := base.Cmd("port", testContainerName) - portCmd.Base.T.Helper() - result = portCmd.Run() - stdoutContent = result.Stdout() + result.Stderr() - assert.Assert(cmd.Base.T, result.ExitCode == 0, stdoutContent) - regexExpression := regexp.MustCompile(`80\/tcp.*?->.*?0.0.0.0:(?P\d{1,5}).*?`) - match := regexExpression.FindStringSubmatch(stdoutContent) - paramsMap := make(map[string]string) - for i, name := range regexExpression.SubexpNames() { - if i > 0 && i <= len(match) { - paramsMap[name] = match[i] + i := i + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: fmt.Sprintf("port %s", tc.containerPort), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + containerName := data.Identifier(fmt.Sprintf("container-%d", i)) + data.Labels().Set("containerName", containerName) + helpers.Ensure("run", "-d", "--name", containerName, "-p", tc.containerPort, testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier(fmt.Sprintf("container-%d", i))) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("port", data.Labels().Get("containerName")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + regexExpression := regexp.MustCompile(`80\/tcp.*?->.*?0.0.0.0:(?P\d{1,5}).*?`) + match := regexExpression.FindStringSubmatch(stdout) + paramsMap := make(map[string]string) + for j, name := range regexExpression.SubexpNames() { + if j > 0 && j <= len(match) { + paramsMap[name] = match[j] + } + } + assert.Assert(t, paramsMap["portNumber"] != "", "could not extract port number from: %s", stdout) + connectURL := fmt.Sprintf("http://%s:%s", "127.0.0.1", paramsMap["portNumber"]) + resp, err := nettestutil.HTTPGet(connectURL, 5, false) + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) + }, } - } - if _, ok := paramsMap["portNumber"]; !ok { - t.Fail() - return - } - connectURL := fmt.Sprintf("http://%s:%s", "127.0.0.1", paramsMap["portNumber"]) - resp, err := nettestutil.HTTPGet(connectURL, 5, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) + }, }) } + testCase.Run(t) } func TestUniqueHostPortAssignement(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Auto port assign is not supported rootless mode yet") - } + testCase := nerdtest.Setup() - type testCase struct { - containerPort string - runShouldSuccess bool - } + testCase.Require = nerdtest.Rootful // Auto port assign is not supported rootless mode yet - testCases := []testCase{ - { - containerPort: "80", - runShouldSuccess: true, - }, - { - containerPort: "80-81", - runShouldSuccess: true, - }, - { - containerPort: "80-81/tcp", - runShouldSuccess: true, - }, + type portTestCase struct { + containerPort string + } + testCases := []portTestCase{ + {containerPort: "80"}, + {containerPort: "80-81"}, + {containerPort: "80-81/tcp"}, } - - tID := testutil.Identifier(t) for i, tc := range testCases { - i := i tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) - testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName1, testContainerName2).Run() - - pFlag := tc.containerPort - cmd1 := base.Cmd("run", "-d", - "--name", testContainerName1, "-p", - pFlag, - testutil.NginxAlpineImage) - - cmd2 := base.Cmd("run", "-d", - "--name", testContainerName2, "-p", - pFlag, - testutil.NginxAlpineImage) - var result *icmd.Result - stdoutContent := "" - if tc.runShouldSuccess { - cmd1.AssertOK() - cmd2.AssertOK() - } else { - cmd1.AssertFail() - cmd2.AssertFail() - return - } - portCmd1 := base.Cmd("port", testContainerName1) - portCmd2 := base.Cmd("port", testContainerName2) - portCmd1.Base.T.Helper() - portCmd2.Base.T.Helper() - result = portCmd1.Run() - stdoutContent = result.Stdout() + result.Stderr() - assert.Assert(t, result.ExitCode == 0, stdoutContent) - port1, err := extractHostPort(stdoutContent, "80") - assert.NilError(t, err) - result = portCmd2.Run() - stdoutContent = result.Stdout() + result.Stderr() - assert.Assert(t, result.ExitCode == 0, stdoutContent) - port2, err := extractHostPort(stdoutContent, "80") - assert.NilError(t, err) - assert.Assert(t, port1 != port2, "Host ports are not unique") - - // Make HTTP GET request to container 1 - connectURL1 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port1) - resp1, err := nettestutil.HTTPGet(connectURL1, 5, false) - assert.NilError(t, err) - respBody1, err := io.ReadAll(resp1.Body) - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(respBody1), testutil.NginxAlpineIndexHTMLSnippet)) - - // Make HTTP GET request to container 2 - connectURL2 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port2) - resp2, err := nettestutil.HTTPGet(connectURL2, 5, false) - assert.NilError(t, err) - respBody2, err := io.ReadAll(resp2.Body) - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(respBody2), testutil.NginxAlpineIndexHTMLSnippet)) + i := i + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: fmt.Sprintf("port %s", tc.containerPort), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + name1 := data.Identifier(fmt.Sprintf("c%d-1", i)) + name2 := data.Identifier(fmt.Sprintf("c%d-2", i)) + data.Labels().Set("container1", name1) + data.Labels().Set("container2", name2) + helpers.Ensure("run", "-d", "--name", name1, "-p", tc.containerPort, testutil.NginxAlpineImage) + helpers.Ensure("run", "-d", "--name", name2, "-p", tc.containerPort, testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier(fmt.Sprintf("c%d-1", i)), data.Identifier(fmt.Sprintf("c%d-2", i))) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("port", data.Labels().Get("container1")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + port1, err := extractHostPort(stdout, "80") + assert.NilError(t, err) + + // Get port for second container + port2Stdout := helpers.Capture("port", data.Labels().Get("container2")) + port2, err := extractHostPort(port2Stdout, "80") + assert.NilError(t, err) + + assert.Assert(t, port1 != port2, "Host ports are not unique") + + // Make HTTP GET request to container 1 + connectURL1 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port1) + resp1, err := nettestutil.HTTPGet(connectURL1, 5, false) + assert.NilError(t, err) + respBody1, err := io.ReadAll(resp1.Body) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(respBody1), testutil.NginxAlpineIndexHTMLSnippet)) + + // Make HTTP GET request to container 2 + connectURL2 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port2) + resp2, err := nettestutil.HTTPGet(connectURL2, 5, false) + assert.NilError(t, err) + respBody2, err := io.ReadAll(resp2.Body) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(respBody2), testutil.NginxAlpineIndexHTMLSnippet)) + }, + } + }, }) } + + testCase.Run(t) } func TestHostPortAlreadyInUse(t *testing.T) { - testCases := []struct { + testCase := nerdtest.Setup() + + testCase.NoParallel = true + + type portConflictCase struct { hostPort string containerPort string - }{ - { - hostPort: "5000", - containerPort: "80/tcp", - }, - { - hostPort: "5000", - containerPort: "80/tcp", - }, - { - hostPort: "5000", - containerPort: "80/udp", - }, - { - hostPort: "5000", - containerPort: "80/sctp", - }, } - - tID := testutil.Identifier(t) + testCases := []portConflictCase{ + {hostPort: "5000", containerPort: "80/tcp"}, + {hostPort: "5000", containerPort: "80/tcp"}, + {hostPort: "5000", containerPort: "80/udp"}, + {hostPort: "5000", containerPort: "80/sctp"}, + } for i, tc := range testCases { tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - if strings.Contains(tc.containerPort, "sctp") && rootlessutil.IsRootless() { - t.Skip("sctp is not supported in rootless mode") - } - testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) - testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) - base := testutil.NewBase(t) - t.Cleanup(func() { - base.Cmd("rm", "-f", testContainerName1, testContainerName2).AssertOK() - }) - pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort) - cmd1 := base.Cmd("run", "-d", - "--name", testContainerName1, "-p", - pFlag, - testutil.NginxAlpineImage) - - cmd2 := base.Cmd("run", "-d", - "--name", testContainerName2, "-p", - pFlag, - testutil.NginxAlpineImage) - - cmd1.AssertOK() - cmd2.AssertFail() - }) + i := i + subTest := &test.Case{ + Description: fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + name1 := data.Identifier(fmt.Sprintf("c%d-1", i)) + data.Labels().Set("container1", name1) + pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort) + helpers.Ensure("run", "-d", "--name", name1, "-p", pFlag, testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier(fmt.Sprintf("c%d-1", i)), data.Identifier(fmt.Sprintf("c%d-2", i))) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + name2 := data.Identifier(fmt.Sprintf("c%d-2", i)) + pFlag := fmt.Sprintf("%s:%s", tc.hostPort, tc.containerPort) + return helpers.Command("run", "-d", "--name", name2, "-p", pFlag, testutil.NginxAlpineImage) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + } + if strings.Contains(tc.containerPort, "sctp") { + subTest.Require = nerdtest.Rootful + } + testCase.SubTests = append(testCase.SubTests, subTest) } + + testCase.Run(t) } func TestRunPort(t *testing.T) { @@ -453,116 +428,167 @@ func TestRunWithManyPortsThenCleanUp(t *testing.T) { } func TestRunContainerWithStaticIP(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Static IP assignment is not supported rootless mode yet.") + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.Rootful // Static IP assignment is not supported rootless mode yet + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("networkName", data.Identifier("network")) + helpers.Ensure("network", "create", data.Identifier("network"), "--subnet", "172.0.0.0/16") } - networkName := "test-network" - networkSubnet := "172.0.0.0/16" - base := testutil.NewBase(t) - cmd := base.Cmd("network", "create", networkName, "--subnet", networkSubnet) - cmd.AssertOK() - defer base.Cmd("network", "rm", networkName).Run() - testCases := []struct { - ip string - shouldSuccess bool - useNetwork bool - checkTheIPAddress bool - }{ + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("network")) + } + + // XXX see https://github.com/containerd/nerdctl/issues/3101 + // docker 24 silently ignored the ip - now, docker 26 is erroring out - furthermore, this ip only makes sense + // in the context of nerdctl bridge network, so, this test needs rewritting either way + testCase.SubTests = []*test.Case{ { - ip: "172.0.0.2", - shouldSuccess: true, - useNetwork: true, - checkTheIPAddress: true, + Description: "static IP within subnet succeeds", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set("containerName", data.Identifier("static-ip")) + helpers.Ensure("run", "-d", "--name", data.Identifier("static-ip"), "--network", data.Labels().Get("networkName"), "--ip", "172.0.0.2", testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("static-ip")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Labels().Get("containerName"), + "--format", "{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("172.0.0.2")), }, { - ip: "192.0.0.2", - shouldSuccess: false, - useNetwork: true, - checkTheIPAddress: false, - }, - // XXX see https://github.com/containerd/nerdctl/issues/3101 - // docker 24 silently ignored the ip - now, docker 26 is erroring out - furthermore, this ip only makes sense - // in the context of nerdctl bridge network, so, this test needs rewritting either way - /* - { - ip: "10.4.0.2", - shouldSuccess: true, - useNetwork: false, - checkTheIPAddress: false, + Description: "static IP outside subnet fails", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("bad-ip")) }, - */ - } - tID := testutil.Identifier(t) - for i, tc := range testCases { - i := i - tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName := fmt.Sprintf("%s-%d", tID, i) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName).Run() - args := []string{ - "run", "-d", "--name", testContainerName, - } - if tc.useNetwork { - args = append(args, []string{"--network", networkName}...) - } - args = append(args, []string{"--ip", tc.ip, testutil.NginxAlpineImage}...) - cmd := base.Cmd(args...) - if !tc.shouldSuccess { - cmd.AssertFail() - return - } - cmd.AssertOK() - - if tc.checkTheIPAddress { - inspectCmd := base.Cmd("inspect", testContainerName, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") - result := inspectCmd.Run() - stdoutContent := result.Stdout() + result.Stderr() - assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent) - if !strings.Contains(stdoutContent, tc.ip) { - t.Fail() - return - } - } - }) + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier("bad-ip"), + "--network", data.Labels().Get("networkName"), + "--ip", "192.0.0.2", testutil.NginxAlpineImage) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, } + + testCase.Run(t) } func TestRunDNS(t *testing.T) { - base := testutil.NewBase(t) - - base.Cmd("run", "--rm", "--dns", "8.8.8.8", testutil.CommonImage, - "cat", "/etc/resolv.conf").AssertOutContains("nameserver 8.8.8.8\n") - base.Cmd("run", "--rm", "--dns-search", "test", testutil.CommonImage, - "cat", "/etc/resolv.conf").AssertOutContains("search test\n") - base.Cmd("run", "--rm", "--dns-search", "test", "--dns-search", "test1", testutil.CommonImage, - "cat", "/etc/resolv.conf").AssertOutContains("search test test1\n") - base.Cmd("run", "--rm", "--dns-opt", "no-tld-query", "--dns-option", "attempts:10", testutil.CommonImage, - "cat", "/etc/resolv.conf").AssertOutContains("options no-tld-query attempts:10\n") - cmd := base.Cmd("run", "--rm", "--dns", "8.8.8.8", "--dns-search", "test", "--dns-option", "attempts:10", testutil.CommonImage, - "cat", "/etc/resolv.conf") - cmd.AssertOutContains("nameserver 8.8.8.8\n") - cmd.AssertOutContains("search test\n") - cmd.AssertOutContains("options attempts:10\n") + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "dns nameserver", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--dns", "8.8.8.8", testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("nameserver 8.8.8.8\n")), + }, + { + Description: "dns search single", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--dns-search", "test", testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("search test\n")), + }, + { + Description: "dns search multiple", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--dns-search", "test", "--dns-search", "test1", testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("search test test1\n")), + }, + { + Description: "dns options", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--dns-opt", "no-tld-query", "--dns-option", "attempts:10", testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("options no-tld-query attempts:10\n")), + }, + { + Description: "dns combined flags", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--dns", "8.8.8.8", "--dns-search", "test", "--dns-option", "attempts:10", testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("nameserver 8.8.8.8\n"), + expect.Contains("search test\n"), + expect.Contains("options attempts:10\n"), + )), + }, + } + + testCase.Run(t) } func TestRunNetworkHostHostname(t *testing.T) { - base := testutil.NewBase(t) - - hostname, err := os.Hostname() - assert.NilError(t, err) - hostname = hostname + "\n" - base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "hostname").AssertOutExactly(hostname) - base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly(hostname) - base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "hostname").AssertOutExactly("override\n") - base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly("override\n") + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostname, err := os.Hostname() + assert.NilError(helpers.T(), err) + data.Labels().Set("hostname", hostname) + } + + testCase.SubTests = []*test.Case{ + { + Description: "hostname command returns host hostname", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--network", "host", testutil.CommonImage, "hostname") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(data.Labels().Get("hostname") + "\n"), + } + }, + }, + { + Description: "HOSTNAME env returns host hostname", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Equals(data.Labels().Get("hostname") + "\n"), + } + }, + }, + { + Description: "hostname override with hostname command", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "hostname") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("override\n")), + }, + { + Description: "hostname override with HOSTNAME env", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("override\n")), + }, + } + + testCase.Run(t) } func TestRunNetworkHost2613(t *testing.T) { - base := testutil.NewBase(t) + nerdtest.Setup() - base.Cmd("run", "--rm", "--add-host", "foo:1.2.3.4", testutil.CommonImage, "getent", "hosts", "foo").AssertOutExactly("1.2.3.4 foo foo\n") + testCase := &test.Case{ + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--add-host", "foo:1.2.3.4", testutil.CommonImage, "getent", "hosts", "foo") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1.2.3.4 foo foo\n")), + } + + testCase.Run(t) } func TestSharedNetworkSetup(t *testing.T) { @@ -586,10 +612,7 @@ func TestSharedNetworkSetup(t *testing.T) { helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure( - "run", "-d", "--name", data.Identifier("container2"), - "--network=container:"+data.Labels().Get("container1"), - testutil.NginxAlpineImage) + helpers.Ensure("run", "-d", "--name", data.Identifier("container2"), "--network=container:"+data.Labels().Get("container1"), testutil.NginxAlpineImage) data.Labels().Set("container2", data.Identifier("container2")) nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) }, @@ -695,216 +718,328 @@ func TestSharedNetworkWithNone(t *testing.T) { } func TestRunContainerInExistingNetNS(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("Can't create new netns in rootless mode") + testCase := nerdtest.Setup() + + testCase.Require = require.All( + nerdtest.Rootful, + require.Not(nerdtest.Docker), + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + netNS, err := netns.NewNetNS(data.Temp().Dir("netns-dir") + "/netns") + assert.NilError(helpers.T(), err) + err = netNS.Do(func(netns ns.NetNS) error { + loopback, err := netlink.LinkByName("lo") + assert.NilError(helpers.T(), err) + err = netlink.LinkSetUp(loopback) + assert.NilError(helpers.T(), err) + return nil + }) + assert.NilError(helpers.T(), err) + data.Labels().Set("netNSPath", netNS.GetPath()) + + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + time.Sleep(3 * time.Second) } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - netNS, err := netns.NewNetNS(t.TempDir() + "/netns") - assert.NilError(t, err) - err = netNS.Do(func(netns ns.NetNS) error { - loopback, err := netlink.LinkByName("lo") - assert.NilError(t, err) - err = netlink.LinkSetUp(loopback) - assert.NilError(t, err) - return nil - }) - assert.NilError(t, err) - defer netNS.Remove() - - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--name", containerName, - "--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage).AssertOK() - base.EnsureContainerStarted(containerName) - time.Sleep(3 * time.Second) - - err = netNS.Do(func(netns ns.NetNS) error { - stdout, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output() - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet)) - return nil - }) - assert.NilError(t, err) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + if netNSPath := data.Labels().Get("netNSPath"); netNSPath != "" { + loadedNS := netns.LoadNetNS(netNSPath) + _ = loadedNS.Remove() + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", "--format", "{{.State.Running}}", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "true"), "container should be running") + + netNSPath := data.Labels().Get("netNSPath") + testNetNS, err := ns.GetNS(netNSPath) + assert.NilError(t, err) + err = testNetNS.Do(func(ns.NetNS) error { + curlOut, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output() + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(curlOut), testutil.NginxAlpineIndexHTMLSnippet)) + return nil + }) + assert.NilError(t, err) + }, + } + } + + testCase.Run(t) } func TestRunContainerWithMACAddress(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() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("bridge"), "--driver", "bridge") + helpers.Ensure("network", "create", data.Identifier("macvlan"), "--driver", "macvlan") + helpers.Ensure("network", "create", data.Identifier("ipvlan"), "--driver", "ipvlan") + + data.Labels().Set("networkBridge", data.Identifier("bridge")) + data.Labels().Set("networkMACvlan", data.Identifier("macvlan")) + data.Labels().Set("networkIPvlan", data.Identifier("ipvlan")) + + // Get the default MAC address of eth0 on the host network + cmd := helpers.Command("run", "--rm", "-i", "--network", "host", testutil.CommonImage) + cmd.Feed(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'")) + cmd.Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + data.Labels().Set("defaultMac", stdout) + }, + }) } - 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, ""}, // nothing - {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container" - {"bridge", false, passedMac}, - {networkBridge, false, passedMac}, - {networkMACvlan, false, passedMac}, - {networkIPvlan, true, "not support"}, + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("bridge")) + helpers.Anyhow("network", "rm", data.Identifier("macvlan")) + helpers.Anyhow("network", "rm", data.Identifier("ipvlan")) } - for i, test := range tests { - containerName := fmt.Sprintf("%s_%d", tID, i) - testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) - expect := test.Expect - network := test.Network - wantErr := test.WantErr - t.Run(testName, func(tt *testing.T) { - tt.Parallel() - - macAddress, err := nettestutil.GenerateMACAddress() - if err != nil { - t.Errorf("failed to generate MAC address: %s", err) - } - if expect == passedMac { - expect = macAddress - } + type macTestCase struct { + networkKey string // label key or literal network name + isLiteral bool // if true, use networkKey as-is; otherwise look up from labels + wantErr bool + expectKey string // "defaultMac", "passedMac", "", or a literal substring + } - res := base.Cmd("run", "--rm", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage). - CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() + macTestCases := []macTestCase{ + {networkKey: "host", isLiteral: true, wantErr: false, expectKey: "defaultMac"}, + {networkKey: "none", isLiteral: true, wantErr: false, expectKey: ""}, + {networkKey: "container:whatever", isLiteral: true, wantErr: true, expectKey: "container"}, + {networkKey: "bridge", isLiteral: true, wantErr: false, expectKey: "passedMac"}, + {networkKey: "networkBridge", isLiteral: false, wantErr: false, expectKey: "passedMac"}, + {networkKey: "networkMACvlan", isLiteral: false, wantErr: false, expectKey: "passedMac"}, + {networkKey: "networkIPvlan", isLiteral: false, wantErr: true, expectKey: "not support"}, + } - if wantErr { - assert.Assert(t, res.ExitCode != 0, "Command should have failed", res) - assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined())) - } else { - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) - assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout())) - } - }) + for i, mc := range macTestCases { + mc := mc + i := i + desc := mc.networkKey + if !mc.isLiteral { + desc = mc.networkKey + " (custom)" + } + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: fmt.Sprintf("network %s", desc), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + macAddress, err := nettestutil.GenerateMACAddress() + assert.NilError(helpers.T(), err) + data.Labels().Set(fmt.Sprintf("mac-%d", i), macAddress) + + network := mc.networkKey + if !mc.isLiteral { + network = data.Labels().Get(mc.networkKey) + } + cmd := helpers.Command("run", "--rm", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage) + cmd.Feed(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'")) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if mc.wantErr { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Output: func(stdout string, t tig.T) { + // For error cases, check combined stderr + }, + } + } + + expectedStr := "" + switch mc.expectKey { + case "defaultMac": + expectedStr = data.Labels().Get("defaultMac") + case "passedMac": + expectedStr = data.Labels().Get(fmt.Sprintf("mac-%d", i)) + case "": + // no output expected (none network) + } + + if expectedStr == "" { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + } + } + return &test.Expected{ + Output: expect.Contains(expectedStr), + } + }, + }) } + + testCase.Run(t) } func TestHostsFileMounts(t *testing.T) { - if rootlessutil.IsRootless() { - if detachedNetNS, _ := rootlessutil.DetachedNetNS(); detachedNetNS != "" { - t.Skip("/etc/hosts is not writable") + testCase := nerdtest.Setup() + + testCase.Require = require.Not(nerdtest.RootlessWithDetachNetNS) // etc/hosts is not writable + testCase.NoParallel = true + + type hostsTestCase struct { + desc string + args []string + wantFail bool + } + + hostsTestCases := []hostsTestCase{ + // /etc/hosts tests + { + desc: "write /etc/hosts default network", + args: []string{"run", "--rm", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts"}, + }, + { + desc: "write /etc/hosts host network", + args: []string{"run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts"}, + }, + { + desc: "write /etc/hosts host network ro mount fails", + args: []string{"run", "--rm", "-v", "/etc/hosts:/etc/hosts:ro", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts"}, + wantFail: true, + }, + { + desc: "write /etc/hosts host network rw mount", + args: []string{"run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts"}, + }, + { + desc: "restore /etc/hosts host network rw mount", + args: []string{"run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, "sh", "-euxc", "head -n -1 /etc/hosts > temp && cat temp > /etc/hosts"}, + }, + { + desc: "write /etc/hosts none network", + args: []string{"run", "--rm", "--network", "none", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/hosts"}, + }, + // /etc/resolv.conf tests + { + desc: "write /etc/resolv.conf default network", + args: []string{"run", "--rm", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf"}, + }, + { + desc: "write /etc/resolv.conf host network", + args: []string{"run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf"}, + }, + { + desc: "write /etc/resolv.conf host network ro mount fails", + args: []string{"run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf"}, + wantFail: true, + }, + { + desc: "write /etc/resolv.conf host network rw mount", + args: []string{"run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf"}, + }, + { + desc: "restore /etc/resolv.conf host network rw mount", + args: []string{"run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, "sh", "-euxc", "head -n -1 /etc/resolv.conf > temp && cat temp > /etc/resolv.conf"}, + }, + { + desc: "write /etc/resolv.conf host network after restore", + args: []string{"run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo >> /etc/resolv.conf"}, + }, + } + + for _, hc := range hostsTestCases { + hc := hc + var expected test.Manager + if hc.wantFail { + expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) + } else { + expected = test.Expects(expect.ExitCodeSuccess, nil, nil) } + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: hc.desc, + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(hc.args...) + }, + Expected: expected, + }) } - base := testutil.NewBase(t) - - base.Cmd("run", "--rm", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/hosts").AssertOK() - base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/hosts").AssertOK() - base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts:ro", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/hosts").AssertFail() - // add a line into /etc/hosts and remove it. - base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/hosts").AssertOK() - base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "head -n -1 /etc/hosts > temp && cat temp > /etc/hosts").AssertOK() - base.Cmd("run", "--rm", "--network", "none", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/hosts").AssertOK() - - base.Cmd("run", "--rm", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() - base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() - base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/resolv.conf").AssertFail() - // add a line into /etc/resolv.conf and remove it. - base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() - base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "head -n -1 /etc/resolv.conf > temp && cat temp > /etc/resolv.conf").AssertOK() - base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, - "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() + + testCase.Run(t) } func TestRunContainerWithStaticIP6(t *testing.T) { + testCase := nerdtest.Setup() + if rootlessutil.IsRootless() && !testutil.RootlessKitIPv6Enabled(t.Context()) { t.Skip("Rootless IPv6 requires CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6=true; see docs/rootless.md") } - networkName := "test-network" + networkSubnet := "2001:db8:5::/64" - _, subnet, err := net.ParseCIDR(networkSubnet) - assert.Assert(t, err == nil) - base := testutil.NewBaseWithIPv6Compatible(t) - base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK() - t.Cleanup(func() { - base.Cmd("network", "rm", networkName).Run() - }) - testCases := []struct { - ip string - shouldSuccess bool - checkTheIPAddress bool - }{ + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("networkName", data.Identifier("ipv6net")) + data.Labels().Set("networkSubnet", networkSubnet) + helpers.Ensure("network", "create", data.Identifier("ipv6net"), "--subnet", networkSubnet, "--ipv6") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("ipv6net")) + } + + testCase.SubTests = []*test.Case{ { - ip: "", - shouldSuccess: true, - checkTheIPAddress: false, + Description: "auto-assigned IPv6 within subnet", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--name", data.Identifier("auto"), + "--network", data.Labels().Get("networkName"), + testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + _, subnet, err := net.ParseCIDR(data.Labels().Get("networkSubnet")) + assert.NilError(t, err) + ip := nerdtest.FindIPv6(stdout) + assert.Assert(t, subnet.Contains(ip), "expected subnet %s to include ip %s", subnet, ip) + }, + } + }, }, { - ip: "2001:db8:5::6", - shouldSuccess: true, - checkTheIPAddress: true, + Description: "static IPv6 exact match", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--name", data.Identifier("static"), + "--network", data.Labels().Get("networkName"), + "--ip6", "2001:db8:5::6", + testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + _, subnet, err := net.ParseCIDR(data.Labels().Get("networkSubnet")) + assert.NilError(t, err) + ip := nerdtest.FindIPv6(stdout) + assert.Assert(t, subnet.Contains(ip), "expected subnet %s to include ip %s", subnet, ip) + assert.Equal(t, "2001:db8:5::6", ip.String()) + }, + } + }, }, { - ip: "2001:db8:4::6", - shouldSuccess: false, - checkTheIPAddress: false, + Description: "static IPv6 outside subnet fails", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--name", data.Identifier("badip6"), + "--network", data.Labels().Get("networkName"), + "--ip6", "2001:db8:4::6", + testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, } - tID := testutil.Identifier(t) - for i, tc := range testCases { - i := i - tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName := fmt.Sprintf("%s-%d", tID, i) - base := testutil.NewBaseWithIPv6Compatible(t) - args := []string{ - "run", "--rm", "--name", testContainerName, "--network", networkName, - } - if tc.ip != "" { - args = append(args, "--ip6", tc.ip) - } - args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...) - cmd := base.Cmd(args...) - if !tc.shouldSuccess { - cmd.AssertFail() - return - } - cmd.AssertOutWithFunc(func(stdout string) error { - ip := nerdtest.FindIPv6(stdout) - if !subnet.Contains(ip) { - return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) - } - if tc.checkTheIPAddress { - if ip.String() != tc.ip { - return fmt.Errorf("expected ip %s, got %s", tc.ip, ip) - } - } - return nil - }) - }) - } + + testCase.Run(t) } func TestNoneNetworkHostName(t *testing.T) { From 0610d66500fc2939a208740389f39c5725b384f9 Mon Sep 17 00:00:00 2001 From: akshitguptaa Date: Tue, 21 Jul 2026 00:08:33 +0530 Subject: [PATCH 708/868] events: rename Status field to Action to match Docker v29 Signed-off-by: akshitguptaa --- .../system/system_events_linux_test.go | 17 +++++------- pkg/cmd/system/events.go | 26 +++++++++---------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index c6b699d8814..2e1d46c4629 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -30,15 +30,10 @@ import ( // startEventOutput returns the substring expected in the JSON output of a // container "start" event. Docker v29 dropped the legacy top-level "status" -// field from the events API, exposing only "Action", whereas nerdctl still -// emits a "Status" field. -// https://github.com/moby/moby/pull/50832 -// https://github.com/containerd/nerdctl/issues/5028 +// field from the events API in favor of "Action" +// (https://github.com/moby/moby/pull/50832), and nerdctl now matches that. func startEventOutput() string { - if nerdtest.IsDocker() { - return "\"Action\":\"start\"" - } - return "tatus\":\"start\"" + return "\"Action\":\"start\"" } func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { @@ -88,7 +83,7 @@ func TestEventFilters(t *testing.T) { }, Data: test.WithLabels(map[string]string{ "filter": "event=START", - "output": "\"Status\":\"start\"", + "output": "\"Action\":\"start\"", }), }, { @@ -117,7 +112,7 @@ func TestEventFilters(t *testing.T) { }, Data: test.WithLabels(map[string]string{ "filter": "event=unknown", - "output": "\"Status\":\"unknown\"", + "output": "\"Action\":\"unknown\"", }), }, { @@ -146,7 +141,7 @@ func TestEventFilters(t *testing.T) { }, Data: test.WithLabels(map[string]string{ "filter": "status=unknown", - "output": "\"Status\":\"unknown\"", + "output": "\"Action\":\"unknown\"", }), }, { diff --git a/pkg/cmd/system/events.go b/pkg/cmd/system/events.go index 41ddc1ffa8c..e67395dc51e 100644 --- a/pkg/cmd/system/events.go +++ b/pkg/cmd/system/events.go @@ -42,25 +42,25 @@ type EventOut struct { ID string Namespace string Topic string - Status Status + Action Action Event string Labels map[string]string } -type Status string +type Action string const ( - START Status = "start" - UNKNOWN Status = "unknown" + START Action = "start" + UNKNOWN Action = "unknown" ) -var statuses = [...]Status{START, UNKNOWN} +var actions = [...]Action{START, UNKNOWN} -func isStatus(status string) bool { - status = strings.ToLower(status) +func isAction(action string) bool { + action = strings.ToLower(action) - for _, supportedStatus := range statuses { - if string(supportedStatus) == status { + for _, supportedAction := range actions { + if string(supportedAction) == action { return true } } @@ -68,7 +68,7 @@ func isStatus(status string) bool { return false } -func TopicToStatus(topic string) Status { +func TopicToAction(topic string) Action { if strings.Contains(strings.ToLower(topic), string(START)) { return START } @@ -85,11 +85,11 @@ func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, er switch strings.ToUpper(filter) { case "EVENT", "STATUS": return func(e *EventOut) bool { - if !isStatus(string(e.Status)) { + if !isAction(string(e.Action)) { return false } - return strings.EqualFold(string(e.Status), filterValue) + return strings.EqualFold(string(e.Action), filterValue) }, nil case "LABEL": parts := strings.SplitN(filterValue, "=", 2) @@ -240,7 +240,7 @@ func Events(ctx context.Context, client *containerd.Client, options types.System labels = container.Labels } } - eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out), labels} + eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToAction(e.Topic), string(out), labels} match := applyFilters(&eOut, filterMap) if match { if tmpl != nil { From c7afb3cbe8b448ce013f4bea39f640e4608505a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:32:34 +0000 Subject: [PATCH 709/868] build(deps): bump actions/checkout from 7.0.0 to 7.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima-freebsd.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 936cdccebde..3398c31be17 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 2bcb30d7894..f9b27d057b9 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index baf7e1af013..3f443e3513f 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index b754a2482b7..3859022bf40 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 867617afd18..849da3d1a89 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 c100b9446b5..ca1331e94f2 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 3ef985dca5f..5ddcc4616d2 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -104,7 +104,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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 index f25478b4c58..393309e32ba 100644 --- a/.github/workflows/job-test-in-lima-freebsd.yml +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -17,7 +17,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 50ad41903fe..67722e1d873 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -37,7 +37,7 @@ jobs: GO_VERSION: ${{ inputs.go-version }} steps: - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 30d619b8388..0c24f49fa23 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 716cce679d5..173127ca1f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 5b18b9ab9c1..92b4e611dce 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -50,7 +50,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index a37b1b7540c..275e3d4ed6a 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-26.04 steps: - name: "Init: checkout" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index 42dbbe8609f..10b2b9a7db3 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -36,7 +36,7 @@ jobs: # canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 100 persist-credentials: false From f3512d28554e3b7a7179c3b770f09db90143fdab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:32:58 +0000 Subject: [PATCH 710/868] build(deps): bump github.com/klauspost/compress from 1.19.0 to 1.19.1 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.0 to 1.19.1. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.19.0...v1.19.1) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b667bf3c09..eb1c69317e8 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 - github.com/klauspost/compress v1.19.0 + github.com/klauspost/compress v1.19.1 github.com/mattn/go-isatty v0.0.23 //gomodjail:unconfined github.com/moby/moby/client v0.5.0 github.com/moby/moby/v2 v2.0.0-beta.18 diff --git a/go.sum b/go.sum index 91a1db1c75e..a09ce1e671a 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ 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/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= -github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 9ffdeb101a0101f33688563e3cc25378f2c68825 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:43:44 +0000 Subject: [PATCH 711/868] build(deps): bump github.com/containerd/imgcrypt/v2 from 2.0.2 to 2.0.3 Bumps [github.com/containerd/imgcrypt/v2](https://github.com/containerd/imgcrypt) from 2.0.2 to 2.0.3. - [Release notes](https://github.com/containerd/imgcrypt/releases) - [Changelog](https://github.com/containerd/imgcrypt/blob/main/CHANGES) - [Commits](https://github.com/containerd/imgcrypt/compare/v2.0.2...v2.0.3) --- updated-dependencies: - dependency-name: github.com/containerd/imgcrypt/v2 dependency-version: 2.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 13 +++++++------ go.sum | 34 +++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 0b667bf3c09..55c49d1053a 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined github.com/containerd/go-cni v1.1.13 //gomodjail:unconfined - github.com/containerd/imgcrypt/v2 v2.0.2 //gomodjail:unconfined + github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined @@ -81,7 +81,7 @@ require ( github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/plugin v1.1.0 // indirect github.com/containerd/ttrpc v1.2.8 // indirect - github.com/containers/ocicrypt v1.2.1 // indirect + github.com/containers/ocicrypt v1.3.2 // indirect github.com/creack/pty v1.1.24 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect @@ -91,13 +91,12 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.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 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-shellwords v1.0.13 // indirect - github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/miekg/pkcs11 v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -120,7 +119,7 @@ require ( github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined github.com/sirupsen/logrus v1.9.4 // indirect - github.com/smallstep/pkcs7 v0.1.1 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/tinylib/msgp v1.3.0 // indirect @@ -137,7 +136,7 @@ require ( 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.81.1 // indirect + google.golang.org/grpc v1.82.0 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -148,7 +147,9 @@ require ( require ( cyphar.com/go-pathrs v0.2.5 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/moby/moby/api v1.55.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect diff --git a/go.sum b/go.sum index 91a1db1c75e..6d575e678a6 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vL github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/Microsoft/hcsshim v0.15.0-rc.3 h1:ZTNzOp0QwJ1EiL3zopSOawIG0j7zAvzJx0rBmcR6HJ0= github.com/Microsoft/hcsshim v0.15.0-rc.3/go.mod h1:VhDiwXgb8cEJxO9H57YL4NNIYqvZKpqvSDcimLyo7m8= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -23,6 +25,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE= github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= @@ -48,8 +52,8 @@ github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7H github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA= github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= -github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtqtALqfuM= -github.com/containerd/imgcrypt/v2 v2.0.2/go.mod h1:8r4JW1b83jkDhaioOUZ7idxIYp+Wn1k4E4KXwy2oSNI= +github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQujLw7UQ3w= +github.com/containerd/imgcrypt/v2 v2.0.3/go.mod h1:Sjfd3uOiBWtb1dUo1yS5Z/ZxdfMU7MpBr2pzs9HoRDs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk= @@ -72,8 +76,8 @@ github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEm github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= -github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= -github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= +github.com/containers/ocicrypt v1.3.2 h1:MuqHSfiPpGzoAQdgDSX85FgixSgIm9mSDXTLTgugY5E= +github.com/containers/ocicrypt v1.3.2/go.mod h1:ntBZabYG0rlvstB/1rK/ba2laaU5EHE0pD5ioHoPTq4= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= @@ -194,8 +198,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= +github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -285,8 +289,8 @@ github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= -github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -356,7 +360,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -397,7 +401,7 @@ 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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -418,7 +422,7 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -429,7 +433,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -440,7 +444,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -471,8 +475,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac 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.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= 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= From c02e91c3715c91102c81c00e734122be2c96f8e8 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Fri, 3 Jul 2026 10:26:14 +0530 Subject: [PATCH 712/868] feat(network): support --aux-address on network create Docker's network create reserves auxiliary addresses so IPAM never hands them out to containers. nerdctl had no equivalent. host-local has no exclude list, but it allocates across every range in a set, so each reserved IP is carved out by splitting the subnet range around it. The reserved name=IP pairs are matched to the subnet that contains them (so dual-stack picks the right family), rejected when they hit the network or gateway address or fall outside every subnet, and recorded so network inspect reports AuxiliaryAddresses the same way Docker does. Signed-off-by: Mayur Das --- cmd/nerdctl/network/network_create.go | 32 ++-- .../network/network_create_linux_test.go | 70 ++++++++ docs/command-reference.md | 3 +- go.mod | 2 + go.sum | 2 + pkg/api/types/network_types.go | 7 +- pkg/cmd/network/create.go | 17 ++ pkg/cmd/network/create_test.go | 35 ++++ pkg/cmd/network/list.go | 32 +++- pkg/inspecttypes/dockercompat/dockercompat.go | 47 ++++- .../dockercompat/dockercompat_test.go | 43 +++++ pkg/labels/labels.go | 7 + pkg/netutil/netutil.go | 128 +++++++++++++- pkg/netutil/netutil_test.go | 166 +++++++++++++++++- pkg/netutil/netutil_unix.go | 129 +++++++++++--- pkg/netutil/netutil_unix_test.go | 86 ++++++++- pkg/netutil/netutil_windows.go | 19 +- 17 files changed, 760 insertions(+), 65 deletions(-) create mode 100644 pkg/cmd/network/create_test.go diff --git a/cmd/nerdctl/network/network_create.go b/cmd/nerdctl/network/network_create.go index 596fa5f785f..5923219239c 100644 --- a/cmd/nerdctl/network/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -49,6 +49,7 @@ func createCommand() *cobra.Command { cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") cmd.Flags().StringArray("ip-range", nil, `Allocate container ip from a sub-range`) + cmd.Flags().StringArray("aux-address", nil, "Auxiliary IPv4 or IPv6 addresses used by Network driver, as name=IP pairs. The IPs are reserved and never assigned to containers") cmd.Flags().StringArray("label", nil, "Set metadata for a network") cmd.Flags().Bool("ipv4", true, "Enable IPv4 networking (set to false together with --ipv6 for an IPv6-only network)") cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking") @@ -93,6 +94,10 @@ func createAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + auxAddresses, err := cmd.Flags().GetStringArray("aux-address") + if err != nil { + return err + } labels, err := cmd.Flags().GetStringArray("label") if err != nil { return err @@ -112,18 +117,19 @@ func createAction(cmd *cobra.Command, args []string) error { } return network.Create(types.NetworkCreateOptions{ - GOptions: globalOptions, - Name: name, - Driver: driver, - Options: strutil.ConvertKVStringsToMap(opts), - IPAMDriver: ipamDriver, - IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), - Subnets: subnets, - Gateway: gateways, - IPRange: ipRanges, - Labels: labels, - IPv6: ipv6, - IPv4: &ipv4, - Internal: internal, + GOptions: globalOptions, + Name: name, + Driver: driver, + Options: strutil.ConvertKVStringsToMap(opts), + IPAMDriver: ipamDriver, + IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), + Subnets: subnets, + Gateway: gateways, + IPRange: ipRanges, + AuxAddresses: auxAddresses, + Labels: labels, + IPv6: ipv6, + IPv4: &ipv4, + Internal: internal, }, cmd.OutOrStdout()) } diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index d0d113e7e8d..589c6a88eb0 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -256,6 +256,76 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "with aux-address", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier(), + "--subnet", "10.6.0.0/24", + "--gateway", "10.6.0.1", + "--aux-address", "router=10.6.0.5", + "--aux-address", "dns=10.6.0.6", + ) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + netw := nerdtest.InspectNetwork(helpers, data.Identifier()) + var aux map[string]string + for _, c := range netw.IPAM.Config { + if c.Subnet == "10.6.0.0/24" { + aux = c.AuxiliaryAddresses + } + } + assert.Equal(t, aux["router"], "10.6.0.5") + assert.Equal(t, aux["dns"], "10.6.0.6") + }, + } + }, + }, + { + Description: "aux-address is reserved", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier(), + "--subnet", "10.6.1.0/24", + "--aux-address", "reserved=10.6.1.5", + ) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // The reserved address is carved out of the range, so requesting + // it explicitly must fail just as it does on Docker. + return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.1.5", testutil.CommonImage, "true") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "an un-reserved address is allocatable", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier(), + "--subnet", "10.6.2.0/24", + "--aux-address", "reserved=10.6.2.5", + ) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Positive control: an address in the same subnet that is not + // reserved allocates fine, so the failure above is specific to the + // reserved IP rather than an unrelated --ip problem. + return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.2.7", testutil.CommonImage, "true") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, } testCase.Run(t) diff --git a/docs/command-reference.md b/docs/command-reference.md index 93c481b4c11..28566a5155e 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1279,12 +1279,13 @@ Flags: - :whale: `--subnet`: Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16" - :whale: `--gateway`: IPv4 or IPv6 Gateway for the master subnet - :whale: `--ip-range`: Allocate container ip from a sub-range +- :whale: `--aux-address`: Auxiliary IPv4 or IPv6 addresses, as `name=IP` pairs. Each IP is reserved and never assigned to a container. Repeatable, and matched to the subnet that contains it. - :whale: `--label`: Set metadata on a network - :whale: `--ipv4`: Enable IPv4. Enabled by default; set to false with `--ipv6` and an IPv6 subnet for an IPv6-only network. `--ipv4=false` is not supported on Windows. - :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet. - :whale: `--internal`: Restrict external access to the network. -Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--scope` +Unimplemented `docker network create` flags: `--attachable`, `--config-from`, `--config-only`, `--ingress`, `--scope` ### :whale: nerdctl network ls diff --git a/go.mod b/go.mod index 0965b568bb6..5c284fc4a69 100644 --- a/go.mod +++ b/go.mod @@ -146,6 +146,8 @@ require ( tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect ) +require go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + require ( cyphar.com/go-pathrs v0.2.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index b2c52e152b2..b3ec1772b3f 100644 --- a/go.sum +++ b/go.sum @@ -350,6 +350,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 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= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go index 90969d6612e..70b5e6e4aab 100644 --- a/pkg/api/types/network_types.go +++ b/pkg/api/types/network_types.go @@ -33,8 +33,11 @@ type NetworkCreateOptions struct { Subnets []string Gateway []string IPRange []string - Labels []string - IPv6 bool + // AuxAddresses holds "name=IP" auxiliary addresses (docker --aux-address). + // Each IP is reserved so IPAM never hands it out to a container. + AuxAddresses []string + Labels []string + IPv6 bool // IPv4 enables IPv4 on the network. A nil value defaults to enabled, so a // directly-constructed NetworkCreateOptions keeps IPv4 on; setting it to // false together with IPv6 yields an IPv6-only network. diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index f64ba519f3a..eef7a9c8b2b 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -19,6 +19,7 @@ package network import ( "fmt" "io" + "sort" "github.com/containerd/errdefs" @@ -41,6 +42,22 @@ func Create(options types.NetworkCreateOptions, stdout io.Writer) error { return fmt.Errorf("IPv6-only network requires an IPv6 subnet, specify --subnet manually") } if len(options.Subnets) == 0 { + // Docker matches each aux-address to a subnet that contains it, so + // without any subnet there is nothing to match. Surface the same + // "no matching subnet for aux-address " error Docker returns. + aux, err := netutil.ParseAuxAddresses(options.AuxAddresses) + if err != nil { + return err + } + if len(aux) > 0 { + // Report a stable IP: map iteration order is random, so sort first. + ips := make([]string, 0, len(aux)) + for _, ip := range aux { + ips = append(ips, ip) + } + sort.Strings(ips) + return fmt.Errorf("no matching subnet for aux-address %s", ips[0]) + } if len(options.Gateway) > 0 || len(options.IPRange) > 0 { return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") } diff --git a/pkg/cmd/network/create_test.go b/pkg/cmd/network/create_test.go new file mode 100644 index 00000000000..74db8b1c292 --- /dev/null +++ b/pkg/cmd/network/create_test.go @@ -0,0 +1,35 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "io" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +// TestCreateAuxAddressWithoutSubnet verifies that an aux-address given without +// any subnet is rejected the same way Docker rejects it, before any CNI setup. +func TestCreateAuxAddressWithoutSubnet(t *testing.T) { + err := Create(types.NetworkCreateOptions{ + AuxAddresses: []string{"host=10.9.0.5"}, + }, io.Discard) + assert.ErrorContains(t, err, "no matching subnet for aux-address 10.9.0.5") +} diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go index d27333a3321..c01471532c0 100644 --- a/pkg/cmd/network/list.go +++ b/pkg/cmd/network/list.go @@ -28,9 +28,34 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/netutil" ) +// hiddenNetworkLabels are nerdctl-internal labels that back network state but are +// not user-facing, so `network ls` output and label filters must not expose them. +var hiddenNetworkLabels = map[string]struct{}{ + labels.NetworkAuxAddresses: {}, +} + +// visibleNetworkLabels returns a copy of the network's labels with the internal +// keys in hiddenNetworkLabels removed. It returns nil when the input is nil so +// bookkeeping like the aux-address reservation never surfaces in `network ls` or +// matches a `--filter label=` query. +func visibleNetworkLabels(m *map[string]string) map[string]string { + if m == nil { + return nil + } + out := make(map[string]string, len(*m)) + for k, v := range *m { + if _, hidden := hiddenNetworkLabels[k]; hidden { + continue + } + out[k] = v + } + return out +} + type networkPrintable struct { ID string // empty for non-nerdctl networks Name string @@ -102,7 +127,7 @@ func List(ctx context.Context, options types.NetworkListOptions) error { } } if n.NerdctlLabels != nil { - p.Labels = formatter.FormatLabels(*n.NerdctlLabels) + p.Labels = formatter.FormatLabels(visibleNetworkLabels(n.NerdctlLabels)) } pp[i] = p } @@ -181,8 +206,11 @@ func getNetworkFilterFuncs(filters []string) ([]func(*map[string]string) bool, [ } func networkMatchesFilter(net *netutil.NetworkConfig, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool) bool { + // Match against the user-visible labels only, so a --filter label= query can + // neither select on nor be confused by nerdctl-internal keys. + visible := visibleNetworkLabels(net.NerdctlLabels) for _, labelFilterFunc := range labelFilterFuncs { - if !labelFilterFunc(net.NerdctlLabels) { + if !labelFilterFunc(&visible) { return false } } diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 1e7211307fd..f05f091def0 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -1052,9 +1052,10 @@ func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) { } type IPAMConfig struct { - Subnet string `json:"Subnet,omitempty"` - Gateway string `json:"Gateway,omitempty"` - IPRange string `json:"IPRange,omitempty"` + Subnet string `json:"Subnet,omitempty"` + Gateway string `json:"Gateway,omitempty"` + IPRange string `json:"IPRange,omitempty"` + AuxiliaryAddresses map[string]string `json:"AuxiliaryAddresses,omitempty"` } type IPAM struct { @@ -1196,7 +1197,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) { res.Name = sCNI.Name for _, plugin := range sCNI.Plugins { for _, ranges := range plugin.Ipam.Ranges { - res.IPAM.Config = append(res.IPAM.Config, ranges...) + // A range-set normally describes one subnet; an aux-address + // reservation splits it into several sub-ranges that all share the + // subnet and gateway. Report the first entry per distinct subnet so a + // split subnet collapses to one IPAM.Config like Docker, without + // dropping entries for different subnets in the same set. The + // aux-addresses themselves are attached later from a nerdctl label. + seen := make(map[string]struct{}, len(ranges)) + for _, r := range ranges { + if _, ok := seen[r.Subnet]; ok { + continue + } + seen[r.Subnet] = struct{}{} + res.IPAM.Config = append(res.IPAM.Config, r) + } } } @@ -1205,7 +1219,30 @@ func NetworkFromNative(n *native.Network) (*Network, error) { } if n.NerdctlLabels != nil { - res.Labels = *n.NerdctlLabels + // Reserved aux-addresses are stored in a nerdctl label (host-local has no + // field for them). Decode it, attach each subnet's pairs to its config so + // inspect reports AuxiliaryAddresses like Docker, and keep the internal + // label out of the user-visible label set. + res.Labels = make(map[string]string, len(*n.NerdctlLabels)) + for k, v := range *n.NerdctlLabels { + if k == labels.NetworkAuxAddresses { + // A malformed value (the label is a user-settable nerdctl/ key) must + // not fail the whole inspect: log it and drop the label, leaving the + // config without AuxiliaryAddresses rather than erroring out. + var auxBySubnet map[string]map[string]string + if err := json.Unmarshal([]byte(v), &auxBySubnet); err != nil { + log.L.WithError(err).Warnf("ignoring malformed %s label", labels.NetworkAuxAddresses) + continue + } + for i := range res.IPAM.Config { + if aux, ok := auxBySubnet[res.IPAM.Config[i].Subnet]; ok { + res.IPAM.Config[i].AuxiliaryAddresses = aux + } + } + continue + } + res.Labels[k] = v + } } // Parse network subnets for interface matching diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 4c9986ca3b5..17c21f6155c 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -605,6 +605,49 @@ func TestGetUlimitsFromNative(t *testing.T) { } } +func TestNetworkFromNative(t *testing.T) { + // The first range-set is one subnet split into sub-ranges by an aux-address + // reservation and must collapse to a single IPAM.Config; the second set holds + // two distinct subnets that must both be kept; the empty set contributes + // nothing. Aux-addresses live in a nerdctl label (not the CNI config) and must + // be attached to the matching subnet while staying out of the user labels. + cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` + + `[{"Subnet":"10.6.0.0/24","Gateway":"10.6.0.1"},{"Subnet":"10.6.0.0/24"}],` + + `[{"Subnet":"10.7.0.0/24"},{"Subnet":"10.8.0.0/24"}],` + + `[]` + + `]}}]}` + lbls := map[string]string{ + labels.NetworkAuxAddresses: `{"10.6.0.0/24":{"router":"10.6.0.5"}}`, + "user": "keep", + } + got, err := NetworkFromNative(&native.Network{CNI: []byte(cni), NerdctlLabels: &lbls}) + assert.NilError(t, err) + assert.DeepEqual(t, []IPAMConfig{ + {Subnet: "10.6.0.0/24", Gateway: "10.6.0.1", AuxiliaryAddresses: map[string]string{"router": "10.6.0.5"}}, + {Subnet: "10.7.0.0/24"}, + {Subnet: "10.8.0.0/24"}, + }, got.IPAM.Config) + // The internal aux label is hidden; genuine user labels are preserved. + assert.DeepEqual(t, map[string]string{"user": "keep"}, got.Labels) +} + +func TestNetworkFromNativeMalformedAux(t *testing.T) { + // The aux label is a user-settable nerdctl/ key, so a malformed value must not + // fail the whole inspect: it is dropped, the config keeps no AuxiliaryAddresses, + // and the internal label still stays out of the user-visible labels. + cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[[{"Subnet":"10.6.0.0/24","Gateway":"10.6.0.1"}]]}}]}` + lbls := map[string]string{ + labels.NetworkAuxAddresses: "not-json", + "user": "keep", + } + got, err := NetworkFromNative(&native.Network{CNI: []byte(cni), NerdctlLabels: &lbls}) + assert.NilError(t, err) + assert.DeepEqual(t, []IPAMConfig{ + {Subnet: "10.6.0.0/24", Gateway: "10.6.0.1"}, + }, got.IPAM.Config) + assert.DeepEqual(t, map[string]string{"user": "keep"}, got.Labels) +} + func TestNetworkSettingsFromNative(t *testing.T) { tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw") if err != nil { diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 78f35552b6c..2ee5fdff578 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -111,6 +111,13 @@ const ( // (like "nerdctl/default-network=true" or "nerdctl/default-network=false") NerdctlDefaultNetwork = Prefix + "default-network" + // NetworkAuxAddresses stores a network's reserved --aux-address name=IP + // pairs, grouped per subnet, as a JSON object (map[subnetCIDR]map[name]IP). + // host-local has no field for auxiliary addresses, so they are kept here + // instead of in the CNI config and read back by `network inspect` to report + // AuxiliaryAddresses like Docker. + NetworkAuxAddresses = Prefix + "network-aux-addresses" + // ContainerAutoRemove is to check whether the --rm option is specified. ContainerAutoRemove = Prefix + "auto-remove" diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 026b210df65..b849ac17e81 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -23,13 +23,16 @@ import ( "encoding/json" "fmt" "net" + "net/netip" "os" "os/exec" "path/filepath" "sort" "strconv" + "strings" "github.com/containernetworking/cni/libcni" + "go4.org/netipx" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/pkg/namespaces" @@ -341,7 +344,7 @@ func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, } // A nil IPv4 defaults to enabled. ipv4 := opts.IPv4 == nil || *opts.IPv4 - ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6, ipv4, opts.Internal) + ipam, auxBySubnet, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.AuxAddresses, opts.IPAMOptions, opts.IPv6, ipv4, opts.Internal) if err != nil { return nil, err } @@ -349,7 +352,18 @@ func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, if err != nil { return nil, err } - netConf, err = e.generateNetworkConfig(opts.Name, opts.Labels, plugins) + // Reserved aux-addresses are kept in a nerdctl label rather than the CNI + // config, since host-local has no field for them; network inspect reads the + // label back to report AuxiliaryAddresses the way Docker does. + netLabels := opts.Labels + if len(auxBySubnet) > 0 { + b, err := json.Marshal(auxBySubnet) + if err != nil { + return nil, err + } + netLabels = append(append([]string{}, opts.Labels...), fmt.Sprintf("%s=%s", labels.NetworkAuxAddresses, b)) + } + netConf, err = e.generateNetworkConfig(opts.Name, netLabels, plugins) if err != nil { return nil, err } @@ -621,6 +635,116 @@ func parseIPAMRange(subnet *net.IPNet, gatewayStr, ipRangeStr string) (*IPAMRang return res, nil } +// ParseAuxAddresses parses Docker-style "name=IP" auxiliary-address pairs into a +// name-to-IP map. An entry with an empty IP (including one with no "=") is +// dropped; a later entry overrides an earlier one with the same name, matching +// Docker; and a non-empty but unparsable IP is an error. +func ParseAuxAddresses(raw []string) (map[string]string, error) { + if len(raw) == 0 { + return nil, nil + } + aux := make(map[string]string, len(raw)) + for _, kv := range raw { + name, ip, _ := strings.Cut(kv, "=") + if ip == "" { + continue + } + if net.ParseIP(ip) == nil { + return nil, fmt.Errorf("invalid aux-address %q", ip) + } + aux[name] = ip + } + if len(aux) == 0 { + return nil, nil + } + return aux, nil +} + +// splitIPAMRange reserves the given IPs inside a subnet's allocation range by +// carving them out. host-local has no exclude list, but it does allocate across +// every range in a set, so the reserved IPs become gaps between sub-ranges and +// are never handed out. Reserved IPs outside the allocation window need no split +// (host-local cannot reach them anyway). The base range's gateway and ip-range +// are kept on the first sub-range so the rest of the pipeline and `network +// inspect` behave exactly as the un-split case. +func splitIPAMRange(subnet *net.IPNet, base *IPAMRange, reserved []net.IP) ([]IPAMRange, error) { + if len(reserved) == 0 { + return []IPAMRange{*base}, nil + } + + // Resolve the allocation window. With an ip-range the base carries its + // bounds; otherwise it is the whole subnet minus the network address (and, + // for IPv4, the broadcast). + startIP := net.ParseIP(base.RangeStart) + if startIP == nil { + startIP, _ = subnetutil.FirstIPInSubnet(subnet) + } + start, ok := netipx.FromStdIP(startIP) + if !ok { + return nil, fmt.Errorf("invalid range start %q for subnet %s", startIP, subnet) + } + + var end netip.Addr + if endIP := net.ParseIP(base.RangeEnd); endIP != nil { + if end, ok = netipx.FromStdIP(endIP); !ok { + return nil, fmt.Errorf("invalid range end %q for subnet %s", endIP, subnet) + } + } else { + last, _ := subnetutil.LastIPInSubnet(subnet) + if end, ok = netipx.FromStdIP(last); !ok { + return nil, fmt.Errorf("invalid last address for subnet %s", subnet) + } + // IPv4's last address is the broadcast, which host-local never allocates, + // so step back to the last usable host. IPv6 has no broadcast; keep it. + if subnet.IP.To4() != nil { + end = end.Prev() + } + } + + // Keep only the reservations that fall inside the window; ones outside need + // no carving because host-local cannot reach them anyway. + inWindow := make([]netip.Addr, 0, len(reserved)) + for _, ip := range reserved { + if a, ok := netipx.FromStdIP(ip); ok && a.Compare(start) >= 0 && a.Compare(end) <= 0 { + inWindow = append(inWindow, a) + } + } + if len(inWindow) == 0 { + return []IPAMRange{*base}, nil + } + + // Remove each reserved IP from the window; the set's ranges come back sorted + // and coalesced, one per gap, which is the split host-local needs. + var builder netipx.IPSetBuilder + builder.AddRange(netipx.IPRangeFrom(start, end)) + for _, a := range inWindow { + builder.Remove(a) + } + set, err := builder.IPSet() + if err != nil { + return nil, err + } + ranges := set.Ranges() + if len(ranges) == 0 { + return nil, fmt.Errorf("aux-address reservations leave no allocatable IPs in subnet %s", subnet) + } + + out := make([]IPAMRange, len(ranges)) + for i, r := range ranges { + out[i] = IPAMRange{Subnet: subnet.String(), RangeStart: r.From().String(), RangeEnd: r.To().String()} + } + + // host-local reserves the gateway only when it is set on the range it lands + // in, and after splitting the gateway can be in any sub-range, so set it on + // all of them. The original ip-range is nerdctl-only bookkeeping for inspect, + // so keep it on the first sub-range alone. + for i := range out { + out[i].Gateway = base.Gateway + } + out[0].IPRange = base.IPRange + return out, nil +} + // convert the struct to a map func structToMap(in interface{}) (map[string]interface{}, error) { out := make(map[string]interface{}) diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index f818c59a1b2..e7c60d98ce1 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -128,11 +128,175 @@ func TestParseIPAMRange(t *testing.T) { assert.ErrorContains(t, err, tc.err) } else { assert.NilError(t, err) - assert.Equal(t, *tc.expected, *got) + assert.DeepEqual(t, *tc.expected, *got) } } } +func TestParseAuxAddresses(t *testing.T) { + t.Parallel() + type testCase struct { + raw []string + expected map[string]string + err string + } + testCases := []testCase{ + { + raw: nil, + expected: nil, + }, + { + raw: []string{"router=10.1.100.5", "dns=10.1.100.6"}, + expected: map[string]string{"router": "10.1.100.5", "dns": "10.1.100.6"}, + }, + { + // An empty name is allowed, matching Docker. + raw: []string{"=10.1.100.5"}, + expected: map[string]string{"": "10.1.100.5"}, + }, + { + // An entry with no "=" has an empty IP and is dropped, matching Docker. + raw: []string{"10.1.100.5"}, + expected: nil, + }, + { + // A later value overrides an earlier one with the same name. + raw: []string{"a=10.1.100.5", "a=10.1.100.6"}, + expected: map[string]string{"a": "10.1.100.6"}, + }, + { + raw: []string{"v6=2001:db8::5"}, + expected: map[string]string{"v6": "2001:db8::5"}, + }, + { + raw: []string{"bad=not-an-ip"}, + err: "invalid aux-address", + }, + } + for _, tc := range testCases { + got, err := ParseAuxAddresses(tc.raw) + if tc.err != "" { + assert.ErrorContains(t, err, tc.err) + continue + } + assert.NilError(t, err) + assert.DeepEqual(t, tc.expected, got) + } +} + +func TestSplitIPAMRange(t *testing.T) { + t.Parallel() + ips := func(addrs ...string) []net.IP { + out := make([]net.IP, len(addrs)) + for i, a := range addrs { + out[i] = net.ParseIP(a) + } + return out + } + type testCase struct { + name string + subnet string + base *IPAMRange + reserved []net.IP + expected []IPAMRange + err string + } + testCases := []testCase{ + { + name: "no reservation leaves the range untouched", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1"}, + reserved: nil, + expected: []IPAMRange{{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1"}}, + }, + { + name: "a mid-subnet reservation splits the range in two", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1"}, + reserved: ips("10.1.100.5"), + expected: []IPAMRange{ + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1"}, + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.6", RangeEnd: "10.1.100.254", Gateway: "10.1.100.1"}, + }, + }, + { + name: "two reservations produce three sub-ranges", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1"}, + reserved: ips("10.1.100.6", "10.1.100.5"), + expected: []IPAMRange{ + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1"}, + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.7", RangeEnd: "10.1.100.254", Gateway: "10.1.100.1"}, + }, + }, + { + // The gateway is the first usable address, so reserving the next one + // leaves a gateway-only sub-range; host-local reserves the gateway, so + // allocation still starts after the reservation. + name: "a reservation right after the gateway leaves a gateway-only range", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1"}, + reserved: ips("10.1.100.2"), + expected: []IPAMRange{ + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.1", Gateway: "10.1.100.1"}, + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.3", RangeEnd: "10.1.100.254", Gateway: "10.1.100.1"}, + }, + }, + { + name: "a reservation inside an ip-range splits within its bounds", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + reserved: ips("10.1.100.5"), + expected: []IPAMRange{ + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28"}, + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.6", RangeEnd: "10.1.100.15", Gateway: "10.1.100.1"}, + }, + }, + { + name: "a reservation outside the ip-range needs no split", + subnet: "10.1.100.0/24", + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + reserved: ips("10.1.100.200"), + expected: []IPAMRange{ + {Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + }, + }, + { + // Reserving every usable address in the window leaves nothing to hand + // out, which is an error rather than an empty range set. + name: "reservations leaving no allocatable address error", + subnet: "10.1.100.0/30", + base: &IPAMRange{Subnet: "10.1.100.0/30", Gateway: "10.1.100.1"}, + reserved: ips("10.1.100.1", "10.1.100.2"), + err: "leave no allocatable", + }, + { + name: "an IPv6 reservation splits the range around it", + subnet: "2001:db8::/64", + base: &IPAMRange{Subnet: "2001:db8::/64", Gateway: "2001:db8::1"}, + reserved: ips("2001:db8::5"), + // IPv6 has no broadcast, so the last address stays allocatable. + expected: []IPAMRange{ + {Subnet: "2001:db8::/64", RangeStart: "2001:db8::1", RangeEnd: "2001:db8::4", Gateway: "2001:db8::1"}, + {Subnet: "2001:db8::/64", RangeStart: "2001:db8::6", RangeEnd: "2001:db8::ffff:ffff:ffff:ffff", Gateway: "2001:db8::1"}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, subnet, err := net.ParseCIDR(tc.subnet) + assert.NilError(t, err) + got, err := splitIPAMRange(subnet, tc.base, tc.reserved) + if tc.err != "" { + assert.ErrorContains(t, err, tc.err) + return + } + assert.NilError(t, err) + assert.DeepEqual(t, tc.expected, got) + }) + } +} + // Tests whether nerdctl properly creates the default network when required. // Note that this test will require a CNI driver bearing the same name as // the type of the default network. (denoted by netutil.DefaultNetworkName, diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index 7e2549da449..9eadc546db7 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -25,6 +25,7 @@ import ( "net" "os/exec" "path/filepath" + "sort" "strconv" "strings" @@ -215,10 +216,20 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, auxAddresses []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, map[string]map[string]string, error) { var ipamConfig interface{} + // auxBySubnet carries each subnet's reserved aux-addresses back to the caller + // so they can be stored in a nerdctl label instead of the CNI config; it stays + // nil for drivers other than host-local, which have no such reservation. + var auxBySubnet map[string]map[string]string switch driver { case "default", "host-local": + // Reserved auxiliary addresses are only meaningful for host-local, where + // they are enforced by carving the reserved IPs out of the range below. + aux, err := ParseAuxAddresses(auxAddresses) + if err != nil { + return nil, nil, err + } ipamConf := newHostLocalIPAMConfig() if !internal { // An IPv6-only network has no IPv4 gateway, so its default route @@ -232,19 +243,21 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string {Dst: defaultRoute}, } } - ranges, findIPv4, err := e.parseIPAMRanges(subnets, gateways, ipRanges, ipv6) + ranges, findIPv4, auxByNet, err := e.parseIPAMRanges(subnets, gateways, ipRanges, aux, ipv6) if err != nil { - return nil, err + return nil, nil, err } + auxBySubnet = auxByNet if !ipv4 && findIPv4 { - return nil, fmt.Errorf("--ipv4=false conflicts with an IPv4 subnet") + return nil, nil, fmt.Errorf("--ipv4=false conflicts with an IPv4 subnet") } ipamConf.Ranges = append(ipamConf.Ranges, ranges...) if ipv4 && !findIPv4 { // The default IPv4 range uses a computed gateway and no ip-range; // any user-supplied gateway or ip-range belongs to an explicit subnet. + // It also has no user subnet, so no aux-address can match it. // Skipped when IPv4 is disabled, leaving the network IPv6-only. - ranges, _, _ = e.parseIPAMRanges([]string{""}, nil, nil, ipv6) + ranges, _, _, _ = e.parseIPAMRanges([]string{""}, nil, nil, nil, ipv6) ipamConf.Ranges = append(ipamConf.Ranges, ranges...) } ipamConfig = ipamConf @@ -252,7 +265,7 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string ipamConf := newDHCPIPAMConfig() crd, err := defaults.CNIRuntimeDir() if err != nil { - return nil, err + return nil, nil, err } ipamConf.DaemonSocketPath = filepath.Join(crd, "dhcp.sock") if err := systemutil.IsSocketAccessible(ipamConf.DaemonSocketPath); err != nil { @@ -271,7 +284,7 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string SkipDefault bool `json:"skipDefault"` }{} if err := json.Unmarshal([]byte(optValue), parsed); err != nil { - return nil, fmt.Errorf("unparsable ipam option %s %q", optName, optValue) + return nil, nil, fmt.Errorf("unparsable ipam option %s %q", optName, optValue) } if parsed.Type == "provide" { ipamConf.ProvideOptions = append(ipamConf.ProvideOptions, provideOption{ @@ -285,23 +298,23 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string SkipDefault: parsed.SkipDefault, }) } else { - return nil, fmt.Errorf("ipam option must have a type (provide or request)") + return nil, nil, fmt.Errorf("ipam option must have a type (provide or request)") } } ipamConfig = ipamConf default: - return nil, fmt.Errorf("unsupported ipam driver %q", driver) + return nil, nil, fmt.Errorf("unsupported ipam driver %q", driver) } ipam, err := structToMap(ipamConfig) if err != nil { - return nil, err + return nil, nil, err } - return ipam, nil + return ipam, auxBySubnet, nil } -func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRanges []string, ipv6 bool) ([][]IPAMRange, bool, error) { +func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRanges []string, aux map[string]string, ipv6 bool) ([][]IPAMRange, bool, map[string]map[string]string, error) { // Resolve every requested subnet first; parseSubnet also rejects overlaps // with existing networks. The pairing below then works purely on the parsed // CIDRs, so it can be unit-tested without probing the host's networks. @@ -309,24 +322,27 @@ func (e *CNIEnv) parseIPAMRanges(subnets []string, gateways []string, ipRanges [ for i := range subnets { subnet, err := e.parseSubnet(subnets[i]) if err != nil { - return nil, false, err + return nil, false, nil, err } parsedSubnets[i] = subnet } - return pairIPAMRanges(parsedSubnets, gateways, ipRanges, ipv6) + return pairIPAMRanges(parsedSubnets, gateways, ipRanges, aux, ipv6) } -// pairIPAMRanges matches each gateway and ip-range to the subnet that contains -// it and builds the per-subnet IPAM ranges. It is split out from subnet -// resolution so the matching can be tested without touching live networks. -func pairIPAMRanges(subnets []*net.IPNet, gateways []string, ipRanges []string, ipv6 bool) ([][]IPAMRange, bool, error) { +// pairIPAMRanges matches each gateway, ip-range and aux-address to the subnet +// that contains it and builds the per-subnet IPAM ranges. It is split out from +// subnet resolution so the matching can be tested without touching live networks. +// The returned auxBySubnet maps each subnet CIDR to its reserved name=IP pairs so +// the caller can persist them outside the CNI config (host-local has no field for +// them); it is nil when no aux-address is given. +func pairIPAMRanges(subnets []*net.IPNet, gateways []string, ipRanges []string, aux map[string]string, ipv6 bool) ([][]IPAMRange, bool, map[string]map[string]string, error) { // Parse the gateways once up front; matching them to subnets below is then // just a containment check, with no parse error mixed into the loop. parsedGateways := make([]net.IP, len(gateways)) for i, g := range gateways { gw := net.ParseIP(g) if gw == nil { - return nil, false, fmt.Errorf("failed to parse gateway %q", g) + return nil, false, nil, fmt.Errorf("failed to parse gateway %q", g) } parsedGateways[i] = gw } @@ -336,13 +352,27 @@ func pairIPAMRanges(subnets []*net.IPNet, gateways []string, ipRanges []string, for i, r := range ipRanges { _, ipNet, err := net.ParseCIDR(r) if err != nil { - return nil, false, fmt.Errorf("failed to parse ip-range %q", r) + return nil, false, nil, fmt.Errorf("failed to parse ip-range %q", r) } parsedRanges[i] = ipNet } + // Parse the aux-addresses once too, so the per-subnet loop only tests + // containment. aux is already validated by ParseAuxAddresses, so every value + // parses. matchedAux records which ones landed in a subnet, both to flag an + // unmatched aux as an error and to attach each aux to only the first subnet + // that contains it. + parsedAux := make(map[string]net.IP, len(aux)) + for name, ipStr := range aux { + parsedAux[name] = net.ParseIP(ipStr) + } + matchedAux := make(map[string]bool, len(parsedAux)) + findIPv4 := false ranges := make([][]IPAMRange, 0, len(subnets)) + // auxBySubnet holds each subnet's reserved name=IP pairs so the caller can + // persist them in a nerdctl label; it stays nil unless an aux-address matches. + var auxBySubnet map[string]map[string]string usedGateways := make([]bool, len(gateways)) usedRanges := make([]bool, len(ipRanges)) for _, subnet := range subnets { @@ -373,23 +403,68 @@ func pairIPAMRanges(subnets []*net.IPNet, gateways []string, ipRanges []string, } ipamRange, err := parseIPAMRange(subnet, gateway, ipRange) if err != nil { - return nil, findIPv4, err + return nil, findIPv4, nil, err + } + // Collect the aux-addresses that fall inside this subnet, rejecting the + // ones Docker also rejects (the network or gateway address), then reserve + // them by splitting the range. + gatewayIP := net.ParseIP(ipamRange.Gateway) + subnetAux := map[string]string{} + var reserved []net.IP + for name, ip := range parsedAux { + // Like gateway/ip-range, an aux-address attaches only to the first + // subnet that contains it. + if matchedAux[name] { + continue + } + if !subnet.Contains(ip) { + continue + } + matchedAux[name] = true + if ip.Equal(subnet.IP) || (gatewayIP != nil && ip.Equal(gatewayIP)) { + return nil, findIPv4, nil, fmt.Errorf("failed to allocate secondary ip address (%s:%s): Address already in use", name, ip) + } + subnetAux[name] = ip.String() + reserved = append(reserved, ip) + } + rangeSet, err := splitIPAMRange(subnet, ipamRange, reserved) + if err != nil { + return nil, findIPv4, nil, err + } + // Record the reservation against the subnet CIDR host-local writes, so the + // caller can store it in a label and inspect can match it back by subnet. + if len(subnetAux) > 0 { + if auxBySubnet == nil { + auxBySubnet = map[string]map[string]string{} + } + auxBySubnet[ipamRange.Subnet] = subnetAux } - ranges = append(ranges, []IPAMRange{*ipamRange}) + ranges = append(ranges, rangeSet) } - // Only known after every subnet is seen: a gateway or ip-range that matched - // none is a user error, same as Docker. + // Only known after every subnet is seen: a gateway, ip-range or aux-address + // that matched no subnet is a user error, same as Docker. for j, ok := range usedGateways { if !ok { - return nil, findIPv4, fmt.Errorf("no matching subnet for gateway %q", gateways[j]) + return nil, findIPv4, nil, fmt.Errorf("no matching subnet for gateway %q", gateways[j]) } } for j, ok := range usedRanges { if !ok { - return nil, findIPv4, fmt.Errorf("no matching subnet for ip-range %q", ipRanges[j]) + return nil, findIPv4, nil, fmt.Errorf("no matching subnet for ip-range %q", ipRanges[j]) } } - return ranges, findIPv4, nil + // Report a stable IP: map iteration order is random, so sort the unmatched. + var unmatchedAux []string + for name, ip := range parsedAux { + if !matchedAux[name] { + unmatchedAux = append(unmatchedAux, ip.String()) + } + } + if len(unmatchedAux) > 0 { + sort.Strings(unmatchedAux) + return nil, findIPv4, nil, fmt.Errorf("no matching subnet for aux-address %s", unmatchedAux[0]) + } + return ranges, findIPv4, auxBySubnet, nil } // FirewallPluginGEQVersion checks if the firewall plugin is greater than or equal to the specified version diff --git a/pkg/netutil/netutil_unix_test.go b/pkg/netutil/netutil_unix_test.go index 142a3e36b71..1d6ce4a59a2 100644 --- a/pkg/netutil/netutil_unix_test.go +++ b/pkg/netutil/netutil_unix_test.go @@ -92,7 +92,7 @@ func TestPairIPAMRangesIPRange(t *testing.T) { subnets := parse(t, "10.6.0.0/16", "2001:db8:6::/64") // Given v6-first to prove the pairing is by containment, not by index. ipRanges := []string{"2001:db8:6::/80", "10.6.1.0/24"} - ranges, findIPv4, err := pairIPAMRanges(subnets, nil, ipRanges, true) + ranges, findIPv4, _, err := pairIPAMRanges(subnets, nil, ipRanges, nil, true) assert.NilError(t, err) assert.Equal(t, true, findIPv4) got := map[string]string{} @@ -104,22 +104,98 @@ func TestPairIPAMRangesIPRange(t *testing.T) { }) t.Run("an ip-range matching no subnet errors", func(t *testing.T) { - _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"192.168.1.0/24"}, false) + _, _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"192.168.1.0/24"}, nil, false) assert.ErrorContains(t, err, `no matching subnet for ip-range "192.168.1.0/24"`) }) t.Run("an IPv4 ip-range with only an IPv6 subnet errors", func(t *testing.T) { - _, _, err := pairIPAMRanges(parse(t, "2001:db8:6::/64"), nil, []string{"10.6.1.0/24"}, true) + _, _, _, err := pairIPAMRanges(parse(t, "2001:db8:6::/64"), nil, []string{"10.6.1.0/24"}, nil, true) assert.ErrorContains(t, err, `no matching subnet for ip-range "10.6.1.0/24"`) }) t.Run("a second ip-range claiming the same subnet errors", func(t *testing.T) { - _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"10.6.1.0/24", "10.6.2.0/24"}, false) + _, _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"10.6.1.0/24", "10.6.2.0/24"}, nil, false) assert.ErrorContains(t, err, `no matching subnet for ip-range "10.6.2.0/24"`) }) t.Run("a malformed ip-range errors", func(t *testing.T) { - _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"bogus"}, false) + _, _, _, err := pairIPAMRanges(parse(t, "10.6.0.0/16"), nil, []string{"bogus"}, nil, false) assert.ErrorContains(t, err, `failed to parse ip-range "bogus"`) }) } + +// TestPairIPAMRangesAuxAddress exercises the aux-address side of pairIPAMRanges: +// each reserved address is matched to the subnet that contains it, recorded for +// inspect, and carved out of the range, while the network/gateway address and an +// address matching no subnet are rejected. +func TestPairIPAMRangesAuxAddress(t *testing.T) { + t.Parallel() + parse := func(t *testing.T, cidrs ...string) []*net.IPNet { + t.Helper() + subnets := make([]*net.IPNet, len(cidrs)) + for i, c := range cidrs { + _, n, err := net.ParseCIDR(c) + assert.NilError(t, err) + subnets[i] = n + } + return subnets + } + + t.Run("a reserved address is recorded and carved out of the range", func(t *testing.T) { + ranges, _, aux, err := pairIPAMRanges(parse(t, "10.7.0.0/24"), nil, nil, map[string]string{"host": "10.7.0.5"}, false) + assert.NilError(t, err) + assert.Equal(t, 1, len(ranges)) + // The reservation is returned keyed by subnet for the caller to store, and + // the range is split so .5 falls in the gap between the two sub-ranges. + assert.DeepEqual(t, map[string]string{"host": "10.7.0.5"}, aux["10.7.0.0/24"]) + assert.Equal(t, 2, len(ranges[0])) + assert.Equal(t, "10.7.0.4", ranges[0][0].RangeEnd) + assert.Equal(t, "10.7.0.6", ranges[0][1].RangeStart) + }) + + t.Run("a reserved network address is rejected", func(t *testing.T) { + _, _, _, err := pairIPAMRanges(parse(t, "10.7.0.0/24"), nil, nil, map[string]string{"net": "10.7.0.0"}, false) + assert.ErrorContains(t, err, "Address already in use") + }) + + t.Run("a reserved gateway address is rejected", func(t *testing.T) { + // With no explicit gateway the first address (.1) is the gateway, so an + // aux-address on it collides. + _, _, _, err := pairIPAMRanges(parse(t, "10.7.0.0/24"), nil, nil, map[string]string{"gw": "10.7.0.1"}, false) + assert.ErrorContains(t, err, "Address already in use") + }) + + t.Run("an aux-address matching no subnet errors", func(t *testing.T) { + _, _, _, err := pairIPAMRanges(parse(t, "10.7.0.0/24"), nil, nil, map[string]string{"x": "192.168.5.5"}, false) + assert.ErrorContains(t, err, "no matching subnet for aux-address 192.168.5.5") + }) + + t.Run("dual-stack keeps each aux-address on its own family's subnet", func(t *testing.T) { + ranges, _, aux, err := pairIPAMRanges(parse(t, "10.7.0.0/24", "fd00:7::/64"), nil, nil, map[string]string{"v4": "10.7.0.9", "v6": "fd00:7::9"}, true) + assert.NilError(t, err) + assert.Equal(t, 2, len(ranges)) + assert.DeepEqual(t, map[string]string{"v4": "10.7.0.9"}, aux["10.7.0.0/24"]) + assert.DeepEqual(t, map[string]string{"v6": "fd00:7::9"}, aux["fd00:7::/64"]) + }) + + t.Run("multiple aux-addresses in one subnet split it into three ranges", func(t *testing.T) { + ranges, _, aux, err := pairIPAMRanges(parse(t, "10.7.0.0/24"), nil, nil, map[string]string{"a": "10.7.0.5", "b": "10.7.0.9"}, false) + assert.NilError(t, err) + assert.Equal(t, 1, len(ranges)) + // Both reservations come back under the subnet key, and the subnet is + // carved into three ranges with .5 and .9 sitting in the two gaps. + assert.DeepEqual(t, map[string]string{"a": "10.7.0.5", "b": "10.7.0.9"}, aux["10.7.0.0/24"]) + assert.Equal(t, 3, len(ranges[0])) + assert.Equal(t, "10.7.0.4", ranges[0][0].RangeEnd) + assert.Equal(t, "10.7.0.6", ranges[0][1].RangeStart) + assert.Equal(t, "10.7.0.8", ranges[0][1].RangeEnd) + assert.Equal(t, "10.7.0.10", ranges[0][2].RangeStart) + }) + + t.Run("an aux-address in a subnet filtered out by disabled IPv6 errors", func(t *testing.T) { + // The fd00:7::/64 subnet is skipped because ipv6 is false, so its + // aux-address matches nothing and is reported, not silently dropped. + _, _, _, err := pairIPAMRanges(parse(t, "10.7.0.0/24", "fd00:7::/64"), nil, nil, map[string]string{"v6": "fd00:7::9"}, false) + assert.ErrorContains(t, err, "no matching subnet for aux-address fd00:7::9") + }) +} diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 6fef605b2f3..9400a1e535c 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -72,15 +72,20 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string, ipRanges []string, auxAddresses []string, opts map[string]string, ipv6, ipv4, internal bool) (map[string]interface{}, map[string]map[string]string, error) { switch driver { case "default": default: - return nil, fmt.Errorf("unsupported ipam driver %q", driver) + return nil, nil, fmt.Errorf("unsupported ipam driver %q", driver) } // IPv6-only networks are not supported on Windows. if !ipv4 { - return nil, fmt.Errorf("--ipv4=false is not supported on Windows") + return nil, nil, fmt.Errorf("--ipv4=false is not supported on Windows") + } + // The Windows nat IPAM has no way to reserve individual addresses, so there + // are never any aux-addresses to hand back to the caller. + if len(auxAddresses) > 0 { + return nil, nil, fmt.Errorf("--aux-address is not supported on Windows") } // Windows is single-subnet, so use at most one gateway and one ip-range. @@ -96,19 +101,19 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gateways []string ipamConfig := newWindowsIPAMConfig() subnet, err := e.parseSubnet(subnets[0]) if err != nil { - return nil, err + return nil, nil, err } ipamRange, err := parseIPAMRange(subnet, gatewayStr, ipRangeStr) if err != nil { - return nil, err + return nil, nil, err } ipamConfig.Subnet = ipamRange.Subnet ipamConfig.Routes = append(ipamConfig.Routes, IPAMRoute{Gateway: ipamRange.Gateway}) ipam, err := structToMap(ipamConfig) if err != nil { - return nil, err + return nil, nil, err } - return ipam, nil + return ipam, nil, nil } func FirewallPluginGEQVersion(firewallPath string, versionStr string) (bool, error) { From f05f585f1f92da8df37dcc007995ff11efd28ef1 Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Thu, 23 Jul 2026 22:25:09 +0100 Subject: [PATCH 713/868] test: convert compose run linux test to Tigron framework Signed-off-by: Daniel Benjamin --- cmd/nerdctl/compose/compose_run_linux_test.go | 199 ++++++++++-------- 1 file changed, 106 insertions(+), 93 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 217c21708d0..2f05e732fd8 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -20,9 +20,9 @@ import ( "fmt" "io" "path/filepath" + "strconv" "strings" "testing" - "time" "gotest.tools/v3/assert" @@ -34,6 +34,7 @@ import ( "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" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) @@ -114,123 +115,135 @@ services: } func TestComposeRunWithServicePorts(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) + testCase := nerdtest.Setup() - dockerComposeYAML := fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } + + dockerComposeYAML := fmt.Sprintf(` services: web: image: %s ports: - - 8080:80 -`, testutil.NginxAlpineImage) + - %d:80 +`, testutil.NginxAlpineImage, hostPort) - 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() + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - defer base.Cmd("rm", "-f", "-v", containerName).Run() - go func() { - // 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", "--service-ports", "--name", containerName, "web").Run() - }() - - checkNginx := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) - if err != nil { - return err - } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { - t.Logf("respBody=%q", respBody) - return fmt.Errorf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + + // specify the name of container in order to remove + // TODO: when `compose rm` is implemented, replace it. + cmd := helpers.Command("compose", "-f", composePath, "run", "--service-ports", "--name", data.Identifier(), "web") + cmd.WithPseudoTTY() + cmd.Background() + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + if composeYAML := data.Labels().Get("composeYAML"); composeYAML != "" { + helpers.Anyhow("compose", "-f", composeYAML, "down", "-v") } - return nil - } - var nginxWorking bool - for i := 0; i < 30; i++ { - t.Logf("(retry %d)", i) - err := checkNginx() - if err == nil { - nginxWorking = true - break + if portStr := data.Labels().Get("hostPort"); portStr != "" { + if port, err := strconv.Atoi(portStr); err == nil { + _ = portlock.Release(port) + } } - t.Log(err) - time.Sleep(3 * time.Second) } - if !nginxWorking { - t.Fatal("nginx is not working") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) } - t.Log("nginx seems functional") + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")), 5, false) + assert.NilError(tt, err) + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + assert.NilError(tt, err) + tt.Log(fmt.Sprintf("respBody=%q", respBody)) + assert.Assert(tt, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet), fmt.Sprintf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet)) + }, + } + } + + testCase.Run(t) } func TestComposeRunWithPublish(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) + testCase := nerdtest.Setup() - dockerComposeYAML := fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + hostPort, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) + helpers.T().FailNow() + } + + dockerComposeYAML := fmt.Sprintf(` services: web: image: %s `, testutil.NginxAlpineImage) - 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() + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) - defer base.Cmd("rm", "-f", "-v", containerName).Run() - go func() { - // 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", "--publish", "8080:80", "--name", containerName, "web").Run() - }() - - checkNginx := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) - if err != nil { - return err - } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { - t.Logf("respBody=%q", respBody) - return fmt.Errorf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("hostPort", strconv.Itoa(hostPort)) + + // specify the name of container in order to remove + // TODO: when `compose rm` is implemented, replace it. + cmd := helpers.Command("compose", "-f", composePath, "run", "--publish", fmt.Sprintf("%d:80", hostPort), "--name", data.Identifier(), "web") + cmd.WithPseudoTTY() + cmd.Background() + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + if composeYAML := data.Labels().Get("composeYAML"); composeYAML != "" { + helpers.Anyhow("compose", "-f", composeYAML, "down", "-v") } - return nil - } - var nginxWorking bool - for i := 0; i < 30; i++ { - t.Logf("(retry %d)", i) - err := checkNginx() - if err == nil { - nginxWorking = true - break + if portStr := data.Labels().Get("hostPort"); portStr != "" { + if port, err := strconv.Atoi(portStr); err == nil { + _ = portlock.Release(port) + } } - t.Log(err) - time.Sleep(3 * time.Second) } - if !nginxWorking { - t.Fatal("nginx is not working") + + 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: func(stdout string, tt tig.T) { + resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%s", data.Labels().Get("hostPort")), 5, false) + assert.NilError(tt, err) + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + assert.NilError(tt, err) + tt.Log(fmt.Sprintf("respBody=%q", respBody)) + assert.Assert(tt, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet), fmt.Sprintf("respBody does not contain %q", testutil.NginxAlpineIndexHTMLSnippet)) + }, + } } - t.Log("nginx seems functional") + + testCase.Run(t) } func TestComposeRunWithEnv(t *testing.T) { From b723e4562f3a987a020dcb73dbdab42b5558605f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:32:28 +0000 Subject: [PATCH 714/868] build(deps): bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/6599ee8b7a49aef6a770f63d261d214911a7ce02...6fc4b006235f201fdab3722e17240ab420d580e5) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 275e3d4ed6a..133ba843ed9 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,7 +92,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0 + uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From 05a0bac8bfdd4a6854660ade903bb4d813f16e7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:32:38 +0000 Subject: [PATCH 715/868] build(deps): bump github.com/mattn/go-isatty from 0.0.23 to 0.0.24 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.23 to 0.0.24. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.23...v0.0.24) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-version: 0.0.24 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8b08abd3233..170ecf5592d 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.1 - github.com/mattn/go-isatty v0.0.23 //gomodjail:unconfined + github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined 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 diff --git a/go.sum b/go.sum index 2a8b6f82c2c..dfeb92febd0 100644 --- a/go.sum +++ b/go.sum @@ -190,8 +190,8 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= -github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= From 588e6b4cd3fd1410328bdb8885b713cb00d2f1ca Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:23:12 -0700 Subject: [PATCH 716/868] fix: suppress spurious hostsstore NotFound warning on container removal Motivation: On Windows, `nerdctl run --rm ...` (and `nerdctl rm`) logs a confusing warning after every container removal: level=warning msg="failed to remove hosts file for container \"\"" error="hosts-store error\nnot found\nGetFileAttributesEx ...\\etchosts\\default\\: The system cannot find the file specified." Fixes #4678 Approach: pkg/containerutil/container_network_manager_windows.go never calls hostsstore.Acquire/AllocHostsFile (unlike the Linux and generic network manager code paths), so no hosts-store directory is ever created for a Windows container. As a result, hostsstore.Delete(id) in RemoveContainer (pkg/cmd/container/remove.go) always fails with store.ErrNotFound on Windows, and that failure was unconditionally logged as a warning. This is purely a noisy/confusing log message, not a functional bug: hostsstore.Delete's error was already only logged (soft failure), so container removal succeeds identically before and after this change. The fix mirrors the existing nameStore.Release handling a few lines above in the same function (which already ignores store.ErrNotFound to tolerate double-release with --rm): hostsstore.Delete's NotFound error is now ignored for the same reason - the hosts-store entry may simply never have been allocated for this container/platform/network mode. Validation: - go build ./... passes. - go test ./pkg/... passes (all packages ok, including pkg/dnsutil/hostsstore and pkg/store). - gofmt -l pkg/cmd/container/remove.go reports no issues. - pkg/cmd/container has no unit tests for RemoveContainer (it requires a live containerd.Container); this matches the existing test coverage pattern for that function (integration-only, via cmd/nerdctl/container/container_remove_test.go). Signed-off-by: Pujitha Paladugu Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- pkg/cmd/container/remove.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index 744d20aabb3..ceffe0374b7 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -258,8 +258,11 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions hs, err := hostsstore.New(dataStore, containerNamespace) if err != nil { log.G(ctx).WithError(err).Warnf("failed to instantiate hostsstore for %q", containerNamespace) - } else if err = hs.Delete(id); err != nil { + } else if err = hs.Delete(id); err != nil && !errors.Is(err, store.ErrNotFound) { // De-allocate hosts file - soft failure + // Some platforms and network modes never allocate a hosts file for the container in the first + // place (e.g. Windows containers), so ignore NotFound errors here, similarly to the + // nameStore.Release call above. log.G(ctx).WithError(err).Warnf("failed to remove hosts file for container %q", id) } From 94e4228fa9574b2c8aeedfd1e4e8f7c2ddadffcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:56:50 +0000 Subject: [PATCH 717/868] build(deps): bump google.golang.org/grpc from 1.82.0 to 1.82.1 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.0 to 1.82.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.82.0...v1.82.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 170ecf5592d..962a2cf4ce1 100644 --- a/go.mod +++ b/go.mod @@ -136,7 +136,7 @@ require ( 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.82.0 // indirect + google.golang.org/grpc v1.82.1 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index dfeb92febd0..427dc95990a 100644 --- a/go.sum +++ b/go.sum @@ -475,8 +475,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac 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.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= 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= From e508fcf22c84feb01be1e495728012e023721b88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:32:15 +0000 Subject: [PATCH 718/868] build(deps): bump docker/login-action from 4.4.0 to 4.5.1 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.1. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.5.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 3398c31be17..a81b2ee0ab9 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -46,7 +46,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From c44c9c1f16d7ecc43ec1bfe5a456ae02a1c242e8 Mon Sep 17 00:00:00 2001 From: Mustaeen Ahmed Date: Sat, 25 Jul 2026 00:38:31 -0700 Subject: [PATCH 719/868] refactor container_list_windows_test.go to use Tigron Tests now use nerdtest.Setup and Tigron. Updates #4613 Signed-off-by: Mustaeen Ahmed --- .../container/container_list_windows_test.go | 342 +++++++++--------- 1 file changed, 161 insertions(+), 181 deletions(-) diff --git a/cmd/nerdctl/container/container_list_windows_test.go b/cmd/nerdctl/container/container_list_windows_test.go index 08bd3c3c5c8..3da81b2ef07 100644 --- a/cmd/nerdctl/container/container_list_windows_test.go +++ b/cmd/nerdctl/container/container_list_windows_test.go @@ -23,224 +23,204 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) -type psTestContainer struct { - name string - labels map[string]string - network string -} - -func preparePsTestContainer(t *testing.T, identity string, restart bool, hyperv bool) (*testutil.Base, psTestContainer) { - base := testutil.NewBase(t) +func setupPsTestContainer(identity string, restart bool, hyperv bool) func(data test.Data, helpers test.Helpers) { + return func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) + containerName := data.Identifier(identity) + data.Labels().Set("containerName", containerName) + // A container can have multiple labels. + testLabels := make(map[string]string) + + for i := 0; i < 2; i++ { + k := fmt.Sprintf("%s-%d", data.Identifier(identity), i) + testLabels[k] = k + data.Labels().Set(fmt.Sprintf("label-key-%d", i), k) + data.Labels().Set(fmt.Sprintf("label-value-%d", i), k) + } - base.Cmd("pull", "--quiet", testutil.NginxAlpineImage).AssertOK() + args := []string{ + "run", + "-d", + } - testContainerName := testutil.Identifier(t) + identity - t.Cleanup(func() { - base.Cmd("rm", "-f", testContainerName).AssertOK() - }) + if hyperv { + args = append(args, "--isolation", "hyperv") + } - // A container can have multiple labels. - // Therefore, this test container has multiple labels to check it. - testLabels := make(map[string]string) - keys := []string{ - testutil.Identifier(t) + identity, - testutil.Identifier(t) + identity, - } - // fill the value of testLabels - for _, k := range keys { - testLabels[k] = k - } + if !restart { + args = append(args, "--restart=no") + } - args := []string{ - "run", - "-d", - "--name", - testContainerName, - "--label", - formatter.FormatLabels(testLabels), - testutil.NginxAlpineImage, - } - if !restart { - args = append(args, "--restart=no") - } - if hyperv { - args = append(args[:3], args[1:]...) - args[1], args[2] = "--isolation", "hyperv" - } + args = append(args, + "--name", containerName, + "--label", formatter.FormatLabels(testLabels), + testutil.NginxAlpineImage, + ) - base.Cmd(args...).AssertOK() - if restart { - base.EnsureContainerStarted(testContainerName) + helpers.Ensure(args...) + if restart { + nerdtest.EnsureContainerStarted(helpers, containerName) + } } +} - return base, psTestContainer{ - name: testContainerName, - labels: testLabels, - network: testContainerName, +func cleanupPsTestContainer() func(data test.Data, helpers test.Helpers) { + return func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("containerName")) } } func TestListProcessContainer(t *testing.T) { - base, testContainer := preparePsTestContainer(t, "list", true, false) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { - // An example of nerdctl/docker ps -n 1 -s - // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE - // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - - container, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, container, testContainer.name) - - image, _ := tab.ReadRow(lines[1], "IMAGE") - assert.Equal(t, image, testutil.NginxAlpineImage) - - size, _ := tab.ReadRow(lines[1], "SIZE") - - // there is some difference between nerdctl and docker in calculating the size of the container - expectedSize := "36.0 MiB (virtual " + testCase := nerdtest.Setup() + testCase.Setup = setupPsTestContainer("list", true, false) + testCase.Cleanup = cleanupPsTestContainer() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-s", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("containerName"))) + } - if !strings.Contains(size, expectedSize) { - return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, fmt.Sprintf("expected at least 2 lines, got %d", len(lines))) + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "failed to parse header") + container, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, container, data.Labels().Get("containerName")) + image, _ := tab.ReadRow(lines[1], "IMAGE") + assert.Equal(t, image, testutil.NginxAlpineImage) + size, _ := tab.ReadRow(lines[1], "SIZE") + expectedSize := "36.0 MiB (virtual " + assert.Assert( + t, + strings.Contains(size, expectedSize), + fmt.Sprintf("expect container size %s, but got %s", expectedSize, size), + ) + }, } + } - return nil - }) + testCase.Run(t) } - func TestListHyperVContainer(t *testing.T) { - if !testutil.HyperVSupported() { - t.Skip("HyperV is not enabled, skipping test") - } - - base, testContainer := preparePsTestContainer(t, "list", true, true) - inspect := base.InspectContainer(testContainer.name) - //check with HCS if the container is ineed a VM - isHypervContainer, err := testutil.HyperVContainer(inspect) - if err != nil { - t.Fatalf("unable to list HCS containers: %s", err) + testCase := nerdtest.Setup() + testCase.Require = nerdtest.HyperV + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + setupPsTestContainer("list", true, true)(data, helpers) + containerName := data.Labels().Get("containerName") + inspect := nerdtest.InspectContainer(helpers, containerName) + isHypervContainer, err := testutil.HyperVContainer(inspect) + assert.NilError(helpers.T(), err, "unable to list HCS containers") + assert.Assert(helpers.T(), isHypervContainer, "expected HyperV container") } - assert.Assert(t, isHypervContainer, true) - - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "-s").AssertOutWithFunc(func(stdout string) error { - // An example of nerdctl/docker ps -n 1 -s - // CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE - // be8d386c991e docker.io/library/busybox:latest "top" 1 second ago Up c1 16.0 KiB (virtual 1.3 MiB) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - - container, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, container, testContainer.name) - image, _ := tab.ReadRow(lines[1], "IMAGE") - assert.Equal(t, image, testutil.NginxAlpineImage) + testCase.Cleanup = cleanupPsTestContainer() - size, _ := tab.ReadRow(lines[1], "SIZE") - - // there is some difference between nerdctl and docker in calculating the size of the container - expectedSize := "72.0 MiB (virtual " + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-s", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("containerName"))) + } - if !strings.Contains(size, expectedSize) { - return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, fmt.Sprintf("expected at least 2 lines, got %d", len(lines))) + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tSIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "failed to parse header") + container, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, container, data.Labels().Get("containerName")) + image, _ := tab.ReadRow(lines[1], "IMAGE") + assert.Equal(t, image, testutil.NginxAlpineImage) + size, _ := tab.ReadRow(lines[1], "SIZE") + expectedSize := "72.0 MiB (virtual " + assert.Assert(t, strings.Contains(size, expectedSize), + fmt.Sprintf("expect container size %s, but got %s", expectedSize, size)) + }, } + } - return nil - }) + testCase.Run(t) } - func TestListProcessContainerWideMode(t *testing.T) { - testutil.DockerIncompatible(t) - base, testContainer := preparePsTestContainer(t, "listWithMode", true, false) + testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Docker) + testCase.Setup = setupPsTestContainer("listWithMode", true, false) + testCase.Cleanup = cleanupPsTestContainer() - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "--format", "wide").AssertOutWithFunc(func(stdout string) error { - - // An example of nerdctl ps --format wide - // CONTAINER ID IMAGE PLATFORM COMMAND CREATED STATUS PORTS NAMES RUNTIME SIZE - // 17181f208b61 docker.io/library/busybox:latest linux/amd64 "top" About an hour ago Up busybox-17181 io.containerd.runc.v2 16.0 KiB (virtual 1.3 MiB) - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--format", "wide", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("containerName"))) + } - tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, fmt.Sprintf("expected at least 2 lines, got %d", len(lines))) + tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "failed to parse header") + container, _ := tab.ReadRow(lines[1], "NAMES") + assert.Equal(t, container, data.Labels().Get("containerName")) + image, _ := tab.ReadRow(lines[1], "IMAGE") + assert.Equal(t, image, testutil.NginxAlpineImage) + runtime, _ := tab.ReadRow(lines[1], "RUNTIME") + assert.Equal(t, runtime, "io.containerd.runhcs.v1") + size, _ := tab.ReadRow(lines[1], "SIZE") + expectedSize := "36.0 MiB (virtual " + assert.Assert(t, strings.Contains(size, expectedSize), + fmt.Sprintf("expect container size %s, but got %s", expectedSize, size)) + }, } + } - container, _ := tab.ReadRow(lines[1], "NAMES") - assert.Equal(t, container, testContainer.name) - - image, _ := tab.ReadRow(lines[1], "IMAGE") - assert.Equal(t, image, testutil.NginxAlpineImage) - - runtime, _ := tab.ReadRow(lines[1], "RUNTIME") - assert.Equal(t, runtime, "io.containerd.runhcs.v1") - - size, _ := tab.ReadRow(lines[1], "SIZE") - expectedSize := "36.0 MiB (virtual " - if !strings.Contains(size, expectedSize) { - return fmt.Errorf("expect container size %s, but got %s", expectedSize, size) - } - return nil - }) + testCase.Run(t) } - func TestListProcessContainerWithLabels(t *testing.T) { - base, testContainer := preparePsTestContainer(t, "listWithLabels", true, false) + testCase := nerdtest.Setup() + testCase.Setup = setupPsTestContainer("listWithLabels", true, false) + testCase.Cleanup = cleanupPsTestContainer() - // hope there are no tests running parallel - base.Cmd("ps", "-n", "1", "--format", "{{.Labels}}").AssertOutWithFunc(func(stdout string) error { - - // An example of nerdctl ps --format "{{.Labels}}" - // key1=value1,key2=value2,key3=value3 - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 line, got %d", len(lines)) - } - - // check labels using map - // 1. the results has no guarantee to show the same order. - // 2. the results has no guarantee to show only configured labels. - labelsMap, err := strutil.ParseCSVMap(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse labels: %v", err) - } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "--format", "{{.Labels}}", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("containerName"))) + } - for i := range testContainer.labels { - if value, ok := labelsMap[i]; ok { - assert.Equal(t, value, testContainer.labels[i]) - } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) == 1, fmt.Sprintf("expected 1 line, got %d", len(lines))) + labelsMap, err := strutil.ParseCSVMap(lines[0]) + assert.NilError(t, err, "failed to parse labels") + + for idx := 0; idx < 2; idx++ { + labelKey := data.Labels().Get(fmt.Sprintf("label-key-%d", idx)) + labelValue := data.Labels().Get(fmt.Sprintf("label-value-%d", idx)) + + if value, ok := labelsMap[labelKey]; ok { + assert.Equal(t, value, labelValue) + } + } + }, } - return nil - }) + } + testCase.Run(t) } From ec50c5ed5133f56c391e0938eb390ba96b859bfd Mon Sep 17 00:00:00 2001 From: Saleh Date: Sat, 25 Jul 2026 00:54:17 +0300 Subject: [PATCH 720/868] pkg/portutil/procnet: support big-endian hosts in ParseAddress The /proc/net/{tcp,tcp6} address bytes are written in the host's native byte order. ParseAddress unconditionally reversed each 4-byte group, which is correct on little-endian hosts but mangles the address on big-endian hosts such as s390x (e.g. 7F000001 parsed to 1.0.0.127 instead of 127.0.0.1). Add ParseAddressWithByteOrder, reading each group with the given binary.ByteOrder and writing it back in network order; ParseAddress passes binary.NativeEndian. Little-endian behavior is unchanged. Fixes #4465 Signed-off-by: Saleh --- pkg/portutil/procnet/procnet.go | 23 +++++++++++++++-------- pkg/portutil/procnet/procnetd_test.go | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pkg/portutil/procnet/procnet.go b/pkg/portutil/procnet/procnet.go index c68b5bed2b9..f7919d9f29e 100644 --- a/pkg/portutil/procnet/procnet.go +++ b/pkg/portutil/procnet/procnet.go @@ -17,6 +17,7 @@ package procnet import ( + "encoding/binary" "encoding/hex" "fmt" "net" @@ -72,12 +73,20 @@ func removeEmpty(array []string) (results []string) { // // See https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000 // -// ParseAddress is expected to be used for /proc/net/{tcp,tcp6} entries on -// little endian machines. -// Not sure how those entries look like on big endian machines. -// All the code below is copied from the lima project in https://github.com/lima-vm/lima/blob/v0.8.3/pkg/guestagent/procnettcp/procnettcp.go#L95-L137 +// ParseAddress parses the entry using the current host's native byte order. +// Use ParseAddressWithByteOrder to parse an entry whose byte order is known. +// The parsing logic is derived from the lima project in https://github.com/lima-vm/lima/blob/v0.8.3/pkg/guestagent/procnettcp/procnettcp.go#L95-L137 // and is licensed under the Apache License, Version 2.0 func ParseAddress(s string) (net.IP, uint16, error) { + return ParseAddressWithByteOrder(s, binary.NativeEndian) +} + +// ParseAddressWithByteOrder is like ParseAddress but takes the byte order of +// the /proc data explicitly. The kernel writes each 4-byte group of the address +// in the host's native byte order (little-endian on x86/arm64, big-endian on +// s390x), so each group is read with that order and rewritten in network order +// to build the net.IP. +func ParseAddressWithByteOrder(s string, order binary.ByteOrder) (net.IP, uint16, error) { split := strings.SplitN(s, ":", 2) if len(split) != 2 { return nil, 0, fmt.Errorf("unparsable address %q", s) @@ -92,13 +101,11 @@ func ParseAddress(s string) (net.IP, uint16, error) { ipBytes := make([]byte, len(split[0])/2) // 4 bytes (8 chars) or 16 bytes (32 chars) for i := 0; i < len(split[0])/8; i++ { quartet := split[0][8*i : 8*(i+1)] - quartetLE, err := hex.DecodeString(quartet) // surprisingly little endian, per 4 bytes + quartetBytes, err := hex.DecodeString(quartet) if err != nil { return nil, 0, fmt.Errorf("unparsable address %q: unparsable quartet %q: %w", s, quartet, err) } - for j := 0; j < len(quartetLE); j++ { - ipBytes[4*i+len(quartetLE)-1-j] = quartetLE[j] - } + binary.BigEndian.PutUint32(ipBytes[4*i:], order.Uint32(quartetBytes)) } ip := net.IP(ipBytes) diff --git a/pkg/portutil/procnet/procnetd_test.go b/pkg/portutil/procnet/procnetd_test.go index a6ff5dfa30a..c4cb53763f8 100644 --- a/pkg/portutil/procnet/procnetd_test.go +++ b/pkg/portutil/procnet/procnetd_test.go @@ -17,6 +17,7 @@ package procnet import ( + "encoding/binary" "net" "testing" @@ -67,3 +68,24 @@ func TestParseTCP6Zero(t *testing.T) { assert.Check(t, net.IPv6zero.Equal(entries[0].LocalIP)) assert.Equal(t, uint64(22), entries[0].LocalPort) } + +func TestParseAddressWithByteOrder(t *testing.T) { + // Big-endian hosts (e.g. s390x) keep each 4-byte group as written. + ip, port, err := ParseAddressWithByteOrder("7F000001:0050", binary.BigEndian) + assert.NilError(t, err) + assert.Check(t, net.ParseIP("127.0.0.1").Equal(ip)) + assert.Equal(t, uint16(80), port) + + ip, _, err = ParseAddressWithByteOrder("FE8000000000000070A657FFFE71C75D:0050", binary.BigEndian) + assert.NilError(t, err) + assert.Check(t, net.ParseIP("fe80::70a6:57ff:fe71:c75d").Equal(ip)) + + // Little-endian hosts (x86, arm64) reverse each 4-byte group. + ip, _, err = ParseAddressWithByteOrder("0100007F:0050", binary.LittleEndian) + assert.NilError(t, err) + assert.Check(t, net.ParseIP("127.0.0.1").Equal(ip)) + + ip, _, err = ParseAddressWithByteOrder("000080FE00000000FF57A6705DC771FE:0050", binary.LittleEndian) + assert.NilError(t, err) + assert.Check(t, net.ParseIP("fe80::70a6:57ff:fe71:c75d").Equal(ip)) +} From 8643977fb061f7cb74c081ea0a7065a74d030567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:33:36 +0000 Subject: [PATCH 721/868] build(deps): bump go.yaml.in/yaml/v3 from 3.0.4 to 3.0.5 Bumps [go.yaml.in/yaml/v3](https://github.com/yaml/go-yaml) from 3.0.4 to 3.0.5. - [Commits](https://github.com/yaml/go-yaml/compare/v3.0.4...v3.0.5) --- updated-dependencies: - dependency-name: go.yaml.in/yaml/v3 dependency-version: 3.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 170ecf5592d..335ec87c6fc 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( 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 + go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.54.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index dfeb92febd0..2a79719a344 100644 --- a/go.sum +++ b/go.sum @@ -350,8 +350,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= 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= From b3cf06ca9b1ddd0ab65e8195e0f3754414844400 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:32:25 +0000 Subject: [PATCH 722/868] build(deps): bump docker/login-action from 4.5.1 to 4.6.0 Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index a81b2ee0ab9..2d953070ac4 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -46,7 +46,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 32f1ec3dd1dfdfb2a247c4a8d0290d66b6bc80aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:33:08 +0000 Subject: [PATCH 723/868] build(deps): bump the docker group across 1 directory with 2 updates Bumps the docker group with 2 updates in the / directory: [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/moby/moby/client` from 0.5.0 to 0.5.1 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.5.1/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.5.0...v0.5.1) Updates `github.com/moby/moby/v2` from 2.0.0-beta.18 to 2.0.0-beta.19 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.18...v2.0.0-beta.19) --- updated-dependencies: - dependency-name: github.com/moby/moby/client dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 14 +++++++------- go.sum | 35 ++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 962a2cf4ce1..38ba78c9e1e 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.6.2+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.7.0 + github.com/docker/go-connections v0.8.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined @@ -43,8 +43,8 @@ require ( github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.1 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined - github.com/moby/moby/client v0.5.0 - github.com/moby/moby/v2 v2.0.0-beta.18 + github.com/moby/moby/client v0.5.1 + github.com/moby/moby/v2 v2.0.0-beta.19 github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.1 //gomodjail:unconfined @@ -63,7 +63,7 @@ require ( 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 + go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.54.0 golang.org/x/net v0.57.0 golang.org/x/sync v0.22.0 //gomodjail:unconfined @@ -80,7 +80,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/plugin v1.1.0 // indirect - github.com/containerd/ttrpc v1.2.8 // indirect + github.com/containerd/ttrpc v1.2.9 // indirect github.com/containers/ocicrypt v1.3.2 // indirect github.com/creack/pty v1.1.24 // indirect github.com/djherbis/times v1.6.0 // indirect @@ -133,8 +133,8 @@ require ( 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.37.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + golang.org/x/mod v0.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect //gomodjail:unconfined google.golang.org/grpc v1.82.1 // indirect //gomodjail:unconfined diff --git a/go.sum b/go.sum index 427dc95990a..4a161f872f1 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+XesH/6BBuJcdtV6ymGlGg= 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/ttrpc v1.2.9 h1:ha0ak962T0s3CA/RoZ6S6xiWZQF24GrBaEpiGX1uihg= +github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= 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= @@ -101,8 +101,8 @@ github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0= +github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -210,10 +210,10 @@ 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.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/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= +github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= +github.com/moby/moby/v2 v2.0.0-beta.19 h1:8TYdJyXCc2gX4hROLOz9RH6aZ2E9svyAAZA/C7gUrfk= +github.com/moby/moby/v2 v2.0.0-beta.19/go.mod h1:HHqv27j85UHTUl1Ne/nzqWE5T5E2lf6juGR6lIs8h2E= 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.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= @@ -274,8 +274,8 @@ 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.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= 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= @@ -350,8 +350,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= 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= @@ -374,8 +375,8 @@ 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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= 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= @@ -457,8 +458,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= @@ -468,8 +469,8 @@ 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-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/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/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= From 3b791c8b38f2085088c486c0815121946518d31c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:41:07 +0000 Subject: [PATCH 724/868] build(deps): bump github.com/docker/go-connections from 0.8.0 to 0.8.1 Bumps [github.com/docker/go-connections](https://github.com/docker/go-connections) from 0.8.0 to 0.8.1. - [Commits](https://github.com/docker/go-connections/compare/v0.8.0...v0.8.1) --- updated-dependencies: - dependency-name: github.com/docker/go-connections dependency-version: 0.8.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38ba78c9e1e..1c1c0ee3a27 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.6.2+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.8.0 + github.com/docker/go-connections v0.8.1 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4a161f872f1..7a8b2cc79a0 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0= -github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= +github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= From 00648d9a02d29e128bf9bb48eda1ac5931cb068e Mon Sep 17 00:00:00 2001 From: ankit090701 Date: Sat, 1 Aug 2026 19:22:10 +0530 Subject: [PATCH 725/868] fix: image ls with a bare repository name should match all tags `nerdctl image ls myapp`, where myapp is a bare repository name (no tag or digest), returned nothing unless myapp happened to have a `:latest` tag - unlike `docker image ls myapp`, which lists every tag of the repository. referenceutil.Parse normalizes a bare repository name by unconditionally applying distribution/reference's TagNameOnly, which appends an implicit ":latest" tag. listOptions then built an exact-match filter from that normalized reference (`name==docker.io/library/myapp:latest`), so any image tagged anything other than "latest" was silently excluded. Extract the filter-construction logic into a small nameFilterFor helper. When the argument named an explicit tag or digest, keep matching it exactly. When it was a bare repository name, build a `name~=^:` regex filter instead (escaping the repository name with regexp.QuoteMeta), so it matches any tag under that repository. This mirrors the `~=` regex-filter pattern already used elsewhere in the codebase for similar purposes, e.g. pkg/idutil/imagewalker/imagewalker.go and pkg/idutil/containerwalker/containerwalker.go. Added TestNameFilterFor covering a bare repository name (now matches any tag), an explicit tag, an explicit ":latest" tag, a digest, and a bare name with a domain that needs regex-escaping. Verified this test fails without the fix, reproducing the exact reported behaviour (name==docker.io/library/myapp:latest instead of a repo-wide match). Fixes #5113 Signed-off-by: ankit090701 --- cmd/nerdctl/image/image_list.go | 20 +++++++++++- cmd/nerdctl/image/image_list_test.go | 48 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/image/image_list.go b/cmd/nerdctl/image/image_list.go index a42337fba23..7776e0217df 100644 --- a/cmd/nerdctl/image/image_list.go +++ b/cmd/nerdctl/image/image_list.go @@ -18,6 +18,7 @@ package image import ( "fmt" + "regexp" "github.com/spf13/cobra" @@ -81,7 +82,7 @@ func listOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, er if err != nil { return nil, err } - filters = []string{fmt.Sprintf("name==%s", parsedReference)} + filters = nameFilterFor(parsedReference) } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { @@ -125,6 +126,23 @@ func listOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, er } +// nameFilterFor builds the containerd image-service filter(s) matching the +// argument to `nerdctl image ls [REPOSITORY[:TAG]]`. +// +// If the argument named an explicit tag or digest, it's matched exactly. +// Otherwise the argument was a bare repository name: referenceutil.Parse +// normalizes that to an implicit ":latest" tag (matching how most other +// reference-consuming commands resolve a bare name), but for listing +// purposes that would incorrectly hide every other tag of the repository - +// unlike `docker image ls`, which matches all tags of a bare repository +// name. Match any tag under the repository instead. +func nameFilterFor(parsedReference *referenceutil.ImageReference) []string { + if parsedReference.ExplicitTag != "" || parsedReference.Digest != "" { + return []string{fmt.Sprintf("name==%s", parsedReference)} + } + return []string{fmt.Sprintf("name~=^%s:", regexp.QuoteMeta(parsedReference.Name()))} +} + func imagesAction(cmd *cobra.Command, args []string) error { options, err := listOptions(cmd, args) if err != nil { diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index c204fb95852..38c1e486c54 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -38,6 +38,54 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) +// TestNameFilterFor is a regression test for +// https://github.com/containerd/nerdctl/issues/5113: `nerdctl image ls +// myapp`, where myapp is a bare repository name, returned nothing unless +// myapp had a `:latest` tag, because referenceutil.Parse normalizes a bare +// repository name to an implicit ":latest" tag and the resulting exact-match +// filter therefore only ever matched that one tag. +func TestNameFilterFor(t *testing.T) { + testCases := []struct { + name string + arg string + expected []string + }{ + { + name: "bare repository name matches any tag", + arg: "myapp", + expected: []string{`name~=^docker\.io/library/myapp:`}, + }, + { + name: "explicit tag matches exactly", + arg: "myapp:v1", + expected: []string{"name==docker.io/library/myapp:v1"}, + }, + { + name: "explicit latest tag matches exactly", + arg: "myapp:latest", + expected: []string{"name==docker.io/library/myapp:latest"}, + }, + { + name: "digest matches exactly", + arg: "myapp@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + expected: []string{"name==docker.io/library/myapp@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + }, + { + name: "bare repository name with domain and path is escaped for the regex", + arg: "registry.example.com/foo/my.app", + expected: []string{`name~=^registry\.example\.com/foo/my\.app:`}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parsedReference, err := referenceutil.Parse(tc.arg) + assert.NilError(t, err) + assert.DeepEqual(t, tc.expected, nameFilterFor(parsedReference)) + }) + } +} + func TestImages(t *testing.T) { nerdtest.Setup() From 99ad6bdd01a965c05de5ca20bc252a2ad531f0b4 Mon Sep 17 00:00:00 2001 From: Nikolaus Schuetz Date: Sat, 1 Aug 2026 12:33:39 -0700 Subject: [PATCH 726/868] Add tests for identifiers.ValidateDockerCompat pkg/identifiers had no test coverage, though ValidateDockerCompat gates container, volume, and network names across the codebase. Add table-driven tests for the accepted charset, the Docker-compatible two-character minimum, leading-separator and invalid-character rejection, and the empty case, asserting both the message and the wrapped errdefs.ErrInvalidArgument. Signed-off-by: Nikolaus Schuetz --- pkg/identifiers/validate_test.go | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pkg/identifiers/validate_test.go diff --git a/pkg/identifiers/validate_test.go b/pkg/identifiers/validate_test.go new file mode 100644 index 00000000000..61262b5ed44 --- /dev/null +++ b/pkg/identifiers/validate_test.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package identifiers + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/errdefs" +) + +func TestValidateDockerCompat(t *testing.T) { + tests := []struct { + name string + input string + wantErr string + }{ + {name: "two alphanumeric", input: "ab"}, + {name: "digits", input: "12"}, + {name: "with underscore", input: "my_container"}, + {name: "with dash and dot", input: "my-container.1"}, + {name: "mixed separators", input: "A.b_c-2"}, + {name: "empty", input: "", wantErr: "identifier must not be empty"}, + {name: "single character", input: "a", wantErr: "must match pattern"}, + {name: "leading underscore", input: "_ab", wantErr: "must match pattern"}, + {name: "leading dash", input: "-ab", wantErr: "must match pattern"}, + {name: "leading dot", input: ".ab", wantErr: "must match pattern"}, + {name: "contains space", input: "a b", wantErr: "must match pattern"}, + {name: "contains slash", input: "a/b", wantErr: "must match pattern"}, + {name: "contains colon", input: "a:b", wantErr: "must match pattern"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := ValidateDockerCompat(tc.input) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + assert.ErrorIs(t, err, errdefs.ErrInvalidArgument) + return + } + assert.NilError(t, err) + }) + } +} From c55fd084c90758080358927c32925bdd3a799249 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Sat, 1 Aug 2026 18:08:32 +0800 Subject: [PATCH 727/868] feature: support to print snapshot info for image Signed-off-by: ningmingxiao --- pkg/imageinspector/imageinspector.go | 13 +++++++++++++ pkg/inspecttypes/native/image.go | 2 ++ 2 files changed, 15 insertions(+) diff --git a/pkg/imageinspector/imageinspector.go b/pkg/imageinspector/imageinspector.go index ce1f0003f05..8774960ca43 100644 --- a/pkg/imageinspector/imageinspector.go +++ b/pkg/imageinspector/imageinspector.go @@ -19,6 +19,8 @@ package imageinspector import ( "context" + "github.com/opencontainers/image-spec/identity" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/snapshots" @@ -56,6 +58,17 @@ func Inspect(ctx context.Context, client *containerd.Client, image images.Image, } else { n.ImageConfigDesc = imageConfigDesc n.ImageConfig = imageConfig + chainIDs := identity.ChainIDs(imageConfig.RootFS.DiffIDs) + snapshots := make([]snapshots.Info, len(chainIDs)) + for i, id := range chainIDs { + snapInfo, err := snapshotter.Stat(ctx, id.String()) + if err == nil { + snapshots[i] = snapInfo + } else { + log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to get snapshot %s info", id.String()) + } + } + n.Snapshots = snapshots } n.Size, err = imgutil.UnpackedImageSize(ctx, snapshotter, img) if err != nil { diff --git a/pkg/inspecttypes/native/image.go b/pkg/inspecttypes/native/image.go index d7e1ac388d9..7e83d6c3e71 100644 --- a/pkg/inspecttypes/native/image.go +++ b/pkg/inspecttypes/native/image.go @@ -20,6 +20,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/snapshots" ) // Image corresponds to a containerd-native image object. @@ -34,4 +35,5 @@ type Image struct { ImageConfigDesc ocispec.Descriptor `json:"ImageConfigDesc"` ImageConfig ocispec.Image `json:"ImageConfig"` Size int64 `json:"size"` + Snapshots []snapshots.Info `json:"Snapshots,omitempty"` } From 7e915a08fe7bc35ba47b4fe3b2dae09b242d5e7f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 2 Aug 2026 05:17:24 +0900 Subject: [PATCH 728/868] CI: drop ppa:criu/ppa Signed-off-by: Akihiro Suda --- .github/workflows/job-test-in-host.yml | 1 - .github/workflows/job-test-unit.yml | 1 - hack/provisioning/linux/test-integration-env.sh | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 5ddcc4616d2..a610041c83a 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -192,7 +192,6 @@ jobs: # FIXME: remove expect when we are done removing unbuffer from tests echo "::group:: installing test dependencies" - sudo add-apt-repository ppa:criu/ppa -y sudo apt-get install -qq expect criu echo "::endgroup::" diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 0c24f49fa23..72f2b81c96c 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -78,7 +78,6 @@ jobs: elif [ "$RUNNER_OS" == "Linux" ]; then ./hack/provisioning/linux/cni.sh install "${INPUTS_LINUX_CNI_VERSION}" "amd64" "${INPUTS_LINUX_CNI_SHA}" sudo apt-get update -qq - sudo add-apt-repository ppa:criu/ppa -y sudo apt-get install -qq criu fi env: diff --git a/hack/provisioning/linux/test-integration-env.sh b/hack/provisioning/linux/test-integration-env.sh index 586b3836a59..3974c0f5ff5 100755 --- a/hack/provisioning/linux/test-integration-env.sh +++ b/hack/provisioning/linux/test-integration-env.sh @@ -44,7 +44,8 @@ host::packages(){ # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing # `jq` is required to generate test summaries apt-get update -qq >/dev/null - add-apt-repository -y ppa:criu/ppa >/dev/null + # Ubuntu 22.04 needs PPA version of CRIU + grep -q UBUNTU_CODENAME=jammy /etc/os-release && add-apt-repository -y ppa:criu/ppa >/dev/null apt-get install -qq --no-install-recommends \ apparmor \ criu \ From 4d79416b8af6e334b71944d4e34901eae1983560 Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Sun, 2 Aug 2026 09:36:48 +0800 Subject: [PATCH 729/868] feature: support to print snapshot info for container Signed-off-by: ningmingxiao --- pkg/cmd/container/inspect.go | 9 +++++++++ pkg/inspecttypes/native/container.go | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/container/inspect.go b/pkg/cmd/container/inspect.go index f9cdb18308a..e5c2178ffc6 100644 --- a/pkg/cmd/container/inspect.go +++ b/pkg/cmd/container/inspect.go @@ -23,6 +23,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -94,6 +95,14 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker. switch x.mode { case "native": + if n.SnapshotKey != "" { + info, err := x.snapshotter.Stat(ctx, n.SnapshotKey) + if err != nil { + log.G(ctx).WithError(err).Warnf("failed to get snapshot %s info", n.SnapshotKey) + } else { + n.SnapshotInfo = &info + } + } x.entries = append(x.entries, n) case "dockercompat": d, err := dockercompat.ContainerFromNative(n) diff --git a/pkg/inspecttypes/native/container.go b/pkg/inspecttypes/native/container.go index 1bd421a2d62..fd429124ca2 100644 --- a/pkg/inspecttypes/native/container.go +++ b/pkg/inspecttypes/native/container.go @@ -21,6 +21,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" + "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/go-cni" ) @@ -28,8 +29,9 @@ import ( // Not compatible with `docker container inspect`. type Container struct { containers.Container - Spec interface{} `json:"Spec,omitempty"` - Process *Process `json:"Process,omitempty"` + Spec interface{} `json:"Spec,omitempty"` + Process *Process `json:"Process,omitempty"` + SnapshotInfo *snapshots.Info `json:"SnapshotInfo,omitempty"` } type Process struct { From 4dc56753b74d8e365cb5466b6cd9dd44b02bc4fa Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Wed, 22 Jul 2026 21:55:50 +0300 Subject: [PATCH 730/868] feat(image): adopt Docker v29 output for images list Make the default `nerdctl images` output match Docker v29: a collapsed view with IMAGE, ID, DISK USAGE, CONTENT SIZE and EXTRA columns, an "In Use" (U) indicator, and for dangling images. Multi-platform images are collapsed into a single row with aggregated disk and content size. Like Docker, the "In Use" legend is only printed when the output is a terminal, so piped and redirected output stays clean, and rows are ordered by image reference with untagged images last rather than by creation time. In-use is resolved by image target digest, the way Docker matches containers to images, so every reference to a used target is flagged, not only the one the container was created from. The new view is used only for the bare command. Passing --format, --quiet, --no-trunc, --digests or --names falls back to the legacy table (REPOSITORY, TAG, IMAGE ID, CREATED, PLATFORM, SIZE, BLOB SIZE), so existing scripts and templates keep working, including their creation-time ordering. This mirrors Docker's own shouldUseTree fallback. Since the default output now matches Docker, the images tests also run against the Docker target; only the nerdctl-specific --names subtest stays gated. Tests that assert on the default `images` output for untagged images (image prune/remove and build-without-tag) are updated to expect . The expanded per-platform `--tree` view is left for a follow-up. Closes #5027 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/builder/builder_build_test.go | 8 +- cmd/nerdctl/image/image_list.go | 11 +- cmd/nerdctl/image/image_list_test.go | 89 +++++++---- cmd/nerdctl/image/image_prune_test.go | 8 +- cmd/nerdctl/image/image_remove_test.go | 6 +- docs/command-reference.md | 9 +- pkg/cmd/image/list.go | 178 ++++++++++++++++++++-- pkg/cmd/image/list_test.go | 77 ++++++++++ 8 files changed, 328 insertions(+), 58 deletions(-) create mode 100644 pkg/cmd/image/list_test.go diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 84a0d17f860..3d69983e1cc 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -671,12 +671,8 @@ CMD ["echo", "nerdctl-build-test-string"] }, Command: test.Command("images", "--all"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - // TODO: follow Docker v29 behavior (change to ) https://github.com/containerd/nerdctl/issues/5027 - noTag := "" - if nerdtest.IsDocker() { - noTag = "" - } - return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(noTag))(data, helpers) + // The Docker v29 default view renders untagged images as . + return test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(""))(data, helpers) }, } diff --git a/cmd/nerdctl/image/image_list.go b/cmd/nerdctl/image/image_list.go index a42337fba23..1b8e44a6309 100644 --- a/cmd/nerdctl/image/image_list.go +++ b/cmd/nerdctl/image/image_list.go @@ -33,11 +33,18 @@ func ImagesCommand() *cobra.Command { shortHelp := "List images" longHelp := shortHelp + ` -Properties: +By default (Docker v29 compatible view) the following columns are shown: +- IMAGE: Image reference ("repository:tag", "repository@digest", or "") +- ID: OCI digest of the image target (index/manifest), shared for multi-platform images. Matches Docker's ID with the containerd image store (differs from the legacy graphdriver image ID). +- DISK USAGE: Total on-disk size: content store blobs plus the unpacked snapshots +- CONTENT SIZE: Size of the blobs (such as layer tarballs) in the content store +- EXTRA: Flags for the image; "U" means the image is in use by a container + +Passing --format, --quiet, --no-trunc, --digests or --names falls back to the legacy table: - REPOSITORY: Repository - TAG: Tag - NAME: Name of the image, --names for skip parsing as repository and tag. -- IMAGE ID: OCI Digest. Usually different from Docker image ID. Shared for multi-platform images. +- IMAGE ID: OCI digest of the image target (index/manifest), shared for multi-platform images. Matches Docker's ID with the containerd image store (differs from the legacy graphdriver image ID). - CREATED: Created time - PLATFORM: Platform - SIZE: Size of the unpacked snapshots diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index c204fb95852..484b2fb8ab4 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -44,7 +44,6 @@ func TestImages(t *testing.T) { commonImage, _ := referenceutil.Parse(testutil.CommonImage) testCase := &test.Case{ - Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", commonImage.String()) helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) @@ -58,18 +57,13 @@ func TestImages(t *testing.T) { Output: func(stdout string, t tig.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") - header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" - if nerdtest.IsDocker() { - header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" - } - tab := tabutil.NewReader(header) + tab := tabutil.NewReader("IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA") err := tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") found := false for _, line := range lines[1:] { - repo, _ := tab.ReadRow(line, "REPOSITORY") - tag, _ := tab.ReadRow(line, "TAG") - if repo+":"+tag == commonImage.FamiliarName()+":"+commonImage.Tag { + image, _ := tab.ReadRow(line, "IMAGE") + if image == commonImage.FamiliarName()+":"+commonImage.Tag { found = true break } @@ -81,7 +75,9 @@ func TestImages(t *testing.T) { }, { Description: "With names", - Command: test.Command("images", "--names", commonImage.String()), + // --names is a nerdctl-specific flag; Docker does not support it. + Require: require.Not(nerdtest.Docker), + Command: test.Command("images", "--names", commonImage.String()), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( @@ -122,14 +118,52 @@ func TestImages(t *testing.T) { } }, }, + { + Description: "In use", + Setup: func(data test.Data, helpers test.Helpers) { + // Tag the image under a second name: in-use is resolved by target digest, so + // every reference to that target must be flagged, not just the one the + // container was created from. + helpers.Ensure("tag", commonImage.String(), data.Identifier()+":alias") + helpers.Ensure("run", "-d", "--quiet", "--name", data.Identifier(), commonImage.String(), "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", data.Identifier()+":alias") + }, + Command: test.Command("images"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") + tab := tabutil.NewReader("IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "ParseHeader should not fail\n") + // Docker collapses all the names of an image into a single row, while + // nerdctl has one row per image record, so just require that every row + // referring to that target is marked. + found := 0 + for _, line := range lines[1:] { + image, _ := tab.ReadRow(line, "IMAGE") + if image != commonImage.FamiliarName()+":"+commonImage.Tag && + image != data.Identifier()+":alias" { + continue + } + extra, _ := tab.ReadRow(line, "EXTRA") + assert.Equal(t, extra, "U", "the in-use image should be marked with U: "+image) + found++ + } + assert.Assert(t, found > 0, "we should have found the in-use image\n") + }, + } + }, + }, }, } if runtime.GOOS == "windows" { - testCase.Require = require.All( - testCase.Require, - nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524"), - ) + testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524") } testCase.Run(t) @@ -316,18 +350,14 @@ CMD ["echo", "nerdctl-build-notag-string"] Description: "dangling", Command: test.Command("images", "--filter", "dangling=true"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - // TODO: follow Docker v29 behavior (change to ) https://github.com/containerd/nerdctl/issues/5027 - dangling := "" - if nerdtest.IsDocker() { - dangling = "" - } - return test.Expects(0, nil, expect.Contains(dangling))(data, helpers) + // The Docker v29 default view (used here, no --format) renders dangling images as . + return test.Expects(0, nil, expect.Contains(""))(data, helpers) }, }, { Description: "not dangling", Command: test.Command("images", "--filter", "dangling=false"), - Expected: test.Expects(0, nil, expect.DoesNotContain("")), + Expected: test.Expects(0, nil, expect.DoesNotContain("", "")), }, }, } @@ -355,20 +385,15 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { var imageID string var skipLine int lines := strings.Split(strings.TrimSpace(stdout), "\n") - header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" - if nerdtest.IsDocker() { - header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" - } - tab := tabutil.NewReader(header) + tab := tabutil.NewReader("IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA") err := tab.ParseHeader(lines[0]) assert.NilError(t, err, "ParseHeader should not fail\n") found := true for i, line := range lines[1:] { - repo, _ := tab.ReadRow(line, "REPOSITORY") - tag, _ := tab.ReadRow(line, "TAG") - if repo+":"+tag == testutil.BusyboxImage { + image, _ := tab.ReadRow(line, "IMAGE") + if image == testutil.BusyboxImage { skipLine = i - imageID, _ = tab.ReadRow(line, "IMAGE ID") + imageID, _ = tab.ReadRow(line, "ID") break } } @@ -376,7 +401,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { if i == skipLine { continue } - id, _ := tab.ReadRow(line, "IMAGE ID") + id, _ := tab.ReadRow(line, "ID") if id == imageID { found = false break @@ -392,7 +417,7 @@ func TestImagesKubeWithKubeHideDupe(t *testing.T) { Command: test.Command("images"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(""), + Output: expect.Contains(""), } }, }, diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index ca3cbf62e95..e5abb5dc505 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -77,7 +77,7 @@ func TestImagePrune(t *testing.T) { // Swapping order does not change anything. helpers.Ensure("build", "-t", identifier, buildCtx) imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier) }, Command: test.Command("image", "prune", "--force"), @@ -90,7 +90,7 @@ func TestImagePrune(t *testing.T) { }, func(stdout string, t tig.T) { imgList := helpers.Capture("images") - assert.Assert(t, !strings.Contains(imgList, ""), imgList) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) assert.Assert(t, strings.Contains(imgList, identifier)) }, ), @@ -122,7 +122,7 @@ func TestImagePrune(t *testing.T) { helpers.Ensure("build", buildCtx) helpers.Ensure("build", "-t", identifier, buildCtx) imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier) helpers.Ensure("run", "--name", identifier, identifier) }, @@ -136,7 +136,7 @@ func TestImagePrune(t *testing.T) { func(stdout string, t tig.T) { imgList := helpers.Capture("images") assert.Assert(t, strings.Contains(imgList, data.Identifier())) - assert.Assert(t, !strings.Contains(imgList, ""), imgList) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) helpers.Ensure("rm", "-f", data.Identifier()) removed := helpers.Capture("image", "prune", "--force", "--all") assert.Assert(t, strings.Contains(removed, data.Identifier())) diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index d0b5f25ea0c..1ad1cee6a30 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -143,7 +143,7 @@ func TestRemove(t *testing.T) { Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ - Output: expect.Contains(""), + Output: expect.Contains(""), }) }, } @@ -249,7 +249,7 @@ func TestRemove(t *testing.T) { Errors: []error{}, Output: func(stdout string, t tig.T) { helpers.Command("images").Run(&test.Expected{ - Output: expect.Contains(""), + Output: expect.Contains(""), }) }, } @@ -374,7 +374,7 @@ func TestRemoveKubeWithKubeHideDupe(t *testing.T) { ) testCase.SubTests = []*test.Case{ { - Description: "After removing the tag without kube-hide-dupe, repodigest is shown as ", + Description: "After removing the tag without kube-hide-dupe, repodigest is shown as ", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.BusyboxImage) diff --git a/docs/command-reference.md b/docs/command-reference.md index 9e32e1d8adf..9ab8ed7f855 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -833,7 +833,12 @@ support zstdchunked convert List images -:warning: The image ID is usually different from Docker image ID. +:warning: The image ID is the OCI digest of the image target (index/manifest). It matches Docker's ID with the containerd image store, but differs from the legacy graphdriver image ID (config digest). + +By default (Docker v29 compatible view) the columns are `IMAGE`, `ID`, `DISK USAGE`, +`CONTENT SIZE` and `EXTRA` (where `U` means the image is in use by a container). +Passing `--format`, `--quiet`, `--no-trunc`, `--digests` or `--names` falls back to the +legacy table (`REPOSITORY`, `TAG`, `IMAGE ID`, `CREATED`, `PLATFORM`, `SIZE`, `BLOB SIZE`). Usage: `nerdctl images [OPTIONS] [REPOSITORY[:TAG]]` @@ -843,7 +848,7 @@ Flags: - :whale: `-q, --quiet`: Only show numeric IDs - :whale: `--no-trunc`: Don't truncate output - :whale: `--format`: Format the output using the given Go template - - :whale: `--format=table` (default): Table + - :whale: `--format=table`: Legacy table (default is the Docker v29 compatible view) - :whale: `--format='{{json .}}'`: JSON - :nerd_face: `--format=wide`: Wide table - :nerd_face: `--format=json`: Alias of `--format='{{json .}}'` diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go index 0476b6b55fb..f87fce0a272 100644 --- a/pkg/cmd/image/list.go +++ b/pkg/cmd/image/list.go @@ -23,16 +23,19 @@ import ( "errors" "fmt" "io" + "os" "sort" "strings" "text/tabwriter" "text/template" "time" + "unicode/utf8" "github.com/docker/go-units" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/identity" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/term" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/content" @@ -174,9 +177,19 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima if options.Format == "wide" { digestsFlag = true } + // newView selects the Docker v29 default output (IMAGE, ID, DISK USAGE, CONTENT SIZE, EXTRA). + // Any "old-view" signal (--format, --quiet, --no-trunc, --digests, --names) falls back to the + // legacy table, mirroring Docker's tree-view fallback. + newView := options.Format == "" && !options.Quiet && !options.NoTrunc && !options.Digests && !options.Names var tmpl *template.Template - switch options.Format { - case "", "table", "wide": + switch { + case newView: + // The legend is written to the underlying writer before it is wrapped in the tabwriter, + // so it is not aligned to the columns below. Like Docker, it is only shown on a terminal. + printImagesLegend(w) + w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) + fmt.Fprintln(w, "IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA") + case options.Format == "", options.Format == "table", options.Format == "wide": w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) if !options.Quiet { printHeader := "" @@ -191,7 +204,7 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima printHeader += "IMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" fmt.Fprintln(w, printHeader) } - case "raw": + case options.Format == "raw": return errors.New("unsupported format: \"raw\"") default: if options.Quiet { @@ -204,12 +217,22 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima } } + // In-use detection requires a container scan, so only pay for it in the new view where the + // EXTRA column is rendered. + var inUse map[digest.Digest]bool + if newView { + inUse = imagesInUse(ctx, client) + sortByImageRef(finalImageList) + } + printer := &imagePrinter{ w: w, quiet: options.Quiet, noTrunc: options.NoTrunc, digestsFlag: digestsFlag, namesFlag: options.Names, + newView: newView, + inUse: inUse, tmpl: tmpl, client: client, provider: containerdutil.NewProvider(client), @@ -228,12 +251,13 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima } type imagePrinter struct { - w io.Writer - quiet, noTrunc, digestsFlag, namesFlag bool - tmpl *template.Template - client *containerd.Client - provider content.Provider - snapshotter snapshots.Snapshotter + w io.Writer + quiet, noTrunc, digestsFlag, namesFlag, newView bool + inUse map[digest.Digest]bool // image target -> referenced by at least one container + tmpl *template.Template + client *containerd.Client + provider content.Provider + snapshotter snapshots.Snapshotter } type image struct { @@ -347,6 +371,10 @@ func (x *imagePrinter) printImage(ctx context.Context, img images.Image) error { return err } + if x.newView { + return x.printImageCollapsed(img, candidateImages) + } + for platform, desc := range candidateImages { if err := x.printImageSinglePlatform(*desc.config, img, desc.blobSize, desc.size, desc.platform); err != nil { log.G(ctx).WithError(err).Debugf("failed to get platform %q of image %q", platform, img.Name) @@ -424,6 +452,138 @@ func (x *imagePrinter) printImageSinglePlatform(desc ocispec.Descriptor, img ima return nil } +// printImageCollapsed renders a single row in the Docker v29 default view (IMAGE, ID, DISK USAGE, +// CONTENT SIZE, EXTRA), aggregating disk and content size across the platforms present in the +// content store. +func (x *imagePrinter) printImageCollapsed(img images.Image, candidateImages map[string]*image) error { + var totalSnapshotSize, totalContentSize int64 + for _, candidate := range candidateImages { + totalSnapshotSize += candidate.size + totalContentSize += candidate.blobSize + } + + // Match Docker's tree view semantics (api/types/image.ManifestSummary.Size): + // CONTENT SIZE is the content-store blobs, and DISK USAGE ("Total") is the content plus the + // unpacked snapshots. Docker formats both with 3-significant-digit precision. + diskUsage := units.HumanSizeWithPrecision(float64(totalContentSize+totalSnapshotSize), 3) + contentSize := units.HumanSizeWithPrecision(float64(totalContentSize), 3) + + // The new view always truncates the ID (it never coexists with --no-trunc). + id := img.Target.Digest.String() + if _, hex, ok := strings.Cut(id, ":"); ok && len(hex) >= 12 { + id = hex[:12] + } + + extra := "" + if x.inUse[img.Target.Digest] { + extra = "U" + } + + _, err := fmt.Fprintf(x.w, "%s\t%s\t%s\t%s\t%s\n", + newViewImageRef(img.Name), + id, + diskUsage, + contentSize, + extra, + ) + return err +} + +// printImagesLegend writes the right-aligned "In Use" legend for the Docker v29 default view. +// Matching Docker, it is only emitted when the output is a terminal with a known width, so it +// never pollutes piped or redirected output. +func printImagesLegend(w io.Writer) { + f, ok := w.(*os.File) + if !ok { + return + } + width, _, err := term.GetSize(int(f.Fd())) + if err != nil || width <= 0 { + return + } + legend := "i Info → U In Use" + if pad := width - utf8.RuneCountInString(legend); pad > 0 { + legend = strings.Repeat(" ", pad) + legend + } + fmt.Fprintln(w, legend) +} + +// untaggedImageRef is what the Docker v29 view shows in the IMAGE column for dangling images. +const untaggedImageRef = "" + +// sortByImageRef orders the images the way Docker v29 orders its collapsed view: lexicographically +// by the rendered IMAGE column, with untagged images last. The legacy table keeps its own +// creation-time ordering, so this is only applied to the new view. +func sortByImageRef(imageList []images.Image) { + refs := make(map[string]string, len(imageList)) + for _, img := range imageList { + refs[img.Name] = newViewImageRef(img.Name) + } + sort.SliceStable(imageList, func(i, j int) bool { + a, b := refs[imageList[i].Name], refs[imageList[j].Name] + if (a == untaggedImageRef) != (b == untaggedImageRef) { + return b == untaggedImageRef + } + return a < b + }) +} + +// newViewImageRef builds the IMAGE column for the Docker v29 default view: "repo:tag" for tagged +// images, "repo@digest" for images pulled by digest, or "" for dangling images. +func newViewImageRef(name string) string { + parsed, err := referenceutil.Parse(name) + if err != nil { + return untaggedImageRef + } + familiar := parsed.FamiliarName() + if familiar == "" { + return untaggedImageRef + } + if parsed.Tag != "" { + return familiar + ":" + parsed.Tag + } + // A tag-less reference is shown as "repo@digest" only when it carries an explicit registry + // domain (a real pulled-by-digest image). Dangling build artifacts have a synthetic, + // domain-less name (e.g. "@sha256:..." or "@sha256:..." depending on the + // builder) and are rendered as "", matching Docker. + if parsed.Digest != "" && referenceHasDomain(name) { + return familiar + "@" + parsed.Digest.String() + } + return untaggedImageRef +} + +// referenceHasDomain reports whether the raw image reference includes an explicit registry +// domain, using the same heuristic as distribution/reference: the component before the first +// "/" is a domain when it is "localhost" or contains a "." or ":". +func referenceHasDomain(name string) bool { + host, _, ok := strings.Cut(name, "/") + if !ok { + return false + } + return host == "localhost" || strings.ContainsAny(host, ".:") +} + +// imagesInUse returns the set of image target digests that are referenced by at least one +// container (in any state), used to render the Docker v29 "In Use" (U) indicator. Docker matches +// containers to images by digest, so every name pointing at the same target is flagged, not just +// the one the container was created from. +func imagesInUse(ctx context.Context, client *containerd.Client) map[digest.Digest]bool { + inUse := map[digest.Digest]bool{} + containerList, err := client.Containers(ctx) + if err != nil { + log.G(ctx).WithError(err).Warn("failed to list containers for image in-use detection") + return inUse + } + for _, container := range containerList { + image, err := container.Image(ctx) + if err != nil { + continue + } + inUse[image.Target().Digest] = true + } + return inUse +} + func isAttestationManifestDescriptor(desc ocispec.Descriptor) bool { const manifestReferenceType = "vnd.docker.reference.type" const attestationManifest = "attestation-manifest" diff --git a/pkg/cmd/image/list_test.go b/pkg/cmd/image/list_test.go new file mode 100644 index 00000000000..da83f8fc769 --- /dev/null +++ b/pkg/cmd/image/list_test.go @@ -0,0 +1,77 @@ +/* + 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" + + "gotest.tools/v3/assert" + + "github.com/containerd/containerd/v2/core/images" +) + +func TestNewViewImageRef(t *testing.T) { + const digest = "sha256:09538a1f51d3ec5af0449a1640937dfdf79b0e9b8c4da5b8a883086d5c1492ef" + testCases := []struct { + name string + expected string + }{ + {"docker.io/library/hello-world:latest", "hello-world:latest"}, + {"docker.io/moby/buildkit:buildx-stable-1", "moby/buildkit:buildx-stable-1"}, + {"ghcr.io/stargz-containers/alpine:3.13", "ghcr.io/stargz-containers/alpine:3.13"}, + // pulled by digest (has an explicit registry domain, no tag) -> repo@digest + {"docker.io/library/hello-world@" + digest, "hello-world@" + digest}, + {"ghcr.io/stargz-containers/alpine@" + digest, "ghcr.io/stargz-containers/alpine@" + digest}, + // dangling build artifacts: domain-less name with a digest -> untagged + {"@" + digest, ""}, + {"overlayfs@" + digest, ""}, + // bare config digest as name (created by the CRI plugin) -> untagged + {digest, ""}, + // unparsable / empty name -> untagged + {"", ""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, newViewImageRef(tc.name), tc.expected) + }) + } +} + +func TestSortByImageRef(t *testing.T) { + const digest = "sha256:09538a1f51d3ec5af0449a1640937dfdf79b0e9b8c4da5b8a883086d5c1492ef" + imageList := []images.Image{ + {Name: "@" + digest}, + {Name: "docker.io/library/nginx:alpine"}, + {Name: "ghcr.io/stargz-containers/alpine:3.13"}, + {Name: "overlayfs@" + digest}, + {Name: "docker.io/library/alpine:latest"}, + } + sortByImageRef(imageList) + // Ordering is on the rendered IMAGE column (the familiar name), like Docker, so + // "docker.io/library/nginx:alpine" sorts as "nginx:alpine". + expected := []string{ + "docker.io/library/alpine:latest", + "ghcr.io/stargz-containers/alpine:3.13", + "docker.io/library/nginx:alpine", + // untagged images come last, in their original order + "@" + digest, + "overlayfs@" + digest, + } + for i, img := range imageList { + assert.Equal(t, img.Name, expected[i]) + } +} From 4b7008bcc6cb6c477fe952453ea3416e72d1675e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:32:40 +0000 Subject: [PATCH 731/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.6.2+incompatible to 29.7.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.6.2...v29.7.1) Updates `github.com/moby/moby/v2` from 2.0.0-beta.19 to 2.0.0-beta.21 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.19...v2.0.0-beta.21) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.7.1+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index becc7e80834..0a73b89966c 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.6.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.7.1+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -44,7 +44,7 @@ require ( github.com/klauspost/compress v1.19.1 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined github.com/moby/moby/client v0.5.1 - github.com/moby/moby/v2 v2.0.0-beta.19 + github.com/moby/moby/v2 v2.0.0-beta.21 github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 53aeab8ff3e..721a0ae3e69 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,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.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= -github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.7.1+incompatible h1:ILZpP6B7fedIr6ANy824QkDp1WMJuouIq0O2SrBkB2w= +github.com/docker/cli v29.7.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= @@ -212,8 +212,8 @@ 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.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= -github.com/moby/moby/v2 v2.0.0-beta.19 h1:8TYdJyXCc2gX4hROLOz9RH6aZ2E9svyAAZA/C7gUrfk= -github.com/moby/moby/v2 v2.0.0-beta.19/go.mod h1:HHqv27j85UHTUl1Ne/nzqWE5T5E2lf6juGR6lIs8h2E= +github.com/moby/moby/v2 v2.0.0-beta.21 h1:LrUr8ocwGt3nOdPRKmLMKRRPqYqKJP4VbZxT2vZwscs= +github.com/moby/moby/v2 v2.0.0-beta.21/go.mod h1:Myh7qqKNMQ1bdk8kRugUA/xhlEUIw/drvN/h0atw4y0= 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.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= From 1e3a4aa29412ab09c5334e9d0bba0eec210e71b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:33:02 +0000 Subject: [PATCH 732/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.13.0 to 2.14.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.13.0...v2.14.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index becc7e80834..764b62af576 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.3 - github.com/compose-spec/compose-go/v2 v2.13.0 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.14.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.3 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 53aeab8ff3e..410b2a23d52 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE= -github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ= +github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= From 5432faaec9ac7ec56282d27cf7bd2bb43040bdbc Mon Sep 17 00:00:00 2001 From: Subota Ivan <73706465+subotac@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:01:25 +0300 Subject: [PATCH 733/868] test: clean up compose run containers with compose rm Signed-off-by: Subota Ivan <73706465+subotac@users.noreply.github.com> --- cmd/nerdctl/compose/compose_run_linux_test.go | 88 +++++++++---------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 2f05e732fd8..d1c7825391f 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -19,6 +19,7 @@ package compose import ( "fmt" "io" + "os" "path/filepath" "strconv" "strings" @@ -38,6 +39,28 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) +func composeRunCleanup() test.Butler { + return func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Path("compose.yaml") + // Tigron runs cleanup before setup too. A fresh temp project has no + // manifest or resources yet, so avoid waiting for the global compose lock. + if _, err := os.Stat(composePath); os.IsNotExist(err) { + return + } + // A background compose run holds the global compose lock. Stop its exact + // test container first so the process exits before compose rm acquires it. + helpers.Anyhow("stop", data.Identifier()) + helpers.Anyhow("compose", "-f", composePath, "rm", "-f", "-s", "-v") + // Docker Compose excludes one-off containers from `compose rm`, while + // nerdctl Compose selects every container with the project and service labels. + // Remove the explicit `compose run --name` container in compatibility runs. + if nerdtest.IsDocker() { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + } + helpers.Anyhow("compose", "-f", composePath, "down", "-v") + } +} + func TestComposeRun(t *testing.T) { const expectedOutput = "speed 38400 baud" @@ -71,10 +94,7 @@ services: return cmd }, 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") - }, + Cleanup: composeRunCleanup(), }, { Description: "pty run with --rm", @@ -104,10 +124,7 @@ services: Output: expect.Contains(expectedOutput), } }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", "-v", data.Identifier()) - helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") - }, + Cleanup: composeRunCleanup(), }, } @@ -116,6 +133,9 @@ services: func TestComposeRunWithServicePorts(t *testing.T) { testCase := nerdtest.Setup() + // A background compose run holds the global compose lock until cleanup. + testCase.NoParallel = true + cleanup := composeRunCleanup() testCase.Setup = func(data test.Data, helpers test.Helpers) { hostPort, err := portlock.Acquire(0) @@ -139,8 +159,6 @@ services: data.Labels().Set("composeYAML", composePath) data.Labels().Set("hostPort", strconv.Itoa(hostPort)) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. cmd := helpers.Command("compose", "-f", composePath, "run", "--service-ports", "--name", data.Identifier(), "web") cmd.WithPseudoTTY() cmd.Background() @@ -148,10 +166,7 @@ services: } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", "-v", data.Identifier()) - if composeYAML := data.Labels().Get("composeYAML"); composeYAML != "" { - helpers.Anyhow("compose", "-f", composeYAML, "down", "-v") - } + cleanup(data, helpers) if portStr := data.Labels().Get("hostPort"); portStr != "" { if port, err := strconv.Atoi(portStr); err == nil { _ = portlock.Release(port) @@ -183,6 +198,9 @@ services: func TestComposeRunWithPublish(t *testing.T) { testCase := nerdtest.Setup() + // A background compose run holds the global compose lock until cleanup. + testCase.NoParallel = true + cleanup := composeRunCleanup() testCase.Setup = func(data test.Data, helpers test.Helpers) { hostPort, err := portlock.Acquire(0) @@ -204,8 +222,6 @@ services: data.Labels().Set("composeYAML", composePath) data.Labels().Set("hostPort", strconv.Itoa(hostPort)) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. cmd := helpers.Command("compose", "-f", composePath, "run", "--publish", fmt.Sprintf("%d:80", hostPort), "--name", data.Identifier(), "web") cmd.WithPseudoTTY() cmd.Background() @@ -213,10 +229,7 @@ services: } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", "-v", data.Identifier()) - if composeYAML := data.Labels().Get("composeYAML"); composeYAML != "" { - helpers.Anyhow("compose", "-f", composeYAML, "down", "-v") - } + cleanup(data, helpers) if portStr := data.Labels().Get("hostPort"); portStr != "" { if port, err := strconv.Atoi(portStr); err == nil { _ = portlock.Release(port) @@ -285,10 +298,7 @@ services: 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -331,10 +341,7 @@ services: 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -376,10 +383,7 @@ services: 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -434,10 +438,7 @@ services: } } - 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -478,10 +479,7 @@ services: 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -524,10 +522,7 @@ services: 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.Cleanup = composeRunCleanup() testCase.Run(t) } @@ -582,10 +577,7 @@ services: } } - 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.Cleanup = composeRunCleanup() testCase.Run(t) } From 8136de5b0215ba786f4bd6325472f64f1a025a11 Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Fri, 17 Jul 2026 13:12:46 +0530 Subject: [PATCH 734/868] fix(network): stop injecting ipRange into the CNI config host-local's IPAM range has no ipRange field, so `network create --ip-range` was writing a property the plugin ignores into the on-disk conflist. Drop it and instead recompute the range CIDR from rangeStart/rangeEnd when reporting IPRange in `network inspect`, which is the only place the value was read back. Fixes #5068 Signed-off-by: Mayur Das --- pkg/inspecttypes/dockercompat/dockercompat.go | 46 ++++++++++++++----- .../dockercompat/dockercompat_test.go | 36 +++++++++++++++ pkg/netutil/cni_plugin.go | 1 - pkg/netutil/netutil.go | 12 ++--- pkg/netutil/netutil_test.go | 10 ++-- pkg/netutil/netutil_unix_test.go | 8 ++-- pkg/netutil/subnet/subnet.go | 40 ++++++++++++++++ pkg/netutil/subnet/subnet_test.go | 28 +++++++++++ 8 files changed, 153 insertions(+), 28 deletions(-) diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index f05f091def0..e96c40a88fa 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -49,6 +49,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/ipcutil" "github.com/containerd/nerdctl/v2/pkg/labels" + subnetutil "github.com/containerd/nerdctl/v2/pkg/netutil/subnet" "github.com/containerd/nerdctl/v2/pkg/ocihook/state" ) @@ -1086,11 +1087,20 @@ type structuredCNI struct { Name string `json:"name"` Plugins []struct { Ipam struct { - Ranges [][]IPAMConfig `json:"ranges"` + Ranges [][]cniIPAMRange `json:"ranges"` } `json:"ipam"` } `json:"plugins"` } +// cniIPAMRange is the on-disk host-local range. Its bounds let inspect recompute +// the ip-range CIDR, which host-local has no field for. +type cniIPAMRange struct { + Subnet string `json:"subnet"` + Gateway string `json:"gateway"` + RangeStart string `json:"rangeStart"` + RangeEnd string `json:"rangeEnd"` +} + type MemorySetting struct { Limit int64 `json:"limit"` Swap int64 `json:"swap"` @@ -1195,21 +1205,35 @@ func NetworkFromNative(n *native.Network) (*Network, error) { } res.Name = sCNI.Name + // An aux-address reservation splits one subnet into several sub-ranges that + // share the subnet and gateway. Collapse each distinct subnet into a single + // IPAM.Config like Docker, keeping the first entry's gateway and its lowest + // start. host-local returns a split subnet's sub-ranges sorted, so widening the + // end as later ones arrive rebuilds the original allocation window. + idxBySubnet := make(map[string]int) + startBySubnet := make(map[string]string) for _, plugin := range sCNI.Plugins { for _, ranges := range plugin.Ipam.Ranges { - // A range-set normally describes one subnet; an aux-address - // reservation splits it into several sub-ranges that all share the - // subnet and gateway. Report the first entry per distinct subnet so a - // split subnet collapses to one IPAM.Config like Docker, without - // dropping entries for different subnets in the same set. The - // aux-addresses themselves are attached later from a nerdctl label. - seen := make(map[string]struct{}, len(ranges)) for _, r := range ranges { - if _, ok := seen[r.Subnet]; ok { + idx, ok := idxBySubnet[r.Subnet] + if !ok { + idx = len(res.IPAM.Config) + idxBySubnet[r.Subnet] = idx + startBySubnet[r.Subnet] = r.RangeStart + res.IPAM.Config = append(res.IPAM.Config, IPAMConfig{Subnet: r.Subnet, Gateway: r.Gateway}) + } + // host-local has no ipRange field, so recompute it from the outermost + // bounds the way Docker reports it. A window that spans the whole + // subnet means no --ip-range was set, so report none. The + // aux-addresses themselves are attached later from a nerdctl label. + if r.RangeEnd == "" { continue } - seen[r.Subnet] = struct{}{} - res.IPAM.Config = append(res.IPAM.Config, r) + ipRange := subnetutil.CIDRFromRange(startBySubnet[r.Subnet], r.RangeEnd) + if ipRange == r.Subnet { + ipRange = "" + } + res.IPAM.Config[idx].IPRange = ipRange } } } diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 17c21f6155c..be14a0af99d 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -949,3 +949,39 @@ func TestImageFromNative(t *testing.T) { } }) } + +func TestNetworkFromNativeIPRange(t *testing.T) { + // host-local stores only rangeStart/rangeEnd; inspect must recompute the + // --ip-range CIDR from them and report it under IPRange like Docker, while a + // subnet without an ip-range reports no IPRange. + cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` + + `[{"subnet":"172.28.0.0/16","gateway":"172.28.5.254","rangeStart":"172.28.5.1","rangeEnd":"172.28.5.255"}],` + + `[{"subnet":"10.9.0.0/24","gateway":"10.9.0.1"}]` + + `]}}]}` + got, err := NetworkFromNative(&native.Network{CNI: []byte(cni)}) + assert.NilError(t, err) + assert.DeepEqual(t, []IPAMConfig{ + {Subnet: "172.28.0.0/16", Gateway: "172.28.5.254", IPRange: "172.28.5.0/24"}, + {Subnet: "10.9.0.0/24", Gateway: "10.9.0.1"}, + }, got.IPAM.Config) +} + +func TestNetworkFromNativeIPRangeSplit(t *testing.T) { + // An aux-address reservation splits a subnet into sorted sub-ranges on disk. + // inspect must collapse them to one IPAM.Config and rebuild the ip-range from + // the outermost bounds: the first subnet reconstructs its original --ip-range, + // while the second spans its whole subnet (aux-address only, no --ip-range) and + // so reports no IPRange. + cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` + + `[{"subnet":"172.28.0.0/16","gateway":"172.28.5.254","rangeStart":"172.28.5.1","rangeEnd":"172.28.5.9"},` + + `{"subnet":"172.28.0.0/16","rangeStart":"172.28.5.11","rangeEnd":"172.28.5.255"}],` + + `[{"subnet":"10.9.0.0/24","gateway":"10.9.0.1","rangeStart":"10.9.0.1","rangeEnd":"10.9.0.4"},` + + `{"subnet":"10.9.0.0/24","rangeStart":"10.9.0.6","rangeEnd":"10.9.0.254"}]` + + `]}}]}` + got, err := NetworkFromNative(&native.Network{CNI: []byte(cni)}) + assert.NilError(t, err) + assert.DeepEqual(t, []IPAMConfig{ + {Subnet: "172.28.0.0/16", Gateway: "172.28.5.254", IPRange: "172.28.5.0/24"}, + {Subnet: "10.9.0.0/24", Gateway: "10.9.0.1"}, + }, got.IPAM.Config) +} diff --git a/pkg/netutil/cni_plugin.go b/pkg/netutil/cni_plugin.go index b44e76042e2..4f5ad91c368 100644 --- a/pkg/netutil/cni_plugin.go +++ b/pkg/netutil/cni_plugin.go @@ -33,7 +33,6 @@ type IPAMRange struct { RangeStart string `json:"rangeStart,omitempty"` RangeEnd string `json:"rangeEnd,omitempty"` Gateway string `json:"gateway,omitempty"` - IPRange string `json:"ipRange,omitempty"` } type IPAMRoute struct { diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index b849ac17e81..56950e26cd8 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -627,9 +627,9 @@ func parseIPAMRange(subnet *net.IPNet, gatewayStr, ipRangeStr string) (*IPAMRang if !subnet.Contains(rangeStart) || !subnet.Contains(rangeEnd) { return nil, fmt.Errorf("no matching subnet %q for ip-range %q", subnet, ipRangeStr) } + // host-local has no ipRange field; store the bounds and recompute on inspect. res.RangeStart = rangeStart.String() res.RangeEnd = rangeEnd.String() - res.IPRange = ipRangeStr } return res, nil @@ -664,9 +664,9 @@ func ParseAuxAddresses(raw []string) (map[string]string, error) { // carving them out. host-local has no exclude list, but it does allocate across // every range in a set, so the reserved IPs become gaps between sub-ranges and // are never handed out. Reserved IPs outside the allocation window need no split -// (host-local cannot reach them anyway). The base range's gateway and ip-range -// are kept on the first sub-range so the rest of the pipeline and `network -// inspect` behave exactly as the un-split case. +// (host-local cannot reach them anyway). The base range's gateway is kept on +// every sub-range; inspect rebuilds the original ip-range from the outermost +// sub-range bounds, so nothing else has to be carried across the split. func splitIPAMRange(subnet *net.IPNet, base *IPAMRange, reserved []net.IP) ([]IPAMRange, error) { if len(reserved) == 0 { return []IPAMRange{*base}, nil @@ -736,12 +736,10 @@ func splitIPAMRange(subnet *net.IPNet, base *IPAMRange, reserved []net.IP) ([]IP // host-local reserves the gateway only when it is set on the range it lands // in, and after splitting the gateway can be in any sub-range, so set it on - // all of them. The original ip-range is nerdctl-only bookkeeping for inspect, - // so keep it on the first sub-range alone. + // all of them. for i := range out { out[i].Gateway = base.Gateway } - out[0].IPRange = base.IPRange return out, nil } diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index e7c60d98ce1..caf2c6f0522 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -104,7 +104,6 @@ func TestParseIPAMRange(t *testing.T) { expected: &IPAMRange{ Subnet: "10.1.0.0/16", Gateway: "10.1.0.1", - IPRange: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.255", }, @@ -115,7 +114,6 @@ func TestParseIPAMRange(t *testing.T) { expected: &IPAMRange{ Subnet: "10.1.100.0/23", Gateway: "10.1.100.1", - IPRange: "10.1.100.0/25", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.127", }, @@ -245,20 +243,20 @@ func TestSplitIPAMRange(t *testing.T) { { name: "a reservation inside an ip-range splits within its bounds", subnet: "10.1.100.0/24", - base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, reserved: ips("10.1.100.5"), expected: []IPAMRange{ - {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28"}, + {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.4", Gateway: "10.1.100.1"}, {Subnet: "10.1.100.0/24", RangeStart: "10.1.100.6", RangeEnd: "10.1.100.15", Gateway: "10.1.100.1"}, }, }, { name: "a reservation outside the ip-range needs no split", subnet: "10.1.100.0/24", - base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + base: &IPAMRange{Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, reserved: ips("10.1.100.200"), expected: []IPAMRange{ - {Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", IPRange: "10.1.100.0/28", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, + {Subnet: "10.1.100.0/24", Gateway: "10.1.100.1", RangeStart: "10.1.100.1", RangeEnd: "10.1.100.15"}, }, }, { diff --git a/pkg/netutil/netutil_unix_test.go b/pkg/netutil/netutil_unix_test.go index 1d6ce4a59a2..ada8bb1566a 100644 --- a/pkg/netutil/netutil_unix_test.go +++ b/pkg/netutil/netutil_unix_test.go @@ -95,12 +95,14 @@ func TestPairIPAMRangesIPRange(t *testing.T) { ranges, findIPv4, _, err := pairIPAMRanges(subnets, nil, ipRanges, nil, true) assert.NilError(t, err) assert.Equal(t, true, findIPv4) + // The ip-range is no longer stored verbatim; its effect shows up as the + // rangeStart/rangeEnd bounds host-local actually uses. got := map[string]string{} for _, r := range ranges { - got[r[0].Subnet] = r[0].IPRange + got[r[0].Subnet] = r[0].RangeStart } - assert.Equal(t, "10.6.1.0/24", got["10.6.0.0/16"]) - assert.Equal(t, "2001:db8:6::/80", got["2001:db8:6::/64"]) + assert.Equal(t, "10.6.1.1", got["10.6.0.0/16"]) + assert.Equal(t, "2001:db8:6::1", got["2001:db8:6::/64"]) }) t.Run("an ip-range matching no subnet errors", func(t *testing.T) { diff --git a/pkg/netutil/subnet/subnet.go b/pkg/netutil/subnet/subnet.go index 190c7dd4f81..23f44306b44 100644 --- a/pkg/netutil/subnet/subnet.go +++ b/pkg/netutil/subnet/subnet.go @@ -134,3 +134,43 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { cidr.IP[len(cidr.IP)-1]++ return cidr.IP, nil } + +// CIDRFromRange inverts FirstIPInSubnet/LastIPInSubnet: it rebuilds the CIDR from +// the start and end they produced. Returns "" for empty or unparsable bounds. +// A /31 or /127 has no distinct network and broadcast, so it recomputes as /32 or /128. +func CIDRFromRange(startStr, endStr string) string { + if startStr == "" || endStr == "" { + return "" + } + start, end := net.ParseIP(startStr), net.ParseIP(endStr) + if start == nil || end == nil { + return "" + } + // A single-address range is a /32 or /128. + if start.Equal(end) { + if start.To4() != nil { + return start.String() + "/32" + } + return start.String() + "/128" + } + // Canonical byte form: 4 for v4, 16 for v6. + s, e, bits := start.To4(), end.To4(), 32 + if s == nil { + s, e, bits = start.To16(), end.To16(), 128 + } + if e == nil || len(s) != len(e) { + return "" + } + // Undo FirstIPInSubnet's last-byte bump to get the network. + network := make(net.IP, len(s)) + copy(network, s) + network[len(network)-1]-- + // end is the broadcast, so network^end is the host mask; its width is the host bits. + hostBits := 0 + for i := range network { + for b := network[i] ^ e[i]; b != 0; b >>= 1 { + hostBits++ + } + } + return fmt.Sprintf("%s/%d", network.String(), bits-hostBits) +} diff --git a/pkg/netutil/subnet/subnet_test.go b/pkg/netutil/subnet/subnet_test.go index f61e719588e..cae38ca430f 100644 --- a/pkg/netutil/subnet/subnet_test.go +++ b/pkg/netutil/subnet/subnet_test.go @@ -48,3 +48,31 @@ func TestNextSubnet(t *testing.T) { assert.Equal(t, nextSubnet.String(), tc.expect) } } + +func TestCIDRFromRange(t *testing.T) { + testCases := []struct { + name string + start, end string + expect string + }{ + {"no range", "", "", ""}, + {"v4 /24", "10.1.100.1", "10.1.100.255", "10.1.100.0/24"}, + {"v4 /25", "10.24.24.1", "10.24.24.127", "10.24.24.0/25"}, + {"v4 /16", "172.28.0.1", "172.28.255.255", "172.28.0.0/16"}, + {"v4 offset /25", "10.1.100.129", "10.1.100.255", "10.1.100.128/25"}, + {"v4 /32", "10.0.0.5", "10.0.0.5", "10.0.0.5/32"}, + {"v4 /31 collapses to /32", "10.0.0.1", "10.0.0.1", "10.0.0.1/32"}, + {"v6 /64", "fd00:55::1", "fd00:55::ffff:ffff:ffff:ffff", "fd00:55::/64"}, + {"v6 /120", "fd00:7::1", "fd00:7::ff", "fd00:7::/120"}, + {"v6 /128", "fd00::5", "fd00::5", "fd00::5/128"}, + {"v6 /127 collapses to /128", "fd00::1", "fd00::1", "fd00::1/128"}, + {"start unparsable", "bogus", "10.0.0.255", ""}, + {"end unparsable", "10.0.0.1", "bogus", ""}, + {"mismatched families", "10.0.0.1", "fd00::ff", ""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expect, CIDRFromRange(tc.start, tc.end)) + }) + } +} From 5ec677484edee660922c0b1775f80faa6a42e931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:32:51 +0000 Subject: [PATCH 735/868] build(deps): bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/6fc4b006235f201fdab3722e17240ab420d580e5...3dc1ecc9bcb9e94e9b2c709687979e1298497054) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 133ba843ed9..f3f39b875a4 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,7 +92,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From 261b102e5d2a02059708cedfe5580c1702dee36a Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Mon, 3 Aug 2026 12:31:11 +0100 Subject: [PATCH 736/868] refactor: migrate compose cosign verify test to nerdtest Migrate TestComposePushAndPullWithCosignVerify from testutil.NewBase to nerdtest.Setup, the last remaining file in the nerdtest migration (#4613). Uses nerdtest.RegistryWithNoAuth and nerdtest.GenerateCosignKeyPair, and WithPseudoTTY in place of the unbuffer helper for the tty runs. Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/compose/compose_run_linux_test.go | 143 ++++++++++-------- 1 file changed, 84 insertions(+), 59 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index d1c7825391f..c761a121401 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -28,15 +28,15 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/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/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) func composeRunCleanup() test.Butler { @@ -583,32 +583,31 @@ services: } func TestComposePushAndPullWithCosignVerify(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - t.Parallel() - - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - reg := testregistry.NewWithNoAuth(base, 0, false) - t.Cleanup(func() { - keyPair.Cleanup() - reg.Cleanup(nil) - }) - - tID := testutil.Identifier(t) - testImageRefPrefix := fmt.Sprintf("127.0.0.1:%d/%s/", reg.Port, tID) - - var ( - imageSvc0 = testImageRefPrefix + "composebuild_svc0" - imageSvc1 = testImageRefPrefix + "composebuild_svc1" - imageSvc2 = testImageRefPrefix + "composebuild_svc2" + const sttyPartialOutput = "speed 38400 baud" + + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Binary("cosign"), + require.Not(nerdtest.Docker), + nerdtest.Build, + nerdtest.Registry, ) - dockerComposeYAML := fmt.Sprintf(` + testCase.Env["COSIGN_PASSWORD"] = "1" + + dockerfile := fmt.Sprintf("FROM %s", testutil.CommonImage) + + var reg *registry.Server + var composeYAML string + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + pri, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "1") + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + + prefix := fmt.Sprintf("127.0.0.1:%d/%s/", reg.Port, data.Identifier()) + composeYAML = fmt.Sprintf(` services: svc0: build: . @@ -635,37 +634,63 @@ services: x-nerdctl-sign: none entrypoint: - stty -`, imageSvc0, keyPair.PublicKey, keyPair.PrivateKey, - imageSvc1, keyPair.PrivateKey, imageSvc2) - - dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) - - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - comp.WriteFile("Dockerfile", dockerfile) - - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - // 1. build both services/images - base.ComposeCmd("-f", comp.YAMLFullPath(), "build").AssertOK() - // 2. compose push with cosign for svc0/svc1, (and none for svc2) - base.ComposeCmd("-f", comp.YAMLFullPath(), "push").AssertOK() - // 3. compose pull with cosign - base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc0").AssertOK() // key match - base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc1").AssertFail() // key mismatch - base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc2").AssertOK() // verify passed - // 4. compose run - const sttyPartialOutput = "speed 38400 baud" - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc0").AssertOutContains(sttyPartialOutput) // key match - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc1").AssertFail() // key mismatch - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc2").AssertOutContains(sttyPartialOutput) // verify passed - // 5. compose up - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc0").AssertOK() // key match - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc1").AssertFail() // key mismatch - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc2").AssertOK() // verify passed +`, prefix+"composebuild_svc0", pub, pri, prefix+"composebuild_svc1", pri, prefix+"composebuild_svc2") + + data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(dockerfile, "Dockerfile") + + composePath := data.Temp().Path("compose.yaml") + // Build both services/images and push, signing svc0/svc1 with cosign (svc2 unsigned). + helpers.Ensure("compose", "-f", composePath, "build") + helpers.Ensure("compose", "-f", composePath, "push") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + composeRunCleanup()(data, helpers) + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + // Each subtest re-materializes the compose project (the signed images live in the + // shared registry set up above) and exercises one verify scenario: + // svc0 verifies against the matching key, svc1 against a mismatching key (must fail), + // svc2 is not verified. + subTest := func(description, op, svc string, tty bool, expected test.Manager) *test.Case { + return &test.Case{ + Description: description, + Setup: func(data test.Data, helpers test.Helpers) { + data.Temp().Save(composeYAML, "compose.yaml") + data.Temp().Save(dockerfile, "Dockerfile") + }, + Cleanup: composeRunCleanup(), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), op, svc) + if tty { + // stty (the entrypoint) requires a tty, which `run -t` provides. + cmd.WithPseudoTTY() + } + return cmd + }, + Expected: expected, + } + } + + success := test.Expects(expect.ExitCodeSuccess, nil, nil) + fail := test.Expects(expect.ExitCodeGenericFail, nil, nil) + successWithOutput := test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(sttyPartialOutput)) + + testCase.SubTests = []*test.Case{ + subTest("compose pull svc0 (key match)", "pull", "svc0", false, success), + subTest("compose pull svc1 (key mismatch)", "pull", "svc1", false, fail), + subTest("compose pull svc2 (verify none)", "pull", "svc2", false, success), + subTest("compose run svc0 (key match)", "run", "svc0", true, successWithOutput), + subTest("compose run svc1 (key mismatch)", "run", "svc1", true, fail), + subTest("compose run svc2 (verify none)", "run", "svc2", true, successWithOutput), + subTest("compose up svc0 (key match)", "up", "svc0", false, success), + subTest("compose up svc1 (key mismatch)", "up", "svc1", false, fail), + subTest("compose up svc2 (verify none)", "up", "svc2", false, success), + } + + testCase.Run(t) } From 54d6cfac8a922be9e90df1b5cf7ba91bc5b8c24e Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Mon, 6 Jul 2026 17:42:22 +0800 Subject: [PATCH 737/868] optimize: reduce unnecessary function calls Signed-off-by: ningmingxiao --- cmd/nerdctl/container/container_run.go | 25 ++++++++------- pkg/containerutil/containerutil.go | 31 ++++++++++++------- pkg/healthcheck/healthcheck_manager_darwin.go | 4 +-- .../healthcheck_manager_freebsd.go | 4 +-- pkg/healthcheck/healthcheck_manager_linux.go | 26 ++++++++++------ .../healthcheck_manager_windows.go | 4 +-- 6 files changed, 56 insertions(+), 38 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index b52994dd063..f3b9d65a00c 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -458,12 +458,13 @@ func runAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - - statusC, err := task.Wait(ctx) - if err != nil { - return err + var statusC <-chan containerd.ExitStatus + if !createOpt.Detach { + statusC, err = task.Wait(ctx) + if err != nil { + return err + } } - if err := task.Start(ctx); err != nil { return err } @@ -480,12 +481,14 @@ func runAction(cmd *cobra.Command, args []string) error { 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) - } - if err := healthcheck.StartTimer(ctx, c, (*config.Config)(&createOpt.GOptions)); err != nil { - return fmt.Errorf("failed to start healthcheck timer: %w", err) + if hcStr, ok := lab[labels.HealthCheck]; ok && hcStr != "" { + // Setup container healthchecks. + if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions), createOpt.NerdctlCmd, createOpt.NerdctlArgs, lab); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, c, (*config.Config)(&createOpt.GOptions), lab); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } } if createOpt.Detach { diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index fa27533f080..7c16bc720a0 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -304,13 +304,15 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i } // 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) - } - if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { - return fmt.Errorf("failed to start healthcheck timer: %w", err) + if hcStr, ok := lab[labels.HealthCheck]; ok && hcStr != "" { + // If container has health checks configured, create and start systemd timer/service files. + if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs, lab); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, container, cfg, lab); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } } - if !isAttach { return nil } @@ -542,12 +544,19 @@ func Unpause(ctx context.Context, client *containerd.Client, id string, cfg *con return err } - // Recreate healthcheck related 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) + label, err := container.Labels(ctx) + if err != nil { + return err } - if err := healthcheck.StartTimer(ctx, container, cfg); err != nil { - return fmt.Errorf("failed to start healthcheck timer: %w", err) + + if hcStr, ok := label[labels.HealthCheck]; ok && hcStr != "" { + // Recreate healthcheck related systemd timer/service files. + if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs, label); err != nil { + return fmt.Errorf("failed to create healthcheck timer: %w", err) + } + if err := healthcheck.StartTimer(ctx, container, cfg, label); err != nil { + return fmt.Errorf("failed to start healthcheck timer: %w", err) + } } switch status.Status { diff --git a/pkg/healthcheck/healthcheck_manager_darwin.go b/pkg/healthcheck/healthcheck_manager_darwin.go index f48e5df6a38..5b2d710ce99 100644 --- a/pkg/healthcheck/healthcheck_manager_darwin.go +++ b/pkg/healthcheck/healthcheck_manager_darwin.go @@ -25,12 +25,12 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string, label map[string]string) error { return nil } // StartTimer starts the healthcheck timer unit. -func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config, label map[string]string) error { return nil } diff --git a/pkg/healthcheck/healthcheck_manager_freebsd.go b/pkg/healthcheck/healthcheck_manager_freebsd.go index f48e5df6a38..5b2d710ce99 100644 --- a/pkg/healthcheck/healthcheck_manager_freebsd.go +++ b/pkg/healthcheck/healthcheck_manager_freebsd.go @@ -25,12 +25,12 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string, label map[string]string) error { return nil } // StartTimer starts the healthcheck timer unit. -func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config, label map[string]string) error { return nil } diff --git a/pkg/healthcheck/healthcheck_manager_linux.go b/pkg/healthcheck/healthcheck_manager_linux.go index f3a992c2bc4..5fdd69e7e2f 100644 --- a/pkg/healthcheck/healthcheck_manager_linux.go +++ b/pkg/healthcheck/healthcheck_manager_linux.go @@ -36,8 +36,8 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { - hc := extractHealthcheck(ctx, container) +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string, label map[string]string) error { + hc := extractHealthcheck(ctx, container, label) if hc == nil { return nil } @@ -106,8 +106,8 @@ func createDbusConn(ctx context.Context) (*dbus.Conn, error) { } // StartTimer starts the healthcheck timer unit. -func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { - hc := extractHealthcheck(ctx, container) +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config, label map[string]string) error { + hc := extractHealthcheck(ctx, container, label) if hc == nil { return nil } @@ -135,7 +135,7 @@ func StartTimer(ctx context.Context, container containerd.Container, cfg *config // RemoveTransientHealthCheckFiles stops and cleans up the transient timer and service. func RemoveTransientHealthCheckFiles(ctx context.Context, container containerd.Container) error { - hc := extractHealthcheck(ctx, container) + hc := extractHealthcheck(ctx, container, nil) if hc == nil { return nil } @@ -254,11 +254,17 @@ func ForceRemoveTransientHealthCheckFiles(ctx context.Context, containerID strin return nil } -func extractHealthcheck(ctx context.Context, container containerd.Container) *Healthcheck { - l, err := container.Labels(ctx) - if err != nil { - log.G(ctx).WithError(err).Debugf("could not get labels for container %s", container.ID()) - return nil +func extractHealthcheck(ctx context.Context, container containerd.Container, label map[string]string) *Healthcheck { + var l map[string]string + var err error + if label == nil { + l, err = container.Labels(ctx) + if err != nil { + log.G(ctx).WithError(err).Debugf("could not get labels for container %s", container.ID()) + return nil + } + } else { + l = label } hcStr, ok := l[labels.HealthCheck] if !ok || hcStr == "" { diff --git a/pkg/healthcheck/healthcheck_manager_windows.go b/pkg/healthcheck/healthcheck_manager_windows.go index efd606e7da1..0848a85c935 100644 --- a/pkg/healthcheck/healthcheck_manager_windows.go +++ b/pkg/healthcheck/healthcheck_manager_windows.go @@ -25,12 +25,12 @@ import ( ) // CreateTimer sets up the transient systemd timer and service for healthchecks. -func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string) error { +func CreateTimer(ctx context.Context, container containerd.Container, cfg *config.Config, nerdctlCmd string, nerdctlArgs []string, label map[string]string) error { return nil } // StartTimer starts the healthcheck timer unit. -func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config) error { +func StartTimer(ctx context.Context, container containerd.Container, cfg *config.Config, label map[string]string) error { return nil } From c582f7245ae3513163539d0dc2a0d6ef301ec479 Mon Sep 17 00:00:00 2001 From: rainwu Date: Fri, 7 Aug 2026 18:31:43 +0800 Subject: [PATCH 738/868] fix(login): accept equivalent registry hosts in auth creds callback Parse() appends the standard HTTPS port to the registry address, but the containerd authorizer calls the credentials callback with the request URL host, which omits the default port (or uses the registry-1.docker.io alias for Docker Hub). The strict equality check then fails and login aborts. Replace the strict equality check with an equivalence check that accepts the same hostname with the default port omitted, and the Docker Hub index.docker.io/registry-1.docker.io alias pair. Callback hosts with an explicit non-standard port must still match exactly. Fixes #3992 Refs #3245 Signed-off-by: rainwu --- pkg/cmd/login/login.go | 65 ++++++++++++++++++++------ pkg/cmd/login/login_test.go | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 pkg/cmd/login/login_test.go diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index 773bf8edc76..89505e4e891 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" @@ -117,19 +118,7 @@ func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptio } dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir)) - authCreds := func(acArg string) (string, string, error) { - if acArg == host { - if credentials.RegistryToken != "" { - // Even containerd/CRI does not support RegistryToken as of v1.4.3, - // so, nobody is actually using RegistryToken? - log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host) - } - return credentials.Username, credentials.Password, nil - } - return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg) - } - - dOpts = append(dOpts, dockerconfigresolver.WithAuthCreds(authCreds)) + dOpts = append(dOpts, dockerconfigresolver.WithAuthCreds(loginAuthCreds(ctx, host, registryURL, credentials))) ho, err := dockerconfigresolver.NewHostOptions(ctx, host, dOpts...) if err != nil { return "", err @@ -212,3 +201,53 @@ func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error { return errors.New("too many 401 (probably)") } + +// loginAuthCreds returns the credentials callback handed to the containerd +// authorizer during login. +func loginAuthCreds(ctx context.Context, host string, registryURL *dockerconfigresolver.RegistryURL, credentials *dockerconfigresolver.Credentials) func(string) (string, string, error) { + return func(acArg string) (string, string, error) { + if acArg == host || isEquivalentRegistryHost(acArg, registryURL) { + if credentials.RegistryToken != "" { + // Even containerd/CRI does not support RegistryToken as of v1.4.3, + // so, nobody is actually using RegistryToken? + log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host) + } + return credentials.Username, credentials.Password, nil + } + return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg) + } +} + +// isEquivalentRegistryHost reports whether acArg, the host value the +// containerd authorizer passes to the credentials callback, refers to the +// same registry as registryURL, the address the user asked to log in to. +// +// Parse always appends the standard HTTPS port to registryURL when the user +// did not specify one, while the authorizer may call back with a host that +// omits the default port, or with a Docker Hub alias, in which case strict +// equality fails spuriously. +// See https://github.com/containerd/nerdctl/issues/3992 and +// https://github.com/containerd/nerdctl/issues/3245. +func isEquivalentRegistryHost(acArg string, registryURL *dockerconfigresolver.RegistryURL) bool { + acHost, acPort, err := net.SplitHostPort(acArg) + if err != nil { + // acArg carries no port + acHost, acPort = acArg, "" + } + // A callback host carrying an explicit non-standard port can only be + // equivalent by exact equality, which the caller already checked. + if acPort != "" && acPort != dockerconfigresolver.StandardHTTPSPort { + return false + } + // The user did not pass an explicit non-default port, so a callback + // host that merely omits the standard HTTPS port is equivalent. + if registryURL.Port() == dockerconfigresolver.StandardHTTPSPort && acHost == registryURL.Hostname() { + return true + } + // Docker Hub aliases: "docker.io" logins resolve to index.docker.io, + // while the actual registry endpoint is registry-1.docker.io. + if registryURL.Hostname() == "index.docker.io" && acHost == "registry-1.docker.io" { + return true + } + return false +} diff --git a/pkg/cmd/login/login_test.go b/pkg/cmd/login/login_test.go new file mode 100644 index 00000000000..956b356c09f --- /dev/null +++ b/pkg/cmd/login/login_test.go @@ -0,0 +1,92 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package login + +import ( + "context" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" +) + +func TestLoginAuthCredsAcceptsEquivalentHosts(t *testing.T) { + tests := []struct { + name string + address string + acArg string + wantErr bool + }{ + { + name: "exact host with standard port", + address: "harbor.example.io", + acArg: "harbor.example.io:443", + }, + { + // https://github.com/containerd/nerdctl/issues/3992 + name: "host without default port", + address: "harbor.example.io", + acArg: "harbor.example.io", + }, + { + // https://github.com/containerd/nerdctl/issues/3245 + name: "docker.io alias without port", + address: "docker.io", + acArg: "registry-1.docker.io", + }, + { + name: "docker.io alias with port", + address: "docker.io", + acArg: "registry-1.docker.io:443", + }, + { + name: "mismatched host", + address: "harbor.example.io", + acArg: "evil.example.io", + wantErr: true, + }, + { + name: "explicit non-standard port not dropped", + address: "harbor.example.io:8443", + acArg: "harbor.example.io", + wantErr: true, + }, + { + name: "different explicit port", + address: "harbor.example.io", + acArg: "harbor.example.io:8443", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + registryURL, err := dockerconfigresolver.Parse(tt.address) + assert.NilError(t, err) + credentials := &dockerconfigresolver.Credentials{Username: "user", Password: "pass"} + authCreds := loginAuthCreds(context.Background(), registryURL.Host, registryURL, credentials) + username, password, err := authCreds(tt.acArg) + if tt.wantErr { + assert.ErrorContains(t, err, "expected acArg") + return + } + assert.NilError(t, err) + assert.Equal(t, "user", username) + assert.Equal(t, "pass", password) + }) + } +} From 429641b560c780bd0d52c581f6d91c7b4ad5a110 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:33:01 +0000 Subject: [PATCH 739/868] build(deps): bump github.com/rootless-containers/rootlesskit/v3 Bumps [github.com/rootless-containers/rootlesskit/v3](https://github.com/rootless-containers/rootlesskit) from 3.0.2 to 3.1.0. - [Release notes](https://github.com/rootless-containers/rootlesskit/releases) - [Commits](https://github.com/rootless-containers/rootlesskit/compare/v3.0.2...v3.1.0) --- updated-dependencies: - dependency-name: github.com/rootless-containers/rootlesskit/v3 dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f1be9391224..4a85fce9775 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/opencontainers/selinux v1.15.1 github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.0.2 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.1.0 //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 diff --git a/go.sum b/go.sum index ce2f0920ccb..44a21725bda 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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.2 h1:8hRSEUdzKxmMdy+PHD/rJ3E0+nWEkh9woMDbXjSnMKQ= -github.com/rootless-containers/rootlesskit/v3 v3.0.2/go.mod h1:oFY5X3mj9mOpi/F+oBaD5ojo6QZhr9JdcpsQ6qepz6Q= +github.com/rootless-containers/rootlesskit/v3 v3.1.0 h1:6RR+6Y9aml6gIkn4ohcHIpIy6I7Wmyt3iglKKEIb678= +github.com/rootless-containers/rootlesskit/v3 v3.1.0/go.mod h1:oFY5X3mj9mOpi/F+oBaD5ojo6QZhr9JdcpsQ6qepz6Q= 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= From 22ea69fe49ad7a70d1c900212e68790a144ee862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:32:45 +0000 Subject: [PATCH 740/868] build(deps): bump github.com/klauspost/compress from 1.19.1 to 1.19.2 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.19.1...v1.19.2) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f1be9391224..e6dcecd2308 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 - github.com/klauspost/compress v1.19.1 + github.com/klauspost/compress v1.19.2 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined github.com/moby/moby/client v0.5.1 github.com/moby/moby/v2 v2.0.0-beta.21 diff --git a/go.sum b/go.sum index ce2f0920ccb..6e13249e84a 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ 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/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= -github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From beca45feabba42f4130d704c2698e4fd6d559724 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:32:46 +0000 Subject: [PATCH 741/868] build(deps): bump actions/attest-build-provenance from 4.1.1 to 4.2.2 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 4.1.1 to 4.2.2. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/0f67c3f4856b2e3261c31976d6725780e5e4c373...4d101475d8b20a2381f78447822ac1eab6504dd8) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: 4.2.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 173127ca1f5..4e4e4ca6f39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - name: "Generate artifact attestation" - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _output/* From b073d7c393231adde3be6343a44e842aa7f55fc7 Mon Sep 17 00:00:00 2001 From: Ravi Arnan Date: Sat, 8 Aug 2026 17:05:51 +0800 Subject: [PATCH 742/868] fix: do not require CNI plugins for compose projects that avoid CNI compose.New unconditionally passed netutil.WithDefaultNetwork to NewCNIEnv, so every compose command created nerdctl's default bridge network before parsing a single service. On a host without the CNI plugins installed this failed outright, even for projects whose services all use network_mode: host or none and so never touch CNI. The default network is not needed there. NetworkExists, the only consumer of the CNIEnv built in compose.New, is called exclusively with project-scoped network names, and external networks return before reaching it. Services that do attach to the default bridge still get it created on demand, because compose shells out to `nerdctl run`, which ensures the default network via cniNetworkManager. Fixes #4461 Signed-off-by: Ravi Arnan --- cmd/nerdctl/compose/compose_up_linux_test.go | 41 ++++++++++++++++++++ pkg/cmd/compose/compose.go | 5 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 97f06b37fe0..4ac9af1163b 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -1423,3 +1423,44 @@ services: testCase.Run(t) } + +func TestComposeUpNetworkModeHostWithoutCNIPlugins(t *testing.T) { + testCase := nerdtest.Setup() + + // --cni-path and --cni-netconfpath are nerdctl specific. + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` +services: + svc0: + image: %s + network_mode: host + command: "sleep infinity" +`, testutil.CommonImage) + + data.Labels().Set("composeYAML", data.Temp().Save(dockerComposeYAML, "compose.yaml")) + // An empty CNI_PATH and an empty netconf dir together mimic a host that never + // installed the CNI plugins. Services using host networking do not need them. + data.Labels().Set("cniPath", data.Temp().Dir("cni-bin")) + data.Labels().Set("cniNetConfPath", data.Temp().Dir("cni-netconf")) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "--cni-path", data.Labels().Get("cniPath"), + "--cni-netconfpath", data.Labels().Get("cniNetConfPath"), + "compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow( + "--cni-path", data.Labels().Get("cniPath"), + "--cni-netconfpath", data.Labels().Get("cniNetConfPath"), + "compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + + testCase.Run(t) +} diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index 146c4997b99..fbf00600f8e 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -52,7 +52,10 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op return nil, err } - cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace), netutil.WithDefaultNetwork(globalOptions.BridgeIP)) + // The default network is deliberately not created here. It is only needed by + // services that actually attach to it, and `nerdctl run` already creates it on + // demand. + cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace)) if err != nil { return nil, err } From f341b85443d65b30e5d9c512a3a96b6253f5c0bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:40:17 +0000 Subject: [PATCH 743/868] build(deps): bump github.com/cilium/ebpf from 0.17.3 to 0.22.0 Bumps [github.com/cilium/ebpf](https://github.com/cilium/ebpf) from 0.17.3 to 0.22.0. - [Commits](https://github.com/cilium/ebpf/compare/v0.17.3...v0.22.0) --- updated-dependencies: - dependency-name: github.com/cilium/ebpf dependency-version: 0.22.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index f1be9391224..90c5c3d990a 100644 --- a/go.mod +++ b/go.mod @@ -76,7 +76,7 @@ require ( require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/cilium/ebpf v0.17.3 // indirect + github.com/cilium/ebpf v0.22.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/plugin v1.1.0 // indirect diff --git a/go.sum b/go.sum index ce2f0920ccb..eab71c7f50e 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= @@ -22,8 +22,8 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= -github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= +github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= +github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= @@ -128,8 +128,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= From c47e6f8d9aad185fa08789076b9172c677514731 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:01:39 +0000 Subject: [PATCH 744/868] build(deps): bump github.com/docker/cli Bumps the docker group with 1 update in the / directory: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.7.1+incompatible to 29.7.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.7.1...v29.7.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.7.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9696d5f8e69..c0d3169b7c8 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.7.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.7.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 937af16bfe9..a74689e40bd 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,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.7.1+incompatible h1:ILZpP6B7fedIr6ANy824QkDp1WMJuouIq0O2SrBkB2w= -github.com/docker/cli v29.7.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= +github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= From 41c7eca32127d33bdbef5951cf965bf92216a2ac Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 18 Aug 2026 21:20:30 +0900 Subject: [PATCH 745/868] Remove unused functions Remove code that is no longer reachable, as reported by `golang.org/x/tools/cmd/deadcode -test ./...`: - The legacy `testutil.Base`/`testutil.Cmd` test framework (`testutil.NewBase` and friends), superseded by Tigron/nerdtest. - `pkg/testutil/testregistry`, only referenced from a commented-out test. - Unused helpers in `cmd/nerdctl/helpers` (`ComposeUp`, `NewCosignKeyPair`, `CreateBuildContext`). - Unused library functions in `pkg/cmd/container`, `pkg/resolvconf`, `pkg/rootlessutil`, `pkg/internal/filesystem`, and `pkg/api/types/cri`. Also update `docs/testing/README.md` to stop referring to the removed API. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- cmd/nerdctl/helpers/testing.go | 10 - cmd/nerdctl/helpers/testing_linux.go | 113 ---- cmd/nerdctl/login/login_linux_test.go | 29 - docs/testing/README.md | 8 +- pkg/api/types/cri/metadata_types.go | 8 - pkg/cmd/container/idmap.go | 120 ---- pkg/cmd/container/run_blkio_linux.go | 8 - pkg/internal/filesystem/os.go | 8 - pkg/resolvconf/resolvconf.go | 39 -- pkg/rootlessutil/rootlessutil_linux.go | 15 - pkg/testutil/compose.go | 4 - pkg/testutil/images_linux.go | 5 - pkg/testutil/testregistry/certsd_linux.go | 27 - .../testregistry/testregistry_linux.go | 386 ------------ pkg/testutil/testutil.go | 570 ------------------ 15 files changed, 3 insertions(+), 1347 deletions(-) delete mode 100644 cmd/nerdctl/helpers/testing_linux.go delete mode 100644 pkg/testutil/testregistry/certsd_linux.go delete mode 100644 pkg/testutil/testregistry/testregistry_linux.go diff --git a/cmd/nerdctl/helpers/testing.go b/cmd/nerdctl/helpers/testing.go index 6f356e24962..9b9007c248e 100644 --- a/cmd/nerdctl/helpers/testing.go +++ b/cmd/nerdctl/helpers/testing.go @@ -23,18 +23,8 @@ import ( "os" "os/exec" "path/filepath" - "testing" - - "gotest.tools/v3/assert" ) -func CreateBuildContext(t *testing.T, dockerfile string) string { - tmpDir := t.TempDir() - err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644) - assert.NilError(t, err) - return tmpDir -} - func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { if err := os.MkdirAll(rootfsPath, 0755); err != nil { return err diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go deleted file mode 100644 index 60a4cd76beb..00000000000 --- a/cmd/nerdctl/helpers/testing_linux.go +++ /dev/null @@ -1,113 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package helpers - -import ( - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" -) - -type CosignKeyPair struct { - PublicKey string - PrivateKey string - Cleanup func() -} - -func NewCosignKeyPair(t testing.TB, path string, password string) *CosignKeyPair { - td, err := os.MkdirTemp(t.TempDir(), path) - assert.NilError(t, err) - - cmd := exec.Command("cosign", "generate-key-pair") - cmd.Dir = td - cmd.Env = append(cmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", password)) - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) - } - - publicKey := filepath.Join(td, "cosign.pub") - privateKey := filepath.Join(td, "cosign.key") - - return &CosignKeyPair{ - PublicKey: publicKey, - PrivateKey: privateKey, - Cleanup: func() { - _ = os.RemoveAll(td) - }, - } -} - -func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) { - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - base.ComposeCmd(append(append([]string{"-f", comp.YAMLFullPath()}, opts...), "up", "-d")...).AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertOK() - base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK() - - checkWordpress := func() error { - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) - if err != nil { - return err - } - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { - t.Logf("respBody=%q", respBody) - return fmt.Errorf("respBody does not contain %q", testutil.WordpressIndexHTMLSnippet) - } - return nil - } - - var wordpressWorking bool - for i := 0; i < 30; i++ { - t.Logf("(retry %d)", i) - err := checkWordpress() - if err == nil { - wordpressWorking = true - break - } - // NOTE: "

Error establishing a database connection

" is expected for the first few iterations - t.Log(err) - time.Sleep(3 * time.Second) - } - - if !wordpressWorking { - t.Fatal("wordpress is not working") - } - t.Log("wordpress seems functional") - - base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail() - base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() -} diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 90834490508..6d851d2e731 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -184,35 +184,6 @@ func TestLoginPersistence(t *testing.T) { testCase.Run(t) } -/* -func TestAgainstNoAuth(t *testing.T) { - base := testutil.NewBase(t) - t.Parallel() - - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, randomPort, &testregistry.NoAuth{}, nil) - - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) - - c := (&Client{}). - WithCredentials("invalid", "invalid") - - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - - content, _ := os.ReadFile(filepath.Join(c.configPath, "config.json")) - fmt.Println(string(content)) - - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertFail() - -} - -*/ - func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication diff --git a/docs/testing/README.md b/docs/testing/README.md index 82a2b35e86f..cbdc151ec4f 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -104,9 +104,9 @@ GITHUB_ACTIONS=true ./hack/test-integration-rootless.sh ./hack/test-integration. ##### General case It should be possible to parallelize all tests - as such, please make sure you: -- name all resources your test is manipulating after the test identifier (`testutil.Identifier(t)`) +- name all resources your test is manipulating after the test identifier (`data.Identifier()`) to guarantee your test will not interact with other tests -- do NOT use `os.Setenv` - instead, add into `base.Env` +- do NOT use `os.Setenv` - instead, use `Setenv` on the command you are running - use `t.Parallel()` at the beginning of your test (and subtests as well of course) - in the very exceptional case where your test for some reason can NOT be parallelized, be sure to mark it explicitly as such with a comment explaining why @@ -114,9 +114,7 @@ with a comment explaining why ##### For "blanket" destructive operations If you are going to use blanket destructive operations (like `prune`), please: -- use a dedicated namespace: instead of calling `testutil.Base`, call `testutil.BaseWithNamespace` -and be sure that your namespace is named after the test id -- remove the namespace in your test `Cleanup` +- use a dedicated namespace: add `nerdtest.Private` to the test `Require`ments - since docker does not support namespaces, be sure to: - only enable `Parallel` if the target is NOT docker: ` if !nerdtest.IsDocker() { t.Parallel() }` - double check that what you do in the default namespace is safe diff --git a/pkg/api/types/cri/metadata_types.go b/pkg/api/types/cri/metadata_types.go index 58b14151ada..63e5a48d554 100644 --- a/pkg/api/types/cri/metadata_types.go +++ b/pkg/api/types/cri/metadata_types.go @@ -51,14 +51,6 @@ type ContainerMetadata struct { LogPath string } -// MarshalJSON encodes Metadata into bytes in json format. -func (c *ContainerMetadata) MarshalJSON() ([]byte, error) { - return json.Marshal(&ContainerVersionedMetadata{ - Version: metadataVersion, - Metadata: criContainerMetadataInternal(*c), - }) -} - // UnmarshalJSON decodes Metadata from bytes. func (c *ContainerMetadata) UnmarshalJSON(data []byte) error { versioned := &ContainerVersionedMetadata{} diff --git a/pkg/cmd/container/idmap.go b/pkg/cmd/container/idmap.go index 23d366e4082..f8b15dab578 100644 --- a/pkg/cmd/container/idmap.go +++ b/pkg/cmd/container/idmap.go @@ -17,25 +17,12 @@ package container import ( - "errors" "fmt" "strings" "github.com/opencontainers/runtime-spec/specs-go" ) -const invalidID = 1<<32 - 1 - -var invalidUser = User{Uid: invalidID, Gid: invalidID} - -// User is a Uid and Gid pair of a user -// -//nolint:revive -type User struct { - Uid uint32 - Gid uint32 -} - // IDMap contains the mappings of Uids and Gids. // //nolint:revive @@ -44,36 +31,6 @@ type ContainerdIDMap struct { GidMap []specs.LinuxIDMapping `json:"GidMap"` } -// RootPair returns the ID pair for the root user -func (i *ContainerdIDMap) RootPair() (User, error) { - uid, err := toHost(0, i.UidMap) - if err != nil { - return invalidUser, err - } - gid, err := toHost(0, i.GidMap) - if err != nil { - return invalidUser, err - } - return User{Uid: uid, Gid: gid}, nil -} - -// ToHost returns the host user ID pair for the container ID pair. -func (i *ContainerdIDMap) ToHost(pair User) (User, error) { - var ( - target User - err error - ) - target.Uid, err = toHost(pair.Uid, i.UidMap) - if err != nil { - return invalidUser, err - } - target.Gid, err = toHost(pair.Gid, i.GidMap) - if err != nil { - return invalidUser, err - } - return target, nil -} - // Marshal serializes the IDMap object into two strings: // one uidmap list and another one for gidmap list func (i *ContainerdIDMap) Marshal() (string, string) { @@ -87,84 +44,7 @@ func (i *ContainerdIDMap) Marshal() (string, string) { return marshal(i.UidMap), marshal(i.GidMap) } -// Unmarshal deserialize the passed uidmap and gidmap strings -// into a IDMap object. Error is returned in case of failure -func (i *ContainerdIDMap) Unmarshal(uidMap, gidMap string) error { - unmarshal := func(str string, fn func(m specs.LinuxIDMapping)) error { - if len(str) == 0 { - return nil - } - for _, mapping := range strings.Split(str, ",") { - m, err := deserializeLinuxIDMapping(mapping) - if err != nil { - return err - } - fn(m) - } - return nil - } - if err := unmarshal(uidMap, func(m specs.LinuxIDMapping) { - i.UidMap = append(i.UidMap, m) - }); err != nil { - return err - } - return unmarshal(gidMap, func(m specs.LinuxIDMapping) { - i.GidMap = append(i.GidMap, m) - }) -} - -// toHost takes an id mapping and a remapped ID, and translates the -// ID to the mapped host ID. If no map is provided, then the translation -// assumes a 1-to-1 mapping and returns the passed in id # -func toHost(contID uint32, idMap []specs.LinuxIDMapping) (uint32, error) { - if idMap == nil { - return contID, nil - } - for _, m := range idMap { - high, err := safeSum(m.ContainerID, m.Size) - if err != nil { - break - } - if contID >= m.ContainerID && contID < high { - hostID, err := safeSum(m.HostID, contID-m.ContainerID) - if err != nil || hostID == invalidID { - break - } - return hostID, nil - } - } - return invalidID, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID) -} - -// safeSum returns the sum of x and y. or an error if the result overflows -func safeSum(x, y uint32) (uint32, error) { - z := x + y - if z < x || z < y { - return invalidID, errors.New("ID overflow") - } - return z, nil -} - // serializeLinuxIDMapping marshals a LinuxIDMapping object to string func serializeLinuxIDMapping(m specs.LinuxIDMapping) string { return fmt.Sprintf("%d:%d:%d", m.ContainerID, m.HostID, m.Size) } - -// deserializeLinuxIDMapping unmarshals a string to a LinuxIDMapping object -func deserializeLinuxIDMapping(str string) (specs.LinuxIDMapping, error) { - var ( - hostID, ctrID, length int64 - ) - _, err := fmt.Sscanf(str, "%d:%d:%d", &ctrID, &hostID, &length) - if err != nil { - return specs.LinuxIDMapping{}, fmt.Errorf("input value %s unparsable: %w", str, err) - } - if ctrID < 0 || ctrID >= invalidID || hostID < 0 || hostID >= invalidID || length < 0 || length >= invalidID { - return specs.LinuxIDMapping{}, fmt.Errorf("invalid mapping \"%s\"", str) - } - return specs.LinuxIDMapping{ - ContainerID: uint32(ctrID), - HostID: uint32(hostID), - Size: uint32(length), - }, nil -} diff --git a/pkg/cmd/container/run_blkio_linux.go b/pkg/cmd/container/run_blkio_linux.go index 1b3f03929c3..0ec15ed3a0f 100644 --- a/pkg/cmd/container/run_blkio_linux.go +++ b/pkg/cmd/container/run_blkio_linux.go @@ -41,20 +41,12 @@ type WeightDevice struct { Weight uint16 } -func (w *WeightDevice) String() string { - return fmt.Sprintf("%s:%d", w.Path, w.Weight) -} - // ThrottleDevice is a structure that holds device:rate_per_second pair type ThrottleDevice struct { Path string Rate uint64 } -func (t *ThrottleDevice) String() string { - return fmt.Sprintf("%s:%d", t.Path, t.Rate) -} - func toOCIWeightDevices(weightDevices []*WeightDevice) ([]specs.LinuxWeightDevice, error) { var stat unix.Stat_t blkioWeightDevices := make([]specs.LinuxWeightDevice, 0, len(weightDevices)) diff --git a/pkg/internal/filesystem/os.go b/pkg/internal/filesystem/os.go index 62411c5250f..8070f7a474f 100644 --- a/pkg/internal/filesystem/os.go +++ b/pkg/internal/filesystem/os.go @@ -34,14 +34,6 @@ func ReadFile(filename string) (data []byte, err error) { return data, nil } -func Stat(filename string) (os.FileInfo, error) { - if err := ensureRecovery(filename); err != nil { - return nil, errors.Join(ErrFilesystemFailure, err) - } - - return os.Stat(filename) -} - // WriteFile implements an atomic and durable alternative to os.WriteFile that does not change inodes (unlike the usual // approach on atomic writes that relies on renaming files). func WriteFile(filename string, data []byte, perm os.FileMode) error { diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index 30aa53fe21c..544d983d3f2 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -114,12 +114,6 @@ var ( optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) ) -var lastModified struct { - sync.Mutex - sha256 string - contents []byte -} - // File contains the resolv.conf content and its hash type File struct { Content []byte @@ -144,39 +138,6 @@ func GetSpecific(path string) (*File, error) { return &File{Content: resolv, Hash: hash}, nil } -// GetIfChanged retrieves the host /etc/resolv.conf file, checks against the last hash -// and, if modified since last check, returns the bytes and new hash. -// This feature is used by the resolv.conf updater for containers -func GetIfChanged() (*File, error) { - lastModified.Lock() - defer lastModified.Unlock() - - resolv, err := filesystem.ReadFile(Path()) - if err != nil { - return nil, err - } - newHash, err := hashData(bytes.NewReader(resolv)) - if err != nil { - return nil, err - } - if lastModified.sha256 != newHash { - lastModified.sha256 = newHash - lastModified.contents = resolv - return &File{Content: resolv, Hash: newHash}, nil - } - // nothing changed, so return no data - return nil, nil -} - -// GetLastModified retrieves the last used contents and hash of the host resolv.conf. -// Used by containers updating on restart -func GetLastModified() *File { - lastModified.Lock() - defer lastModified.Unlock() - - return &File{Content: lastModified.contents, Hash: lastModified.sha256} -} - // FilterResolvDNS cleans up the config in resolvConf. It has two main jobs: // 1. It looks for localhost (127.*|::1) entries in the provided // resolv.conf, removing local nameserver entries, and, if the resulting diff --git a/pkg/rootlessutil/rootlessutil_linux.go b/pkg/rootlessutil/rootlessutil_linux.go index 492160acdfa..ffc0e24a266 100644 --- a/pkg/rootlessutil/rootlessutil_linux.go +++ b/pkg/rootlessutil/rootlessutil_linux.go @@ -46,21 +46,6 @@ func ParentEUID() int { return i } -func ParentEGID() int { - if !IsRootlessChild() { - return os.Getegid() - } - env := os.Getenv("ROOTLESSKIT_PARENT_EGID") - if env == "" { - panic("environment variable ROOTLESSKIT_PARENT_EGID is not set") - } - i, err := strconv.Atoi(env) - if err != nil { - panic(fmt.Errorf("failed to parse ROOTLESSKIT_PARENT_EGID=%q: %w", env, err)) - } - return i -} - func NewRootlessKitClient() (client.Client, error) { stateDir, err := RootlessKitStateDir() if err != nil { diff --git a/pkg/testutil/compose.go b/pkg/testutil/compose.go index e247dcc7d60..679676d4227 100644 --- a/pkg/testutil/compose.go +++ b/pkg/testutil/compose.go @@ -47,10 +47,6 @@ func (cd *ComposeDir) YAMLFullPath() string { return filepath.Join(cd.dir, cd.yamlBasePath) } -func (cd *ComposeDir) Dir() string { - return cd.dir -} - func (cd *ComposeDir) ProjectName() string { return filepath.Base(cd.dir) } diff --git a/pkg/testutil/images_linux.go b/pkg/testutil/images_linux.go index c566e6274e5..87d371961e7 100644 --- a/pkg/testutil/images_linux.go +++ b/pkg/testutil/images_linux.go @@ -92,11 +92,6 @@ func GetTestImageManifestDigest(key, platform string) string { return pd.Manifest } -func GetTestImageDigest(key string) string { - im := lookup(key) - return im.Digest -} - func GetTestImageMediaType(key string) string { im := lookup(key) return im.MediaType diff --git a/pkg/testutil/testregistry/certsd_linux.go b/pkg/testutil/testregistry/certsd_linux.go deleted file mode 100644 index 2a9587e08c4..00000000000 --- a/pkg/testutil/testregistry/certsd_linux.go +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package testregistry - -import ( - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" -) - -func generateCertsd(dir string, certPath string, hostIP string, port int) error { - return (&hoststoml.HostsToml{ - CA: certPath, - }).Save(dir, hostIP, port) -} diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go deleted file mode 100644 index fec05871fd9..00000000000 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ /dev/null @@ -1,386 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package testregistry - -import ( - "fmt" - "net" - "os" - "path/filepath" - "strconv" - - "golang.org/x/crypto/bcrypt" - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" - "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" - "github.com/containerd/nerdctl/v2/pkg/testutil/testca" -) - -type RegistryServer struct { - IP net.IP - Port int - Scheme string - ListenIP net.IP - Cleanup func(err error) - Logs func() - HostsDir string // contains ":/hosts.toml" -} - -type TokenAuthServer struct { - IP net.IP - Port int - Scheme string - ListenIP net.IP - Cleanup func(err error) - Logs func() - Auth Auth - CertPath string -} - -func EnsureImages(base *testutil.Base) { - registryImage := platform.RegistryImageStable - base.Cmd("pull", "--quiet", registryImage).AssertOK() - base.Cmd("pull", "--quiet", platform.DockerAuthImage).AssertOK() - base.Cmd("pull", "--quiet", platform.KuboImage).AssertOK() -} - -func NewAuthServer(base *testutil.Base, ca *testca.CA, port int, user, pass string, tls bool) *TokenAuthServer { - EnsureImages(base) - - name := testutil.Identifier(base.T) - // listen on 0.0.0.0 to enable 127.0.0.1 - listenIP := net.ParseIP("0.0.0.0") - hostIP, err := nettestutil.NonLoopbackIPv4() - assert.NilError(base.T, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) - // Prepare configuration file for authentication server - // Details: https://github.com/cesanta/docker_auth/blob/1.7.1/examples/simple.yml - configFile, err := os.CreateTemp("", "authconfig") - assert.NilError(base.T, err, fmt.Errorf("failed creating temporary directory for config file: %w", err)) - bpass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) - assert.NilError(base.T, err, fmt.Errorf("failed bcrypt encrypting password: %w", err)) - configFileName := configFile.Name() - scheme := "http" - configContent := fmt.Sprintf(` -server: - addr: ":5100" -token: - issuer: "Acme auth server" - expiration: 900 - certificate: "/auth/domain.crt" - key: "/auth/domain.key" -users: - "%s": - password: "%s" -acl: - - match: {account: "%s"} - actions: ["*"] -`, user, string(bpass), user) - if tls { - scheme = "https" - configContent = fmt.Sprintf(` -server: - addr: ":5100" - certificate: "/auth/domain.crt" - key: "/auth/domain.key" -token: - issuer: "Acme auth server" - expiration: 900 -users: - "%s": - password: "%s" -acl: - - match: {account: "%s"} - actions: ["*"] -`, user, string(bpass), user) - } - _, err = configFile.Write([]byte(configContent)) - assert.NilError(base.T, err, fmt.Errorf("failed writing configuration: %w", err)) - - cert := ca.NewCert(hostIP.String()) - - port, err = portlock.Acquire(port) - assert.NilError(base.T, err, fmt.Errorf("failed acquiring port: %w", err)) - containerName := fmt.Sprintf("auth-%s-%d", name, port) - // Cleanup possible leftovers first - base.Cmd("rm", "-f", containerName).Run() - - cleanup := func(err error) { - result := base.Cmd("rm", "-f", containerName).Run() - errPortRelease := portlock.Release(port) - errCertClose := cert.Close() - errConfigClose := configFile.Close() - errConfigRemove := os.Remove(configFileName) - if err == nil { - assert.NilError(base.T, result.Error, fmt.Errorf("failed stopping container: %w", err)) - assert.NilError(base.T, errPortRelease, fmt.Errorf("failed releasing port: %w", err)) - assert.NilError(base.T, errCertClose, fmt.Errorf("failed cleaning certs: %w", err)) - assert.NilError(base.T, errConfigClose, fmt.Errorf("failed closing config file: %w", err)) - assert.NilError(base.T, errConfigRemove, fmt.Errorf("failed removing config file: %w", err)) - } - } - - err = func() error { - // Run authentication server - cmd := base.Cmd( - "run", - "--pull=never", - "-d", - "-p", fmt.Sprintf("%s:%d:5100", listenIP, port), - "--name", containerName, - "-v", cert.CertPath+":/auth/domain.crt", - "-v", cert.KeyPath+":/auth/domain.key", - "-v", configFileName+":/config/auth_config.yml", - testutil.DockerAuthImage, - "/config/auth_config.yml").Run() - if cmd.Error != nil { - base.T.Logf("%s:\n%s\n%s\n-------\n%s", containerName, cmd.Cmd, cmd.Stdout(), cmd.Stderr()) - return cmd.Error - } - joined := net.JoinHostPort(hostIP.String(), strconv.Itoa(port)) - _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/auth", scheme, joined), 5, true) - return err - }() - - if err != nil { - cl := base.Cmd("logs", containerName).Run() - base.T.Logf("%s:\n%s\n%s\n=========================\n%s", containerName, cl.Cmd, cl.Stdout(), cl.Stderr()) - cleanup(err) - } - assert.NilError(base.T, err, fmt.Errorf("failed starting auth container in a timely manner: %w", err)) - - return &TokenAuthServer{ - IP: hostIP, - Port: port, - Scheme: scheme, - ListenIP: listenIP, - CertPath: cert.CertPath, - Auth: &TokenAuth{ - Address: scheme + "://" + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), - CertPath: cert.CertPath, - }, - Cleanup: cleanup, - Logs: func() { - base.T.Logf("%s: %q", containerName, base.Cmd("logs", containerName).Run().String()) - }, - } - -} - -// Auth is an interface to pass to the test registry for configuring authentication -type Auth interface { - Params(*testutil.Base) []string -} - -type NoAuth struct { -} - -func (na *NoAuth) Params(base *testutil.Base) []string { - return []string{} -} - -type TokenAuth struct { - Address string - CertPath string -} - -func (ta *TokenAuth) Params(base *testutil.Base) []string { - return []string{ - "--env", "REGISTRY_AUTH=token", - "--env", "REGISTRY_AUTH_TOKEN_REALM=" + ta.Address + "/auth", - "--env", "REGISTRY_AUTH_TOKEN_SERVICE=Docker registry", - "--env", "REGISTRY_AUTH_TOKEN_ISSUER=Acme auth server", - "--env", "REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/auth/domain.crt", - "-v", ta.CertPath + ":/auth/domain.crt", - } -} - -type BasicAuth struct { - Realm string - HtFile string - Username string - Password string -} - -func (ba *BasicAuth) Params(base *testutil.Base) []string { - if ba.Realm == "" { - ba.Realm = "Basic Realm" - } - if ba.HtFile == "" && ba.Username != "" && ba.Password != "" { - pass := ba.Password - encryptedPass, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) - tmpDir, _ := os.MkdirTemp(base.T.TempDir(), "htpasswd") - ba.HtFile = filepath.Join(tmpDir, "htpasswd") - _ = filesystem.WriteFile(ba.HtFile, []byte(fmt.Sprintf(`%s:%s`, ba.Username, string(encryptedPass[:]))), 0600) - } - ret := []string{ - "--env", "REGISTRY_AUTH=htpasswd", - "--env", "REGISTRY_AUTH_HTPASSWD_REALM=" + ba.Realm, - "--env", "REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd", - } - if ba.HtFile != "" { - ret = append(ret, "-v", ba.HtFile+":/htpasswd") - } - return ret -} - -func NewRegistry(base *testutil.Base, ca *testca.CA, port int, auth Auth, boundCleanup func(error)) *RegistryServer { - EnsureImages(base) - - name := testutil.Identifier(base.T) - // listen on 0.0.0.0 to enable 127.0.0.1 - listenIP := net.ParseIP("0.0.0.0") - hostIP, err := nettestutil.NonLoopbackIPv4() - assert.NilError(base.T, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) - port, err = portlock.Acquire(port) - assert.NilError(base.T, err, fmt.Errorf("failed acquiring port: %w", err)) - - containerName := fmt.Sprintf("registry-%s-%d", name, port) - // Cleanup possible leftovers first - base.Cmd("rm", "-f", containerName).Run() - - args := []string{ - "run", - "--pull=never", - "-d", - "-p", fmt.Sprintf("%s:%d:5000", listenIP, port), - "--name", containerName, - } - scheme := "http" - var cert *testca.Cert - if ca != nil { - scheme = "https" - cert = ca.NewCert(hostIP.String(), "127.0.0.1", "localhost", "::1") - args = append(args, - "--env", "REGISTRY_HTTP_TLS_CERTIFICATE=/registry/domain.crt", - "--env", "REGISTRY_HTTP_TLS_KEY=/registry/domain.key", - "-v", cert.CertPath+":/registry/domain.crt", - "-v", cert.KeyPath+":/registry/domain.key", - ) - } - - args = append(args, auth.Params(base)...) - registryImage := testutil.RegistryImageStable - args = append(args, registryImage) - - cleanup := func(err error) { - result := base.Cmd("rm", "-f", containerName).Run() - errPortRelease := portlock.Release(port) - var errCertClose error - if cert != nil { - errCertClose = cert.Close() - } - if boundCleanup != nil { - boundCleanup(err) - } - if cert != nil && err == nil { - assert.NilError(base.T, errCertClose, fmt.Errorf("failed cleaning certificates: %w", err)) - } - if err == nil { - assert.NilError(base.T, result.Error, fmt.Errorf("failed removing container: %w", err)) - assert.NilError(base.T, errPortRelease, fmt.Errorf("failed releasing port: %w", err)) - } - } - - hostsDir, err := func() (string, error) { - hDir, err := os.MkdirTemp(base.T.TempDir(), "certs.d") - if err != nil { - return "", err - } - - if ca != nil { - err = generateCertsd(hDir, ca.CertPath, hostIP.String(), port) - if err != nil { - return "", err - } - err = generateCertsd(hDir, ca.CertPath, "127.0.0.1", port) - if err != nil { - return "", err - } - err = generateCertsd(hDir, ca.CertPath, "localhost", port) - if err != nil { - return "", err - } - if port == 443 { - err = generateCertsd(hDir, ca.CertPath, hostIP.String(), 0) - if err != nil { - return "", err - } - err = generateCertsd(hDir, ca.CertPath, "127.0.0.1", 0) - if err != nil { - return "", err - } - err = generateCertsd(hDir, ca.CertPath, "localhost", 0) - if err != nil { - return "", err - } - } - } - - cmd := base.Cmd(args...).Run() - if cmd.Error != nil { - base.T.Logf("%s:\n%s\n%s\n-------\n%s", containerName, cmd.Cmd, cmd.Stdout(), cmd.Stderr()) - return "", cmd.Error - } - - if _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s:%s/v2", scheme, hostIP.String(), strconv.Itoa(port)), 5, true); err != nil { - return "", err - } - - return hDir, nil - }() - - if err != nil { - cl := base.Cmd("logs", containerName).Run() - base.T.Logf("%s:\n%s\n%s\n=========================\n%s", containerName, cl.Cmd, cl.Stdout(), cl.Stderr()) - cleanup(err) - } - assert.NilError(base.T, err, fmt.Errorf("failed starting registry container in a timely manner: %w", err)) - - return &RegistryServer{ - IP: hostIP, - Port: port, - Scheme: scheme, - ListenIP: listenIP, - Cleanup: cleanup, - Logs: func() { - base.T.Logf("%s: %q", containerName, base.Cmd("logs", containerName).Run().String()) - }, - HostsDir: hostsDir, - } -} - -func NewWithTokenAuth(base *testutil.Base, user, pass string, port int, tls bool) *RegistryServer { - ca := testca.New(base.T) - as := NewAuthServer(base, ca, 0, user, pass, tls) - auth := &TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, - } - return NewRegistry(base, ca, port, auth, as.Cleanup) -} - -func NewWithNoAuth(base *testutil.Base, port int, tls bool) *RegistryServer { - var ca *testca.CA - if tls { - ca = testca.New(base.T) - } - return NewRegistry(base, ca, port, &NoAuth{}, nil) -} diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 603566b340c..f7fce1d7b7b 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -17,465 +17,25 @@ package testutil import ( - "encoding/json" "flag" "fmt" - "io" "os" "os/exec" "path/filepath" "runtime" "strings" - "sync" "testing" - "time" "github.com/Masterminds/semver/v3" "github.com/opencontainers/go-digest" - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" - "github.com/containerd/containerd/v2/defaults" "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" - "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) -type Base struct { - T testing.TB - Target string - DaemonIsKillable bool - EnableIPv6 bool - IPv6Compatible bool - EnableKubernetes bool - KubernetesCompatible bool - Binary string - Args []string - Env []string - Dir string -} - -// WithStdin sets the standard input of Cmd to the specified reader -func WithStdin(r io.Reader) func(*Cmd) { - return func(i *Cmd) { - i.Cmd.Stdin = r - } -} - -func (b *Base) Cmd(args ...string) *Cmd { - icmdCmd := icmd.Command(b.Binary, append(b.Args, args...)...) - icmdCmd.Env = b.Env - icmdCmd.Dir = b.Dir - cmd := &Cmd{ - Cmd: icmdCmd, - Base: b, - } - return cmd -} - -// ComposeCmd executes `nerdctl -n nerdctl-test compose` or `docker-compose` -func (b *Base) ComposeCmd(args ...string) *Cmd { - binary := b.Binary - binaryArgs := append(b.Args, append([]string{"compose"}, args...)...) - icmdCmd := icmd.Command(binary, binaryArgs...) - icmdCmd.Env = b.Env - icmdCmd.Dir = b.Dir - cmd := &Cmd{ - Cmd: icmdCmd, - Base: b, - } - return cmd -} - -func (b *Base) ComposeCmdWithHelper(helper []string, args ...string) *Cmd { - helperBin, err := exec.LookPath(helper[0]) - if err != nil { - b.T.Skipf("helper binary %q not found", helper[0]) - } - binary := b.Binary - binaryArgs := append(b.Args, append([]string{"compose"}, args...)...) - - helperArgs := helper[1:] - helperArgs = append(helperArgs, binary) - helperArgs = append(helperArgs, binaryArgs...) - icmdCmd := icmd.Command(helperBin, helperArgs...) - icmdCmd.Env = b.Env - cmd := &Cmd{ - Cmd: icmdCmd, - Base: b, - } - return cmd -} - -func (b *Base) CmdWithHelper(helper []string, args ...string) *Cmd { - helperBin, err := exec.LookPath(helper[0]) - if err != nil { - b.T.Skipf("helper binary %q not found", helper[0]) - } - helperArgs := helper[1:] - helperArgs = append(helperArgs, b.Binary) - helperArgs = append(helperArgs, b.Args...) - helperArgs = append(helperArgs, args...) - - icmdCmd := icmd.Command(helperBin, helperArgs...) - cmd := &Cmd{ - Cmd: icmdCmd, - Base: b, - } - return cmd -} - -func (b *Base) systemctlTarget() string { - if IsDocker() { - return "docker.service" - } - - return "containerd.service" -} - -func (b *Base) systemctlArgs() []string { - var systemctlArgs []string - if os.Geteuid() != 0 { - systemctlArgs = append(systemctlArgs, "--user") - } - return systemctlArgs -} - -func (b *Base) KillDaemon() { - b.T.Helper() - if !b.DaemonIsKillable { - b.T.Skip("daemon is not killable (hint: set \"-test.allow-kill-daemon\")") - } - target := b.systemctlTarget() - b.T.Logf("killing %q", target) - cmdKill := exec.Command("systemctl", - append(b.systemctlArgs(), - []string{"kill", target}...)...) - if out, err := cmdKill.CombinedOutput(); err != nil { - err = fmt.Errorf("cannot kill %q: %q: %w", target, string(out), err) - b.T.Fatal(err) - } - // the daemon should restart automatically -} - -func (b *Base) EnsureDaemonActive() { - b.T.Helper() - target := b.systemctlTarget() - b.T.Logf("checking activity of %q", target) - systemctlArgs := b.systemctlArgs() - const ( - maxRetry = 30 - sleep = 3 * time.Second - ) - for i := 0; i < maxRetry; i++ { - cmd := exec.Command("systemctl", append(systemctlArgs, "is-active", target)...) - out, err := cmd.CombinedOutput() - b.T.Logf("(retry=%d) %s", i, string(out)) - if err == nil { - // The daemon is now running, but the daemon may still refuse connections to containerd.sock - b.T.Logf("daemon %q is now running, checking whether the daemon can handle requests", target) - infoRes := b.Cmd("info").Run() - if infoRes.ExitCode == 0 { - b.T.Logf("daemon %q can now handle requests", target) - return - } - b.T.Logf("(retry=%d) %s", i, infoRes.Combined()) - } - time.Sleep(sleep) - } - b.T.Fatalf("daemon %q not running?", target) -} - -func (b *Base) DumpDaemonLogs(minutes int) { - b.T.Helper() - target := b.systemctlTarget() - cmd := exec.Command("journalctl", - append(b.systemctlArgs(), "-u", target, "--no-pager", "-S", fmt.Sprintf("%d min ago", minutes))...) - b.T.Logf("===== %v =====", cmd.Args) - out, err := cmd.CombinedOutput() - if err != nil { - b.T.Fatal(err) - } - b.T.Log(string(out)) - b.T.Log("==========") -} - -func (b *Base) InspectContainer(name string) dockercompat.Container { - cmdResult := b.Cmd("container", "inspect", name).Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var dc []dockercompat.Container - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { - b.T.Fatal(err) - } - assert.Equal(b.T, 1, len(dc)) - return dc[0] -} - -func (b *Base) InspectImage(name string) dockercompat.Image { - cmdResult := b.Cmd("image", "inspect", name).Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var dc []dockercompat.Image - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { - b.T.Fatal(err) - } - assert.Equal(b.T, 1, len(dc)) - return dc[0] -} - -func (b *Base) InspectNetwork(name string) dockercompat.Network { - cmdResult := b.Cmd("network", "inspect", name).Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var dc []dockercompat.Network - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { - b.T.Fatal(err) - } - assert.Equal(b.T, 1, len(dc)) - return dc[0] -} - -func (b *Base) InspectVolume(name string, args ...string) native.Volume { - cmd := append([]string{"volume", "inspect"}, args...) - cmd = append(cmd, name) - cmdResult := b.Cmd(cmd...).Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var dc []native.Volume - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { - b.T.Fatal(err) - } - assert.Equal(b.T, 1, len(dc)) - return dc[0] -} - -func (b *Base) Info() dockercompat.Info { - cmdResult := b.Cmd("info", "--format", "{{ json . }}").Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var info dockercompat.Info - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &info); err != nil { - b.T.Fatal(err) - } - return info -} - -func (b *Base) InfoNative() native.Info { - b.T.Helper() - if IsDocker() { - b.T.Skip("InfoNative() should not be called for non-nerdctl target") - } - cmdResult := b.Cmd("info", "--mode", "native", "--format", "{{ json . }}").Run() - assert.Equal(b.T, cmdResult.ExitCode, 0) - var info native.Info - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &info); err != nil { - b.T.Fatal(err) - } - return info -} - -func (b *Base) ContainerdAddress() string { - b.T.Helper() - if IsDocker() { - b.T.Skip("ContainerdAddress() should not be called for non-nerdctl target") - } - if os.Geteuid() == 0 { - return defaults.DefaultAddress - } - xdr, err := rootlessutil.XDGRuntimeDir() - if err != nil { - b.T.Log(err) - xdr = fmt.Sprintf("/run/user/%d", os.Geteuid()) - } - pidFile := filepath.Join(xdr, "containerd-rootless", "child_pid") - pidB, err := filesystem.ReadFile(pidFile) - if err != nil { - b.T.Fatal(err) - } - pidS := strings.TrimSpace(string(pidB)) - return filepath.Join("/proc", pidS, "root", defaults.DefaultAddress) -} - -func (b *Base) EnsureContainerStarted(con string) { - b.T.Helper() - - const ( - maxRetry = 5 - sleep = time.Second - ) - for i := 0; i < maxRetry; i++ { - if b.InspectContainer(con).State.Running { - b.T.Logf("container %s is now running", con) - return - } - b.T.Logf("(retry=%d)", i+1) - time.Sleep(sleep) - } - b.T.Fatalf("conainer %s not running", con) -} - -func (b *Base) EnsureContainerExited(con string, expectedExitCode int) { - b.T.Helper() - - const ( - maxRetry = 5 - sleep = time.Second - ) - var c dockercompat.Container - for i := 0; i < maxRetry; i++ { - c = b.InspectContainer(con) - if c.State.Status == "exited" { - b.T.Logf("container %s have exited with status %d", con, c.State.ExitCode) - if c.State.ExitCode == expectedExitCode { - return - } - break - } - b.T.Logf("(retry=%d)", i+1) - time.Sleep(sleep) - } - b.T.Fatalf("expected conainer %s to have exited with code %d, got status %+v", - con, expectedExitCode, c.State) -} - -type Cmd struct { - icmd.Cmd - *Base - runResult *icmd.Result - mu sync.Mutex -} - -func (c *Cmd) Run() *icmd.Result { - c.Base.T.Helper() - c.mu.Lock() - c.runResult = icmd.RunCmd(c.Cmd) - c.mu.Unlock() - return c.runResult -} - -func (c *Cmd) runIfNecessary() *icmd.Result { - c.Base.T.Helper() - c.mu.Lock() - if c.runResult == nil { - c.runResult = icmd.RunCmd(c.Cmd) - } - c.mu.Unlock() - return c.runResult -} - -func (c *Cmd) Start() *icmd.Result { - c.Base.T.Helper() - return icmd.StartCmd(c.Cmd) -} - -func (c *Cmd) CmdOption(cmdOptions ...func(*Cmd)) *Cmd { - for _, opt := range cmdOptions { - opt(c) - } - return c -} - -func (c *Cmd) Assert(expected icmd.Expected) { - c.Base.T.Helper() - c.runIfNecessary().Assert(c.Base.T, expected) -} - -func (c *Cmd) AssertOK() { - c.Base.T.Helper() - c.AssertExitCode(0) -} - -func (c *Cmd) AssertFail() { - c.Base.T.Helper() - res := c.runIfNecessary() - assert.Assert(c.Base.T, res.ExitCode != 0, res) -} - -func (c *Cmd) AssertExitCode(exitCode int) { - c.Base.T.Helper() - res := c.runIfNecessary() - assert.Assert(c.Base.T, res.ExitCode == exitCode, res) -} - -func (c *Cmd) AssertOutContains(s string) { - c.Base.T.Helper() - expected := icmd.Expected{ - Out: s, - } - c.Assert(expected) -} - -func (c *Cmd) AssertCombinedOutContains(s string) { - c.Base.T.Helper() - res := c.runIfNecessary() - assert.Assert(c.Base.T, strings.Contains(res.Combined(), s), fmt.Sprintf("expected output to contain %q: %q", s, res.Combined())) -} - -// AssertOutContainsAll checks if command output contains All strings in `strs`. -func (c *Cmd) AssertOutContainsAll(strs ...string) { - c.Base.T.Helper() - fn := func(stdout string) error { - for _, s := range strs { - if !strings.Contains(stdout, s) { - return fmt.Errorf("expected stdout to contain %q", s) - } - } - return nil - } - c.AssertOutWithFunc(fn) -} - -// AssertOutContainsAny checks if command output contains Any string in `strs`. -func (c *Cmd) AssertOutContainsAny(strs ...string) { - c.Base.T.Helper() - fn := func(stdout string) error { - for _, s := range strs { - if strings.Contains(stdout, s) { - return nil - } - } - return fmt.Errorf("expected stdout to contain any of %q", strings.Join(strs, "|")) - } - c.AssertOutWithFunc(fn) -} - -func (c *Cmd) AssertOutNotContains(s string) { - c.Base.T.Helper() - c.AssertOutWithFunc(func(stdout string) error { - if strings.Contains(stdout, s) { - return fmt.Errorf("expected stdout to not contain %q", s) - } - return nil - }) -} - -func (c *Cmd) AssertOutExactly(s string) { - c.Base.T.Helper() - fn := func(stdout string) error { - if stdout != s { - return fmt.Errorf("expected %q, got %q", s, stdout) - } - return nil - } - c.AssertOutWithFunc(fn) -} - -func (c *Cmd) AssertOutWithFunc(fn func(stdout string) error) { - c.Base.T.Helper() - res := c.runIfNecessary() - assert.Equal(c.Base.T, 0, res.ExitCode, res) - assert.NilError(c.Base.T, fn(res.Stdout()), res.Combined()) -} - -func (c *Cmd) Out() string { - c.Base.T.Helper() - res := c.runIfNecessary() - assert.Equal(c.Base.T, 0, res.ExitCode, res) - return res.Stdout() -} - var ( flagTestTarget string flagTestKillDaemon bool @@ -569,37 +129,6 @@ func GetAllowModifyUsers() bool { return flagTestModifyUsers } -func IsDocker() bool { - return strings.HasPrefix(filepath.Base(GetTarget()), "docker") -} - -func DockerIncompatible(t testing.TB) { - if IsDocker() { - t.Skip("test is incompatible with Docker") - } -} - -func RequiresBuild(t testing.TB) { - if !IsDocker() { - buildkitHost, err := buildkitutil.GetBuildkitHost(Namespace) - if err != nil { - t.Skipf("test requires buildkitd: %+v", err) - } - t.Logf("buildkitHost=%q", buildkitHost) - } -} - -func RequireExecPlatform(t testing.TB, ss ...string) { - ok, err := platformutil.CanExecProbably(ss...) - if !ok { - msg := fmt.Sprintf("test requires platform %v", ss) - if err != nil { - msg += fmt.Sprintf(": %v", err) - } - t.Skip(msg) - } -} - func RequireKernelVersion(t testing.TB, constraint string) { t.Helper() c, err := semver.NewConstraint(constraint) @@ -617,34 +146,6 @@ func RequireKernelVersion(t testing.TB, constraint string) { } } -func RequireContainerdPlugin(base *Base, requiredType, requiredID string, requiredCaps []string) { - base.T.Helper() - info := base.InfoNative() - for _, p := range info.Daemon.Plugins.Plugins { - if p.Type != requiredType { - continue - } - if p.ID != requiredID { - continue - } - pCapMap := make(map[string]struct{}, len(p.Capabilities)) - for _, f := range p.Capabilities { - pCapMap[f] = struct{}{} - } - for _, f := range requiredCaps { - if _, ok := pCapMap[f]; !ok { - base.T.Skipf("test requires containerd plugin \"%s.%s\" with capabilities %v (missing %q)", requiredType, requiredID, requiredCaps, f) - } - } - return - } - if len(requiredCaps) == 0 { - base.T.Skipf("test requires containerd plugin \"%s.%s\"", requiredType, requiredID) - } else { - base.T.Skipf("test requires containerd plugin \"%s.%s\" with capabilities %v", requiredType, requiredID, requiredCaps) - } -} - func RequireSystemService(t testing.TB, sv string) { t.Helper() if runtime.GOOS != "linux" { @@ -661,71 +162,8 @@ func RequireSystemService(t testing.TB, sv string) { } } -// RequireExecutable skips tests when executable `name` is not present in PATH. -func RequireExecutable(t testing.TB, name string) { - if _, err := exec.LookPath(name); err != nil { - t.Skipf("required executable doesn't exist in PATH: %s", name) - } -} - const Namespace = "nerdctl-test" -func NewBaseWithNamespace(t *testing.T, ns string) *Base { - if ns == "" || ns == "default" || ns == Namespace { - t.Fatalf(`the other base namespace cannot be "%s"`, ns) - } - return newBase(t, ns, false, false) -} - -func NewBaseWithIPv6Compatible(t *testing.T) *Base { - return newBase(t, Namespace, true, false) -} - -func NewBase(t *testing.T) *Base { - return newBase(t, Namespace, false, false) -} - -func newBase(t *testing.T, ns string, ipv6Compatible bool, kubernetesCompatible bool) *Base { - base := &Base{ - T: t, - Target: GetTarget(), - DaemonIsKillable: GetDaemonIsKillable(), - EnableIPv6: GetEnableIPv6(), - IPv6Compatible: ipv6Compatible, - EnableKubernetes: GetEnableKubernetes(), - KubernetesCompatible: kubernetesCompatible, - Env: os.Environ(), - } - if base.EnableIPv6 && !base.IPv6Compatible { - t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") - } else if !base.EnableIPv6 && base.IPv6Compatible { - t.Skip("runner skips IPv6 compatible tests in the non-IPv6 environment") - } - if base.EnableKubernetes && !base.KubernetesCompatible { - t.Skip("runner skips non-Kubernetes compatible tests in the Kubernetes environment") - } else if !base.EnableKubernetes && base.KubernetesCompatible { - t.Skip("runner skips Kubernetes compatible tests in the non-Kubernetes environment") - } - if !GetFlakyEnvironment() && !GetEnableKubernetes() && !GetEnableIPv6() { - t.Skip("legacy tests are considered flaky by default and are skipped unless in the flaky environment") - } - var err error - base.Binary, err = exec.LookPath(base.Target) - if err != nil { - t.Fatal(err) - } - - if IsDocker() { - if err = exec.Command(base.Binary, "compose", "version").Run(); err != nil { - t.Fatalf("docker does not support compose: %v", err) - } - } else { - base.Args = []string{"--namespace=" + ns} - } - - return base -} - // Identifier can be used as a name of container, image, volume, network, etc. func Identifier(t testing.TB) string { s := t.Name() @@ -738,11 +176,3 @@ func Identifier(t testing.TB) string { } return s } - -// RegisterBuildCacheCleanup adds a 'builder prune --all --force' cleanup function -// to run on test teardown. -func RegisterBuildCacheCleanup(t *testing.T) { - t.Cleanup(func() { - NewBase(t).Cmd("builder", "prune", "--all", "--force").Run() - }) -} From bf1f60ae7cea309c79d16e524cd6d04921efe0c8 Mon Sep 17 00:00:00 2001 From: Vedant Madane Date: Tue, 18 Aug 2026 22:01:22 +0530 Subject: [PATCH 746/868] docs: document volume :z and :Z SELinux options These options are implemented in mountutil; stop listing them as unimplemented and describe the selinux-enabled requirement. Fixes #3867 Signed-off-by: Vedant Madane --- docs/command-reference.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 20239a0f3b0..b97eb45b50f 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -292,7 +292,10 @@ Volume flags: - :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation - :nerd_face: option `bind`: Not-recursively bind-mounted - :nerd_face: option `rbind`: Recursively bind-mounted - - unimplemented options: `:z` and `:Z` (SELinux relabeling) + - :whale: option `z`: SELinux shared (multi-category) relabel of the volume content so it can be shared among containers + - :whale: option `Z`: SELinux private unshared relabel of the volume content for this container only + - Requires SELinux on the host and nerdctl started with `--selinux-enabled` (or `selinux_enabled = true` in `nerdctl.toml`). + - Example: `nerdctl run --rm -v /var/data:/data:Z --selinux-enabled IMAGE` - :whale: `--tmpfs`: Mount a tmpfs directory, e.g. `--tmpfs /tmp:size=64m,exec`. - :whale: `--mount`: Attach a filesystem mount to the container. Consists of multiple key-value pairs, separated by commas and each From 8cb568d94379b9ca41e9978b7036c6dc3cac3454 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:32:45 +0000 Subject: [PATCH 747/868] build(deps): bump the golang-x group with 3 updates Bumps the golang-x group with 3 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/crypto` from 0.54.0 to 0.55.0 - [Commits](https://github.com/golang/crypto/compare/v0.54.0...v0.55.0) Updates `golang.org/x/net` from 0.57.0 to 0.58.0 - [Commits](https://github.com/golang/net/compare/v0.57.0...v0.58.0) Updates `golang.org/x/text` from 0.40.0 to 0.41.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.40.0...v0.41.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.55.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/net dependency-version: 0.58.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index c0d3169b7c8..99126c7e921 100644 --- a/go.mod +++ b/go.mod @@ -64,12 +64,12 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.54.0 - golang.org/x/net v0.57.0 + golang.org/x/crypto v0.55.0 + golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 //gomodjail:unconfined golang.org/x/sys v0.47.0 //gomodjail:unconfined golang.org/x/term v0.45.0 //gomodjail:unconfined - golang.org/x/text v0.40.0 + golang.org/x/text v0.41.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index a74689e40bd..f6d474dc04d 100644 --- a/go.sum +++ b/go.sum @@ -364,8 +364,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= 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= @@ -393,8 +393,8 @@ 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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= 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= @@ -448,8 +448,8 @@ 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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= 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= From 58da3b3755e42d96b45aa7eb1951089b200274d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:32:51 +0000 Subject: [PATCH 748/868] build(deps): bump github.com/moby/sys/userns in the moby-sys group Bumps the moby-sys group with 1 update: [github.com/moby/sys/userns](https://github.com/moby/sys). Updates `github.com/moby/sys/userns` from 0.1.0 to 0.2.0 - [Release notes](https://github.com/moby/sys/releases) - [Commits](https://github.com/moby/sys/compare/user/v0.1.0...user/v0.2.0) --- updated-dependencies: - dependency-name: github.com/moby/sys/userns dependency-version: 0.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: moby-sys ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c0d3169b7c8..c3cc5ecc6a2 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.1 //gomodjail:unconfined - github.com/moby/sys/userns v0.1.0 //gomodjail:unconfined + github.com/moby/sys/userns v0.2.0 //gomodjail:unconfined github.com/moby/term v0.5.2 //gomodjail:unconfined github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined github.com/opencontainers/go-digest v1.0.0 diff --git a/go.sum b/go.sum index a74689e40bd..87e4281aea0 100644 --- a/go.sum +++ b/go.sum @@ -228,8 +228,8 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0= github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM= +github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= From 77a6a8be7e22fcf6ad240ad7608da25b7fc6b436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:33:11 +0000 Subject: [PATCH 749/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.3 to 2.3.4. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/v2.3.4/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.3...v2.3.4) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c0d3169b7c8..c80d7854b11 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.3 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.3.4 //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 diff --git a/go.sum b/go.sum index a74689e40bd..482c535f194 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,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.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k= -github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= +github.com/containerd/containerd/v2 v2.3.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI= +github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY= 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= From a2ab9f59488689af8de577a931926f406588ad94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:19:21 +0000 Subject: [PATCH 750/868] build(deps): bump github.com/containerd/accelerated-container-image Bumps [github.com/containerd/accelerated-container-image](https://github.com/containerd/accelerated-container-image) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/containerd/accelerated-container-image/releases) - [Commits](https://github.com/containerd/accelerated-container-image/compare/v1.4.3...v1.4.4) --- updated-dependencies: - dependency-name: github.com/containerd/accelerated-container-image dependency-version: 1.4.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9be2683b075..c947df03851 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.3 github.com/compose-spec/compose-go/v2 v2.14.0 //gomodjail:unconfined - github.com/containerd/accelerated-container-image v1.4.3 + github.com/containerd/accelerated-container-image v1.4.4 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 diff --git a/go.sum b/go.sum index 9e332dbacdb..09aeca7e344 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ= github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= -github.com/containerd/accelerated-container-image v1.4.3 h1:mxC5l+CDzojoqf9gGzO1yu1o/A6kzQpD74VzfOwv4OI= -github.com/containerd/accelerated-container-image v1.4.3/go.mod h1:S/kgh79iNF3QIYcztXMnYX0GhS2BS5JAkK5hD3rF2o4= +github.com/containerd/accelerated-container-image v1.4.4 h1:88mL7plI0lvrzCiU1obhB4CFE8YFWFiqzNipydqiYCM= +github.com/containerd/accelerated-container-image v1.4.4/go.mod h1:h8+s7FnzpT1fnPqH31JK9LybCqiRd1IgLgA82vtX+Tc= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= From 539c4cfb5161ff39f1b363777ef401b3c316707c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 19 Aug 2026 20:03:33 +0900 Subject: [PATCH 751/868] test: complete migration of testutil.Identifier to data.Identifier Replace the remaining callers of `testutil.Identifier(t)` with Tigron's `data.Identifier()` (or, for subtests that need the parent's name, a label set in `Setup`), and remove `testutil.Identifier`. The non-Tigron unit tests in pkg/netutil just use `t.Name()` for the scratch file name, which was irrelevant anyway. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- .../container_inspect_windows_test.go | 4 +-- .../container_remove_windows_test.go | 27 ++++++++++------- .../container_rename_windows_test.go | 4 +-- .../container/container_restart_linux_test.go | 20 ++++++------- .../container_run_security_linux_test.go | 30 ++++++++----------- .../container_run_systemd_linux_test.go | 10 +++---- .../container/container_update_linux_test.go | 2 +- pkg/netutil/netutil_test.go | 5 ++-- pkg/testutil/testutil.go | 14 --------- 9 files changed, 51 insertions(+), 65 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_windows_test.go b/cmd/nerdctl/container/container_inspect_windows_test.go index 0712319c8b6..08e22c07997 100644 --- a/cmd/nerdctl/container/container_inspect_windows_test.go +++ b/cmd/nerdctl/container/container_inspect_windows_test.go @@ -35,7 +35,7 @@ func TestInspectProcessContainerContainsLabel(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) + containerName := data.Identifier() data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) @@ -75,7 +75,7 @@ func TestInspectHyperVContainerContainsLabel(t *testing.T) { testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) + containerName := data.Identifier() data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--isolation", "hyperv", "--label", "foo=foo", "--label", "bar=bar", testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) diff --git a/cmd/nerdctl/container/container_remove_windows_test.go b/cmd/nerdctl/container/container_remove_windows_test.go index 91d9f786337..884c8c83343 100644 --- a/cmd/nerdctl/container/container_remove_windows_test.go +++ b/cmd/nerdctl/container/container_remove_windows_test.go @@ -34,10 +34,11 @@ func TestRemoveHyperVContainer(t *testing.T) { testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--isolation", "hyperv", "--name", testutil.Identifier(t), testutil.CommonImage, "sleep", nerdtest.Infinity) - nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) + data.Labels().Set("containerName", data.Identifier()) + helpers.Ensure("run", "-d", "--isolation", "hyperv", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) - inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) //check with HCS if the container is ineed a VM isHypervContainer, err := testutil.HyperVContainer(inspect) assert.NilError(t, err) @@ -45,27 +46,33 @@ func TestRemoveHyperVContainer(t *testing.T) { } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", testutil.Identifier(t)) + helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "should fail to remove when still running", NoParallel: true, - Command: test.Command("rm", testutil.Identifier(t)), - Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("rm", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "should kill the container", NoParallel: true, - Command: test.Command("kill", testutil.Identifier(t)), - Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("kill", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "should remove the container when terminated", NoParallel: true, - Command: test.Command("rm", testutil.Identifier(t)), - Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("rm", data.Labels().Get("containerName")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, } diff --git a/cmd/nerdctl/container/container_rename_windows_test.go b/cmd/nerdctl/container/container_rename_windows_test.go index fcd28e151e1..0b5f639236c 100644 --- a/cmd/nerdctl/container/container_rename_windows_test.go +++ b/cmd/nerdctl/container/container_rename_windows_test.go @@ -30,7 +30,7 @@ func TestRenameProcessContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - testContainerName := testutil.Identifier(t) + testContainerName := data.Identifier() data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) @@ -92,7 +92,7 @@ func TestRenameHyperVContainer(t *testing.T) { testCase.Require = nerdtest.HyperV testCase.Setup = func(data test.Data, helpers test.Helpers) { - testContainerName := testutil.Identifier(t) + testContainerName := data.Identifier() data.Labels().Set("containerName", testContainerName) helpers.Ensure("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity) diff --git a/cmd/nerdctl/container/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go index 9e6ca2d0e09..e72eb8608a2 100644 --- a/cmd/nerdctl/container/container_restart_linux_test.go +++ b/cmd/nerdctl/container/container_restart_linux_test.go @@ -40,22 +40,22 @@ func TestRestart(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", testutil.Identifier(t), testutil.NginxAlpineImage, "sleep", nerdtest.Infinity) - nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.NginxAlpineImage, "sleep", nerdtest.Infinity) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) - inspect := nerdtest.InspectContainer(helpers, testutil.Identifier(t)) + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) data.Labels().Set("pid", strconv.Itoa(inspect.State.Pid)) - helpers.Ensure("restart", testutil.Identifier(t)) - nerdtest.EnsureContainerStarted(helpers, testutil.Identifier(t)) + helpers.Ensure("restart", data.Identifier()) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", testutil.Identifier(t)) + helpers.Anyhow("rm", "-f", data.Identifier()) } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("inspect", testutil.Identifier(t)) + return helpers.Command("inspect", data.Identifier()) } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { @@ -80,7 +80,7 @@ func TestRestartPIDContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - baseContainerName := testutil.Identifier(t) + baseContainerName := data.Identifier() helpers.Ensure("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, baseContainerName) @@ -129,7 +129,7 @@ func TestRestartIPCContainer(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { const shmSize = "32m" - baseContainerName := testutil.Identifier(t) + baseContainerName := data.Identifier() helpers.Ensure("run", "-d", "--shm-size", shmSize, "--ipc", "shareable", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, baseContainerName) @@ -178,7 +178,7 @@ func TestRestartWithTime(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) + containerName := data.Identifier() helpers.Ensure("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index 55a1492e0ae..a7b1cb8c255 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -244,28 +244,26 @@ func TestRunApparmor(t *testing.T) { func TestRunSelinuxWithSecurityOpt(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Selinux - testContainer := testutil.Identifier(t) - testCase.SubTests = []*test.Case{ { Description: "test run with selinux-enabled", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, testutil.CommonImage, "sleep", "infinity") + return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", testContainer) + 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) { - inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) + inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", data.Identifier()) pid := strings.TrimSpace(inspectOut) fileName := fmt.Sprintf("/proc/%s/attr/current", pid) - data, err := os.ReadFile(fileName) + attr, err := os.ReadFile(fileName) assert.NilError(t, err) - assert.Equal(t, strings.Contains(string(data), "container_t"), true) + assert.Equal(t, strings.Contains(string(attr), "container_t"), true) }, ), } @@ -277,28 +275,26 @@ func TestRunSelinuxWithSecurityOpt(t *testing.T) { func TestRunSelinux(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Selinux - testContainer := testutil.Identifier(t) - testCase.SubTests = []*test.Case{ { Description: "test run with selinux-enabled", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity") + return helpers.Command("--selinux-enabled", "run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", testContainer) + 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) { - inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer) + inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", data.Identifier()) pid := strings.TrimSpace(inspectOut) fileName := fmt.Sprintf("/proc/%s/attr/current", pid) - data, err := os.ReadFile(fileName) + attr, err := os.ReadFile(fileName) assert.NilError(t, err) - assert.Equal(t, strings.Contains(string(data), "container_t"), true) + assert.Equal(t, strings.Contains(string(attr), "container_t"), true) }, ), } @@ -311,8 +307,6 @@ func TestRunSelinux(t *testing.T) { func TestRunSelinuxWithVolumeLabel(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = nerdtest.Selinux - testContainer := testutil.Identifier(t) - testCase.SubTests = []*test.Case{ { Description: "test run with selinux-enabled", @@ -320,10 +314,10 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { // The volume directory must live somewhere writable by the (possibly // rootless) user running the tests: nerdctl creates it on `run`. hostDir := data.Temp().Path("volume") - return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("%s:/mnt:Z", hostDir), "--name", testContainer, testutil.CommonImage, "sleep", "infinity") + return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("%s:/mnt:Z", hostDir), "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", testContainer) + helpers.Anyhow("rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go index b6b99f0ee4d..6835d0fb4f4 100644 --- a/cmd/nerdctl/container/container_run_systemd_linux_test.go +++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go @@ -33,7 +33,7 @@ func TestRunWithSystemdAlways(t *testing.T) { testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName", testutil.Identifier(t)) + data.Labels().Set("containerName", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -117,7 +117,7 @@ func TestRunWithSystemdTrueDisabled(t *testing.T) { ) testCase.Setup = func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName", testutil.Identifier(t)) + data.Labels().Set("containerName", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -139,7 +139,7 @@ func TestRunWithSystemdFalse(t *testing.T) { testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName", testutil.Identifier(t)) + data.Labels().Set("containerName", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -176,7 +176,7 @@ func TestRunWithNoSystemd(t *testing.T) { testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { - data.Labels().Set("containerName", testutil.Identifier(t)) + data.Labels().Set("containerName", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -229,7 +229,7 @@ func TestRunWithSystemdPrivilegedSuccess(t *testing.T) { ) testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) + containerName := data.Identifier() data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage) nerdtest.EnsureContainerStarted(helpers, containerName) diff --git a/cmd/nerdctl/container/container_update_linux_test.go b/cmd/nerdctl/container/container_update_linux_test.go index 93be65076ef..da884c06937 100644 --- a/cmd/nerdctl/container/container_update_linux_test.go +++ b/cmd/nerdctl/container/container_update_linux_test.go @@ -32,7 +32,7 @@ func TestUpdateContainer(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { - containerName := testutil.Identifier(t) + containerName := data.Identifier() data.Labels().Set("containerName", containerName) helpers.Ensure("run", "-d", "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity) nerdtest.EnsureContainerStarted(helpers, containerName) diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index e7c60d98ce1..9489541a79f 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -32,7 +32,6 @@ import ( ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" "github.com/containerd/nerdctl/v2/pkg/labels" - "github.com/containerd/nerdctl/v2/pkg/testutil" ) const testBridgeIP = "10.42.100.1/24" // nolint:unused @@ -493,7 +492,7 @@ func TestNetworkWithDefaultNameAlreadyExists(t *testing.T) { assert.NilError(t, tpl.ExecuteTemplate(buf, "test", values)) // Filename is irrelevant as long as it's not nerdctl's. - testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", testutil.Identifier(t))) + testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", t.Name())) err = filesystem.WriteFile(testConfFile, buf.Bytes(), 0600) assert.NilError(t, err) @@ -561,7 +560,7 @@ func TestListNetworksMatchIncludesPseudoNetworks(t *testing.T) { buf := &bytes.Buffer{} assert.NilError(t, tpl.ExecuteTemplate(buf, "test", values)) - testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", testutil.Identifier(t))) + testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", t.Name())) assert.NilError(t, filesystem.WriteFile(testConfFile, buf.Bytes(), 0600)) matches, errs := cniEnv.ListNetworksMatch([]string{"host", "none", "regular-network"}, true) diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index f7fce1d7b7b..a4f4f2366fa 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -27,7 +27,6 @@ import ( "testing" "github.com/Masterminds/semver/v3" - "github.com/opencontainers/go-digest" "github.com/containerd/log" @@ -163,16 +162,3 @@ func RequireSystemService(t testing.TB, sv string) { } const Namespace = "nerdctl-test" - -// Identifier can be used as a name of container, image, volume, network, etc. -func Identifier(t testing.TB) string { - s := t.Name() - s = strings.ReplaceAll(s, " ", "_") - s = strings.ReplaceAll(s, "/", "-") - s = strings.ToLower(s) - s = "nerdctl-" + s - if len(s) > 76 { - s = "nerdctl-" + digest.SHA256.FromString(t.Name()).Encoded() - } - return s -} From aba68615703469292b1e3c65460d93f1e782893f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 19 Aug 2026 20:13:41 +0900 Subject: [PATCH 752/868] test: migrate testutil.Require* to nerdtest requirements Replace `testutil.RequireKernelVersion` and `testutil.RequireSystemService` (which called t.Skip imperatively from inside a test body or Setup) with proper `*test.Requirement`s: `nerdtest.KernelVersion(constraint)` and `nerdtest.SystemService(name)`, and move the callers to `testCase.Require`. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- cmd/nerdctl/compose/compose_up_linux_test.go | 5 +-- .../container/container_inspect_linux_test.go | 7 ++- .../container_run_cgroup_linux_test.go | 7 +-- cmd/nerdctl/image/image_convert_linux_test.go | 7 ++- pkg/testutil/nerdtest/requirements.go | 44 +++++++++++++++++++ pkg/testutil/testutil.go | 40 ----------------- 6 files changed, 56 insertions(+), 54 deletions(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 4ac9af1163b..13a07eabafb 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -847,12 +847,11 @@ func TestComposeUpWithBypass4netns(t *testing.T) { testCase.Require = require.All( require.Not(nerdtest.Docker), nerdtest.Rootless, + nerdtest.KernelVersion(">= 5.9.0-0"), + nerdtest.SystemService("bypass4netnsd"), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { - testutil.RequireKernelVersion(t, ">= 5.9.0-0") - testutil.RequireSystemService(t, "bypass4netnsd") - hostPort, err := portlock.Acquire(0) if err != nil { helpers.T().Log(fmt.Sprintf("Failed to acquire port: %v", err)) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 6be9a2fd510..25056295686 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -783,14 +783,13 @@ func TestContainerInspectBlkioSettings(t *testing.T) { require.Not(nerdtest.Docker), require.Not(nerdtest.Rootless), nerdtest.CGroupV2, - ) - - testCase.Setup = func(data test.Data, helpers test.Helpers) { // See https://github.com/containerd/nerdctl/issues/4185 // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. // For now, disable the test unless on a recent kernel. - testutil.RequireKernelVersion(t, ">= 6.0.0-0") + nerdtest.KernelVersion(">= 6.0.0-0"), + ) + testCase.Setup = func(data test.Data, helpers test.Helpers) { var err error lo, err = loopback.New(4096) if err != nil { diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index 1203e71e32a..1988fa3a059 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -630,12 +630,13 @@ func TestRunBlkioWeightCgroupV2(t *testing.T) { func TestRunBlkioSettingCgroupV2(t *testing.T) { testCase := nerdtest.Setup() - testCase.Require = nerdtest.Rootful - // See https://github.com/containerd/nerdctl/issues/4185 // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. // For now, disable the test unless on a recent kernel. - testutil.RequireKernelVersion(t, ">= 6.0.0-0") + testCase.Require = require.All( + nerdtest.Rootful, + nerdtest.KernelVersion(">= 6.0.0-0"), + ) const ( weight = "150" diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 9c10b247288..cc460c189ab 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -171,10 +171,6 @@ func TestImageConvertNydusVerify(t *testing.T) { var reg *registry.Server - // It is unclear what is problematic here, but we use the kernel version to discriminate against EL - // See: https://github.com/containerd/nerdctl/issues/4332 - testutil.RequireKernelVersion(t, ">= 6.0.0-0") - testCase := &test.Case{ Require: require.All( require.Linux, @@ -184,6 +180,9 @@ func TestImageConvertNydusVerify(t *testing.T) { require.Not(nerdtest.Docker), nerdtest.Rootful, nerdtest.Registry, + // It is unclear what is problematic here, but we use the kernel version to discriminate against EL + // See: https://github.com/containerd/nerdctl/issues/4332 + nerdtest.KernelVersion(">= 6.0.0-0"), ), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index b7542e4c381..04b5473a906 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -22,6 +22,7 @@ import ( "fmt" "os/exec" "path/filepath" + "runtime" "strings" "github.com/Masterminds/semver/v3" @@ -36,6 +37,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/containerdutil" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" + "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/netutil" @@ -588,3 +590,45 @@ func CNIFirewallVersion(requiredVersion string) *test.Requirement { }, } } + +// KernelVersion requires the host kernel version to satisfy the given semver constraint (e.g. ">= 6.0.0-0"). +// If the kernel version cannot be parsed as semver, the requirement is not met. +func KernelVersion(constraint string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + c, err := semver.NewConstraint(constraint) + assert.NilError(helpers.T(), err, "invalid kernel version constraint") + // EL kernel versions are not semver, so, cleanup first + un := strings.Split(infoutil.UnameR(), "-")[0] + unameR, err := semver.NewVersion(un) + if err != nil { + return false, fmt.Sprintf("cannot parse kernel version %q: %v", un, err) + } + if !c.Check(unameR) { + return false, fmt.Sprintf("kernel version %v does not satisfy constraints %v", unameR, c) + } + return true, fmt.Sprintf("kernel version %v satisfies constraints %v", unameR, c) + }, + } +} + +// SystemService requires the given systemd service (user service when rootless) to be active. +func SystemService(sv string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + if runtime.GOOS != "linux" { + return false, fmt.Sprintf("service %q is not supported on %q", sv, runtime.GOOS) + } + var systemctlArgs []string + if rootlessutil.IsRootless() { + systemctlArgs = append(systemctlArgs, "--user") + } + systemctlArgs = append(systemctlArgs, "-q", "is-active", sv) + cmd := exec.Command("systemctl", systemctlArgs...) + if err := cmd.Run(); err != nil { + return false, fmt.Sprintf("service %q does not seem active: %v: %v", sv, cmd.Args, err) + } + return true, fmt.Sprintf("service %q is active", sv) + }, + } +} diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index a4f4f2366fa..6cfc06417ae 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -20,19 +20,12 @@ import ( "flag" "fmt" "os" - "os/exec" "path/filepath" - "runtime" - "strings" "testing" - "github.com/Masterminds/semver/v3" - "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/internal/filesystem" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" ) var ( @@ -128,37 +121,4 @@ func GetAllowModifyUsers() bool { return flagTestModifyUsers } -func RequireKernelVersion(t testing.TB, constraint string) { - t.Helper() - c, err := semver.NewConstraint(constraint) - if err != nil { - t.Fatal(err) - } - // EL kernel versions are not semver, so, cleanup first - un := strings.Split(infoutil.UnameR(), "-")[0] - unameR, err := semver.NewVersion(un) - if err != nil { - t.Skip(err) - } - if !c.Check(unameR) { - t.Skipf("version %v does not satisfy constraints %v", unameR, c) - } -} - -func RequireSystemService(t testing.TB, sv string) { - t.Helper() - if runtime.GOOS != "linux" { - t.Skipf("Service %q is not supported on %q", sv, runtime.GOOS) - } - var systemctlArgs []string - if rootlessutil.IsRootless() { - systemctlArgs = append(systemctlArgs, "--user") - } - systemctlArgs = append(systemctlArgs, []string{"-q", "is-active", sv}...) - cmd := exec.Command("systemctl", systemctlArgs...) - if err := cmd.Run(); err != nil { - t.Skipf("Service %q does not seem active: %v: %v", sv, cmd.Args, err) - } -} - const Namespace = "nerdctl-test" From ee27547527ab9ede5db592320b0bc5164817ea23 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 19 Aug 2026 20:17:01 +0900 Subject: [PATCH 753/868] test: migrate testutil.Namespace to nerdtest.Namespace Tests should read the namespace they actually run in from the Tigron config (`helpers.Read(nerdtest.Namespace)`) rather than hardcoding the `testutil.Namespace` constant, which is wrong for `nerdtest.Private` tests and for Kubernetes runs. Move the default value into nerdtest and drop `testutil.Namespace`. Assisted-by: Claude Fable 5 Signed-off-by: Akihiro Suda --- cmd/nerdctl/builder/builder_build_test.go | 4 ++-- cmd/nerdctl/builder/builder_builder_test.go | 4 ++-- cmd/nerdctl/container/container_kill_linux_test.go | 2 +- cmd/nerdctl/container/container_logs_test.go | 4 ++-- cmd/nerdctl/container/container_remove_linux_test.go | 2 +- cmd/nerdctl/container/container_run_cgroup_linux_test.go | 2 +- cmd/nerdctl/container/container_stop_linux_test.go | 2 +- pkg/testutil/nerdtest/command.go | 2 +- pkg/testutil/testutil.go | 2 -- 9 files changed, 11 insertions(+), 13 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 3d69983e1cc..b7e3a384b54 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -1051,7 +1051,7 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) Description: "build with buildkit-host", Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr - buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + buildkitAddr, err := buildkitutil.GetBuildkitHost(string(helpers.Read(nerdtest.Namespace))) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") @@ -1080,7 +1080,7 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) Description: "build with env specified", Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr - buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + buildkitAddr, err := buildkitutil.GetBuildkitHost(string(helpers.Read(nerdtest.Namespace))) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") diff --git a/cmd/nerdctl/builder/builder_builder_test.go b/cmd/nerdctl/builder/builder_builder_test.go index 75100795e70..b93a2360db9 100644 --- a/cmd/nerdctl/builder/builder_builder_test.go +++ b/cmd/nerdctl/builder/builder_builder_test.go @@ -75,7 +75,7 @@ CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr - buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + buildkitAddr, err := buildkitutil.GetBuildkitHost(string(helpers.Read(nerdtest.Namespace))) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") @@ -104,7 +104,7 @@ CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { // Get BuildkitAddr - buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + buildkitAddr, err := buildkitutil.GetBuildkitHost(string(helpers.Read(nerdtest.Namespace))) assert.NilError(helpers.T(), err) buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") diff --git a/cmd/nerdctl/container/container_kill_linux_test.go b/cmd/nerdctl/container/container_kill_linux_test.go index 6827cdce8d7..8bd2adb3e99 100644 --- a/cmd/nerdctl/container/container_kill_linux_test.go +++ b/cmd/nerdctl/container/container_kill_linux_test.go @@ -70,7 +70,7 @@ func TestKillCleanupForwards(t *testing.T) { chain = "DOCKER" } else { redirectChain := "CNI-HOSTPORT-DNAT" - chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID) + chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, string(helpers.Read(nerdtest.Namespace)), containerID) } data.Labels().Set("chain", chain) diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 983a4405d8b..a849dd1388a 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -168,7 +168,7 @@ func TestLogsWithInheritedFlags(t *testing.T) { testCase.Require = require.Not(nerdtest.Docker) testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("-n="+testutil.Namespace, "run", "--name", data.Identifier(), testutil.CommonImage, + helpers.Ensure("-n="+string(helpers.Read(nerdtest.Namespace)), "run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar") } @@ -177,7 +177,7 @@ func TestLogsWithInheritedFlags(t *testing.T) { } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("-n="+testutil.Namespace, "logs", "-n", "1", data.Identifier()) + return helpers.Command("-n="+string(helpers.Read(nerdtest.Namespace)), "logs", "-n", "1", data.Identifier()) } // FIXME: why? diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go index 57ea219fb44..3e15da38ad7 100644 --- a/cmd/nerdctl/container/container_remove_linux_test.go +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -113,7 +113,7 @@ func TestContainerRmIptables(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { // Get the container ID from the label containerID := data.Labels().Get("containerID") - id := fmt.Sprintf("%s-%s", testutil.Namespace, containerID) + id := fmt.Sprintf("%s-%s", helpers.Read(nerdtest.Namespace), containerID) chain := cniutils.FormatChainName("bridge", id) return &test.Expected{ ExitCode: expect.ExitCodeSuccess, diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index 1988fa3a059..e081bc03341 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -321,7 +321,7 @@ func TestIssue3781(t *testing.T) { assert.NilError(t, err) addr = filepath.Join("/proc", fmt.Sprintf("%d", childPid), "root", defaults.DefaultAddress) } - client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) + client, err := containerd.New(addr, containerd.WithDefaultNamespace(string(helpers.Read(nerdtest.Namespace)))) assert.NilError(t, err) defer client.Close() ctx := context.Background() diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index 388a66b8f2d..66cd3a618fa 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -181,7 +181,7 @@ func TestStopCleanupForwards(t *testing.T) { if nerdtest.IsDocker() { chain = "DOCKER" } else { - chain = iptablesutil.GetRedirectedChain(t, ipt, "CNI-HOSTPORT-DNAT", testutil.Namespace, containerID) + chain = iptablesutil.GetRedirectedChain(t, ipt, "CNI-HOSTPORT-DNAT", string(helpers.Read(nerdtest.Namespace)), containerID) } data.Labels().Set("chain", chain) diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go index 6dbad324347..bd55630e31a 100644 --- a/pkg/testutil/nerdtest/command.go +++ b/pkg/testutil/nerdtest/command.go @@ -34,7 +34,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" ) -const defaultNamespace = testutil.Namespace +const defaultNamespace = "nerdctl-test" // IMPORTANT note on file writing here: // Inside the context of a single test, there is no concurrency, as setup, command and cleanup operate in sequence diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 6cfc06417ae..0fb1204c206 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -120,5 +120,3 @@ func GetDaemonIsKillable() bool { func GetAllowModifyUsers() bool { return flagTestModifyUsers } - -const Namespace = "nerdctl-test" From e376295bbdb6779843e16074f38c6771a4e9701a Mon Sep 17 00:00:00 2001 From: MsfPablo Date: Wed, 19 Aug 2026 13:50:53 +0200 Subject: [PATCH 754/868] Fix typos in docs and a comment Signed-off-by: MsfPablo --- docs/dev/store.md | 2 +- docs/nydus.md | 2 +- .../nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md | 2 +- .../ipfs-stargz-snapshotter/README.md | 6 +++--- examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md | 2 +- pkg/ipfs/registry_ipfs.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/dev/store.md b/docs/dev/store.md index a4bd3a9b20b..bf32d6fa8d0 100644 --- a/docs/dev/store.md +++ b/docs/dev/store.md @@ -144,7 +144,7 @@ Users of the `namestore` do not have to bother with locking. These methods are s This is a good example of how to leverage core store primitives to implement a developer friendly, safe storage for "something" (in that case "names"). -Finaly note an important point - mentioned above: locking should be done to the smallest possible "segment" of sub-directories. +Finally note an important point - mentioned above: locking should be done to the smallest possible "segment" of sub-directories. Specifically, any store should lock only - at most - resources under the _namespace_ being manipulated. For example, a container lifecycle storage should not lock out any other container, but only its own private directory. diff --git a/docs/nydus.md b/docs/nydus.md index c8f912d01cf..df17626eade 100644 --- a/docs/nydus.md +++ b/docs/nydus.md @@ -37,6 +37,6 @@ Nerdctl supports to convert an OCI image or docker format v2 image to Nydus imag Before the conversion, you should have the `nydus-image` binary installed, which is contained in the ["nydus static package"](https://github.com/dragonflyoss/image-service/releases). You can run the command like `nerdctl image convert --nydus --oci --nydus-builder-path ` to convert the `` to a Nydus image whose tag is ``. -By now, the converted Nydus image cannot be run directly. It shoud be unpacked to nydus snapshotter before `nerdctl run`, which is a part of the processing flow of `nerdctl image pull`. So you need to push the converted image to a registry after the conversion and use `nerdctl --snapshotter nydus image pull` to unpack it to the nydus snapshotter before running the image. +By now, the converted Nydus image cannot be run directly. It should be unpacked to nydus snapshotter before `nerdctl run`, which is a part of the processing flow of `nerdctl image pull`. So you need to push the converted image to a registry after the conversion and use `nerdctl --snapshotter nydus image pull` to unpack it to the nydus snapshotter before running the image. Optionally, you can use the nydusify conversion tool to check if the format of the converted Nydus image is valid. For more details about the Nydus image validation and how to build Nydus image, please refer to [nydusify](https://github.com/dragonflyoss/image-service/blob/master/docs/nydusify.md) and [acceld](https://github.com/goharbor/acceleration-service). diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md index 05bd41fdc67..87c9da634dc 100644 --- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md +++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md @@ -8,7 +8,7 @@ Usage: - [`ipfs-key`](https://github.com/whyrusleeping/ipfs-key) is required (see https://ipfscluster.io/documentation/guides/k8s/) - Deploy `bootstrap.yaml` and `nerdctl-ipfs-registry.yaml` (e.g. using `kubectl apply`) - Make sure nodes contain containerd >= v1.5.8 -- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resouce requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) +- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resource requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) ## Example on kind diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md index 80db2fcd4f5..460ab3ecc3f 100644 --- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md +++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md @@ -8,7 +8,7 @@ Usage: - Deploy `bootstrap.yaml` and `nerdctl-ipfs-registry.yaml` (e.g. using `kubectl apply`) - Make sure nodes contain containerd >= v1.5.8 and stargz-snapshotter. - Here we use `ghcr.io/containerd/stargz-snapshotter:0.12.1-kind` that contains both of them. (This image requires kind >= 0.16.0) -- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resouce requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) +- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resource requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) ## About eStargz and Stargz Snapshotter @@ -64,7 +64,7 @@ $ docker exec -it kind-worker /bin/bash (kind-worker)# nerdctl rmi ghcr.io/stargz-containers/jenkins:2.60.3-esgz ``` -> NOTE: This example copies a pre-converted eStargz image (`ghcr.io/stargz-containers/jenkins:2.60.3-esgz`) from the registry to IPFS but you can push non-eStargz image to IPFS with converting it to eStargz using `--estargz` flag of `nerdctl push`. This flag automatically performs convertion of the image to eStargz. +> NOTE: This example copies a pre-converted eStargz image (`ghcr.io/stargz-containers/jenkins:2.60.3-esgz`) from the registry to IPFS but you can push non-eStargz image to IPFS with converting it to eStargz using `--estargz` flag of `nerdctl push`. This flag automatically performs conversion of the image to eStargz. The eStargz image added to `kind-worker` is shared to `kind-worker2` via IPFS. You can perform lazy pulling of this eStargz image among nodes using the following manifest. @@ -98,7 +98,7 @@ EOF > NOTE1: Kubernetes doesn't support `ipfs://CID` URL on YAML as of now so we need to use `localhost:5050/ipfs/CID` form instead. In the future, this limitation should be eliminated. -> NOTE2: stargz-snapshotter currently perfoms lazy pulling via `nerdctl ipfs registry` running on localhost instead of leveraging its [native support for fetching contents via ipfs daemon](https://github.com/containerd/stargz-snapshotter/blob/v0.12.0/docs/ipfs.md). This is because of the limitation described in NOTE1 and expected to be fixed once NOTE1 is solved. +> NOTE2: stargz-snapshotter currently performs lazy pulling via `nerdctl ipfs registry` running on localhost instead of leveraging its [native support for fetching contents via ipfs daemon](https://github.com/containerd/stargz-snapshotter/blob/v0.12.0/docs/ipfs.md). This is because of the limitation described in NOTE1 and expected to be fixed once NOTE1 is solved. The image runs on all nodes. You may observe faster pulling of the image by eStargz. diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md index 53ef383802f..c0c61b22595 100644 --- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md +++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md @@ -7,7 +7,7 @@ Usage: - [`ipfs-swarm-key-gen`](https://github.com/Kubuxu/go-ipfs-swarm-key-gen) is required (see https://github.com/ipfs/kubo/blob/v0.15.0/docs/experimental-features.md#private-networks) - Deploy `bootstrap.yaml` and `nerdctl-ipfs-registry.yaml` (e.g. using `kubectl apply`) - Make sure nodes contain containerd >= v1.5.8 -- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resouce requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) +- You might want to change some configuration written in `nerdctl-ipfs-registry.yaml` (e.g. [chaning profile based on your node's resource requirements](https://docs.ipfs.tech/how-to/default-profile/#available-profiles)) ## Example on kind diff --git a/pkg/ipfs/registry_ipfs.go b/pkg/ipfs/registry_ipfs.go index 915947310a2..cd883389c0c 100644 --- a/pkg/ipfs/registry_ipfs.go +++ b/pkg/ipfs/registry_ipfs.go @@ -126,7 +126,7 @@ func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, e } func (s *server) serveContentByCID(ctx context.Context, targetCID string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) { - // TODO: make sure cidStr is a vaild CID? + // TODO: make sure cidStr is a valid CID? c, desc, err := s.resolveCIDOfRootBlob(ctx, targetCID) if err != nil { return "", nil, "", 0, err From 349e1a8042563bce763245e2eb089b5d272cb2d9 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Wed, 19 Aug 2026 21:43:32 +0300 Subject: [PATCH 755/868] feat(push): support --all-tags to push all tags `nerdctl push` accepts a bare repository name, but referenceutil.Parse normalizes it to ":latest", so only that single tag is pushed. Add the Docker-compatible `-a, --all-tags` flag, which pushes every local tag of the repository instead. Push is split into a dispatcher and pushSingle(): without --all-tags the dispatcher just delegates, with it the local tags are resolved through the `name~=^:` image filter and pushed one by one. The temporary images push creates for itself are skipped, so an interrupted push cannot leak a "-tmp-reduced-platform" tag into the registry, and the list is sorted because ImageService().List() guarantees no order. A tag or a digest in the reference is rejected, as docker does. The check looks at ExplicitTag rather than Tag: Parse() runs TagNameOnly(), so Tag is "latest" even for a bare repository name. A SOCI index is attached to the image manifest rather than to the tag, so it is now built once per distinct target digest. Pushing several tags of one image no longer makes each tag overwrite the index pushed by the previous one. Pushing more than once per process also uncovered a bug in the plain HTTP fallback. pushImageWithLocal builds a fresh in-memory tracker per push, but the fallback rebuilt the resolver through dockerconfigresolver.New, which silently substitutes the process-wide PushTracker. containerd's dockerPusher keys that tracker by content ref ("index-"), not by reference, and returns ErrAlreadyExists before issuing any request when the digest is already committed; remotes.push() treats that as success, so the manifest PUT that creates the tag never happens and the command still exits 0. Rebuild the resolver from the host options instead, reusing the resolver options assembled above so the fallback keeps the per-push tracker. The tests assert that the pushed tags are present in the registry rather than that they are the only ones: the listing is a superset, since a SOCI index is attached through the referrers fallback tag ("sha256-") on registries without the referrers API. That fallback tag is also what the SOCI sub-test checks to confirm the index reached the registry, since a push without SOCI never creates one. Closes #3751 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/image/image_push.go | 7 + cmd/nerdctl/image/image_push_linux_test.go | 147 +++++++++++++++++++++ docs/command-reference.md | 3 +- pkg/api/types/image_types.go | 2 + pkg/cmd/image/push.go | 82 +++++++++++- 5 files changed, 235 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/image/image_push.go b/cmd/nerdctl/image/image_push.go index 47104a4b7e5..f0535d9bce9 100644 --- a/cmd/nerdctl/image/image_push.go +++ b/cmd/nerdctl/image/image_push.go @@ -47,6 +47,8 @@ func PushCommand() *cobra.Command { cmd.Flags().Bool("all-platforms", false, "Push content for all platforms") // #endregion + cmd.Flags().BoolP("all-tags", "a", false, "Push all tags of an image to the repository") + cmd.Flags().Bool("estargz", false, "Convert the image into eStargz") cmd.Flags().Bool("ipfs-ensure-image", true, "Ensure the entire contents of the image is locally available before push") cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") @@ -85,6 +87,10 @@ func pushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) { if err != nil { return types.ImagePushOptions{}, err } + allTags, err := cmd.Flags().GetBool("all-tags") + if err != nil { + return types.ImagePushOptions{}, err + } estargz, err := cmd.Flags().GetBool("estargz") if err != nil { return types.ImagePushOptions{}, err @@ -119,6 +125,7 @@ func pushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) { SociOptions: sociOptions, Platforms: platform, AllPlatforms: allPlatforms, + AllTags: allTags, Estargz: estargz, IpfsEnsureImage: ipfsEnsureImage, IpfsAddress: ipfsAddress, diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index c547341d012..82ac577f59d 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -17,9 +17,12 @@ package image import ( + "encoding/json" "errors" "fmt" "net/http" + "slices" + "strings" "testing" "gotest.tools/v3/assert" @@ -278,7 +281,151 @@ func TestPush(t *testing.T) { }, Expected: test.Expects(0, nil, nil), }, + { + Description: "all tags", + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + testImageRepo := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) + data.Labels().Set("testImageRepo", testImageRepo) + helpers.Ensure("tag", testutil.CommonImage, testImageRepo+":v1") + helpers.Ensure("tag", testutil.CommonImage, testImageRepo+":v2") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("testImageRepo") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRepo")+":v1") + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRepo")+":v2") + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("push", "--insecure-registry", "--all-tags", data.Labels().Get("testImageRepo")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + assertRegistryHasTags(t, registryNoAuthHTTPRandom, data.Identifier(), "v1", "v2") + }, + } + }, + }, + { + Description: "all tags, with a tag", + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:v1", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) + data.Labels().Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("push", "--insecure-registry", "--all-tags", data.Labels().Get("testImageRef")) + }, + Expected: test.Expects(1, []error{errors.New("tag can't be used with --all-tags/-a")}, nil), + }, + { + Description: "all tags, no local tag", + Require: require.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testImageRepo := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) + return helpers.Command("push", "--insecure-registry", "--all-tags", testImageRepo) + }, + Expected: test.Expects(1, []error{errors.New("an image does not exist locally with the tag")}, nil), + }, + { + Description: "all tags, soci", + Require: require.All( + nerdtest.Soci, + require.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.UbuntuImage) + testImageRepo := fmt.Sprintf("%s:%d/%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier()) + data.Labels().Set("testImageRepo", testImageRepo) + helpers.Ensure("tag", testutil.UbuntuImage, testImageRepo+":v1") + helpers.Ensure("tag", testutil.UbuntuImage, testImageRepo+":v2") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("testImageRepo") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRepo")+":v1") + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRepo")+":v2") + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--all-tags", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Labels().Get("testImageRepo")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + assertRegistryHasTags(t, registryNoAuthHTTPRandom, data.Identifier(), "v1", "v2") + assertRegistrySociIndex(t, registryNoAuthHTTPRandom, data.Identifier()) + }, + } + }, + }, }, } testCase.Run(t) } + +// referrersFallbackTagPrefix starts the tag under which the distribution spec has a registry +// without the referrers API keep the artifacts referring to a manifest ("sha256-"). +const referrersFallbackTagPrefix = "sha256-" + +// registryTags returns the tags the registry lists for the repository `repo`. +func registryTags(t tig.T, reg *registry.Server, repo string) []string { + t.Helper() + + tagsURL := fmt.Sprintf("http://%s:%d/v2/%s/tags/list", reg.IP.String(), reg.Port, repo) + resp, err := http.Get(tagsURL) + assert.NilError(t, err, "error making http request") + defer func() { + if resp.Body != nil { + _ = resp.Body.Close() + } + }() + assert.Equal(t, resp.StatusCode, http.StatusOK, "tag list should be available") + + var tagList struct { + Name string `json:"name"` + Tags []string `json:"tags"` + } + assert.NilError(t, json.NewDecoder(resp.Body).Decode(&tagList), "error decoding the tag list") + + return tagList.Tags +} + +// assertRegistryHasTags verifies the registry lists every tag of `want` for the repository `repo`. +// The listing legitimately holds more than the pushed tags: a SOCI index adds a referrers fallback +// tag, and a re-run of the test hits a repository the previous run already populated. +func assertRegistryHasTags(t tig.T, reg *registry.Server, repo string, want ...string) { + t.Helper() + + tags := registryTags(t, reg, repo) + for _, tag := range want { + assert.Assert(t, slices.Contains(tags, tag), "expected tag %q in %v", tag, tags) + } +} + +// assertRegistrySociIndex verifies a SOCI index was pushed to the repository `repo`. +// +// The test registry is distribution 2.x, which predates the referrers API, so SOCI attaches its +// index through the referrers fallback tag. A push without SOCI never creates such a tag, so its +// presence is what tells the index apart from the tags of the image itself. +func assertRegistrySociIndex(t tig.T, reg *registry.Server, repo string) { + t.Helper() + + tags := registryTags(t, reg, repo) + found := slices.ContainsFunc(tags, func(tag string) bool { + return strings.HasPrefix(tag, referrersFallbackTagPrefix) + }) + assert.Assert(t, found, "expected a SOCI index referrers tag in %v", tags) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 20239a0f3b0..db3c961ef51 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -899,6 +899,7 @@ Flags: - :nerd_face: `--platform=(amd64|arm64|...)`: Push content for a specific platform - :nerd_face: `--all-platforms`: Push content for all platforms +- :whale: `-a, --all-tags`: Push all tags of an image to the repository. `NAME` must not contain a tag. - :nerd_face: `--sign`: Sign the image (none|cosign|notation). See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md) for details. - :nerd_face: `--cosign-key`: Path to the private key file, KMS, URI or Kubernetes Secret for `--sign=cosign` - :nerd_face: `--notation-key-name`: Signing key name for a key previously added to notation's key list for `--sign=notation` @@ -908,7 +909,7 @@ Flags: - :nerd_face: `--soci-span-size`: Span size in bytes that soci index uses to segment layer data. Default is 4 MiB. - :nerd_face: `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB. -Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true) +Unimplemented `docker push` flags: `--disable-content-trust` (default true) ### :whale: nerdctl load diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 4bea77dcf8b..503f2a3c776 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -205,6 +205,8 @@ type ImagePushOptions struct { Platforms []string // AllPlatforms convert content for all platforms AllPlatforms bool + // AllTags push all the tags of the repository named by the reference + AllTags bool // Estargz convert image to sStargz Estargz bool diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go index 505e8eb7129..b56c0b5f02f 100644 --- a/pkg/cmd/image/push.go +++ b/pkg/cmd/image/push.go @@ -24,6 +24,9 @@ import ( "net/http" "os" "path/filepath" + "regexp" + "slices" + "strings" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -57,8 +60,72 @@ import ( "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" ) +const ( + // Suffixes of the temporary images push creates for itself before uploading them. + tmpReducedPlatformSuffix = "-tmp-reduced-platform" + tmpEsgzSuffix = "-tmp-esgz" +) + // Push pushes an image specified by `rawRef`. +// With options.AllTags, `rawRef` must be a bare repository name, and every local tag of that +// repository is pushed. func Push(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePushOptions) error { + if !options.AllTags { + return pushSingle(ctx, client, rawRef, options, false) + } + + parsedReference, err := referenceutil.Parse(rawRef) + if err != nil { + return err + } + // ExplicitTag, not Tag: Parse normalizes a bare repository name to ":latest". + if parsedReference.ExplicitTag != "" || parsedReference.Digest != "" { + return errors.New("tag can't be used with --all-tags/-a") + } + if parsedReference.Protocol != "" { + return fmt.Errorf("--all-tags is not supported for %q references", parsedReference.Protocol) + } + + imgs, err := localTags(ctx, client, parsedReference.Name()) + if err != nil { + return err + } + if len(imgs) == 0 { + return fmt.Errorf("an image does not exist locally with the tag: %s", parsedReference.Name()) + } + + // A SOCI index is attached to the image manifest rather than to the tag, so it only needs to be + // built once per distinct target. Doing it per tag makes every tag overwrite the index pushed by + // the previous one: https://github.com/containerd/nerdctl/issues/3751 + indexed := make(map[digest.Digest]struct{}, len(imgs)) + for _, img := range imgs { + _, alreadyIndexed := indexed[img.Target.Digest] + if err = pushSingle(ctx, client, img.Name, options, alreadyIndexed); err != nil { + return err + } + indexed[img.Target.Digest] = struct{}{} + } + return nil +} + +// localTags returns the local images tagged under the repository `name`, sorted by name. +func localTags(ctx context.Context, client *containerd.Client, name string) ([]images.Image, error) { + imgs, err := client.ImageService().List(ctx, fmt.Sprintf("name~=^%s:", regexp.QuoteMeta(name))) + if err != nil { + return nil, err + } + // Drop the temporary images push creates for itself, which an interrupted push may have left behind. + imgs = slices.DeleteFunc(imgs, func(img images.Image) bool { + return strings.HasSuffix(img.Name, tmpReducedPlatformSuffix) || strings.HasSuffix(img.Name, tmpEsgzSuffix) + }) + // ImageService().List does not guarantee an order, and the order decides which tag gets indexed. + slices.SortFunc(imgs, func(a, b images.Image) int { + return strings.Compare(a.Name, b.Name) + }) + return imgs, nil +} + +func pushSingle(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePushOptions, alreadyIndexed bool) error { parsedReference, err := referenceutil.Parse(rawRef) if err != nil { return err @@ -120,7 +187,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options } pushRef := ref if !options.AllPlatforms { - pushRef = ref + "-tmp-reduced-platform" + pushRef = ref + tmpReducedPlatformSuffix // Push fails with "400 Bad Request" when the manifest is multi-platform but we do not locally have multi-platform blobs. // So we create a tmp reduced-platform image to avoid the error. // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 @@ -140,7 +207,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options } if options.Estargz { - pushRef = ref + "-tmp-esgz" + pushRef = ref + tmpEsgzSuffix esgzImg, err := nerdconverter.Convert(ctx, client, pushRef, ref, converter.WithPlatform(platMC), converter.WithLayerConvertFunc(eStargzConvertFunc())) if err != nil { return fmt.Errorf("failed to convert to eStargz: %v", err) @@ -185,7 +252,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options options.SignOptions); err != nil { return err } - if options.GOptions.Snapshotter == "soci" { + if options.GOptions.Snapshotter == "soci" && !alreadyIndexed { if err = snapshotterutil.CreateSociIndexV1(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil { return err } @@ -281,11 +348,16 @@ func pushImageWithLocal(ctx context.Context, client *containerd.Client, parsedRe if options.GOptions.InsecureRegistry { log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) - resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...) + // Rebuild the resolver rather than calling dockerconfigresolver.New, which would fall + // back to the process-wide dockerconfigresolver.PushTracker. That tracker is keyed by + // digest, not by reference, so a second push of an already-pushed digest short-circuits + // with ErrAlreadyExists and its tag is never written to the registry. + ho, err = dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...) if err != nil { return err } - return pushFunc(resolver) + resolverOpts.Hosts = dockerconfig.ConfigureHosts(ctx, *ho) + return pushFunc(docker.NewResolver(resolverOpts)) } log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") From 9d1ff9247125ad52e15a84b8e5e4e0865d632d83 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Wed, 5 Aug 2026 21:23:48 +0300 Subject: [PATCH 756/868] fix(image): pin the image a container runs to its digest `nerdctl images` marks an image as in use by resolving the image name stored on the container, which follows the tag wherever it points now. After `nerdctl tag` moves a tag onto another image, the container gets attributed to an image it never ran: the U indicator lands on the wrong row. Record the image target digest on the container at creation time, in a new nerdctl/image-digest label, and use it for the in-use lookup. Containers created before this label existed, or created outside nerdctl, are still resolved by name; an unparsable value falls back the same way rather than dropping the container from the set. That digest is also what `nerdctl inspect` now reports as Image, where Docker reports the image ID: with the containerd image store that ID is the digest of the image target (moby daemon/containerd/image.go, image.ID(img.Target.Digest)), pinned on the container when it is created. nerdctl used to report the image name there, which a retag moves just the same. The reference the user asked for stays in Config.Image, as it does in Docker. This also matters for the ACTIVE and RECLAIMABLE columns of `nerdctl system df`, which build on the same lookup. Signed-off-by: Eugene Kalinin --- .../container/container_inspect_linux_test.go | 8 ++- cmd/nerdctl/image/image_list_test.go | 62 +++++++++++++++++++ pkg/cmd/container/create.go | 12 ++++ pkg/cmd/image/list.go | 44 +++++++++++-- pkg/cmd/image/list_test.go | 45 ++++++++++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 20 +++++- .../dockercompat/dockercompat_test.go | 30 +++++++++ pkg/labels/labels.go | 5 ++ 8 files changed, 219 insertions(+), 7 deletions(-) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 6be9a2fd510..b80387ee07c 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -278,7 +278,7 @@ func TestContainerInspectConfigImage(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ - Description: "Container inspect contains Config.Image field", + Description: "Container inspect names the image by digest, and by reference in Config.Image", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.AlpineImage, "sleep", nerdtest.Infinity) }, @@ -297,6 +297,12 @@ func TestContainerInspectConfigImage(t *testing.T) { container := containers[0] assert.Assert(tt, container.Config != nil, "container Config should not be nil") assert.Assert(tt, container.Config.Image != "", "Config.Image should not be empty") + // Docker identifies the image a container runs by digest, pinned at creation, and + // keeps the reference the user asked for in Config.Image. + assert.Assert(tt, strings.HasPrefix(container.Image, "sha256:"), + "Image should be a digest, got %q", container.Image) + assert.Assert(tt, !strings.HasPrefix(container.Config.Image, "sha256:"), + "Config.Image should be a reference, got %q", container.Config.Image) }), } diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index f9f5ad3a594..4281b0339a1 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -38,6 +38,17 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) +// padRow widens a row of a table back to the width of its header, so that its last column can be +// read. tabutil indexes the columns by byte offset and slices without checking the bounds, and a +// row can be shorter than the header in two ways: the trailing column is empty, and the padding of +// the very last line is gone once the output has been trimmed. +func padRow(header, row string) string { + if pad := len(header) - len(row); pad > 0 { + return row + strings.Repeat(" ", pad) + } + return row +} + // TestNameFilterFor is a regression test for // https://github.com/containerd/nerdctl/issues/5113: `nerdctl image ls // myapp`, where myapp is a bare repository name, returned nothing unless @@ -207,6 +218,57 @@ func TestImages(t *testing.T) { } }, }, + { + Description: "In use survives a retag", + Setup: func(data test.Data, helpers test.Helpers) { + // Run a container off a private tag, then move that tag onto another image. + // The container still runs the original image, so that is the one that must + // stay marked as in use. + helpers.Ensure("tag", commonImage.String(), data.Identifier()+":moving") + helpers.Ensure("run", "-d", "--quiet", "--name", data.Identifier(), + data.Identifier()+":moving", "sleep", nerdtest.Infinity) + helpers.Ensure("tag", testutil.NginxAlpineImage, data.Identifier()+":moving") + + nginx, _ := referenceutil.Parse(testutil.NginxAlpineImage) + data.Labels().Set("retaggedTo", nginx.FamiliarName()+":"+nginx.Tag) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", data.Identifier()+":moving") + }, + Command: test.Command("images"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "there should be at least two lines\n") + tab := tabutil.NewReader("IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "ParseHeader should not fail\n") + + original := commonImage.FamiliarName() + ":" + commonImage.Tag + retagged := data.Labels().Get("retaggedTo") + seen := 0 + for _, line := range lines[1:] { + line = padRow(lines[0], line) + image, _ := tab.ReadRow(line, "IMAGE") + extra, _ := tab.ReadRow(line, "EXTRA") + switch image { + case original: + assert.Equal(t, extra, "U", + "the image the container runs must stay in use: "+image) + seen++ + case retagged: + assert.Equal(t, extra, "", + "the image the tag now points at is not in use: "+image) + seen++ + } + } + assert.Equal(t, seen, 2, "both images should be listed\n") + }, + } + }, + }, }, } diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 83abc56ad0b..e2f89159d95 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -215,6 +215,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.user = ensuredImage.ImageConfig.User } + // Pin the image the container is created from. containerd only records the image name, and a + // name can later be retagged onto a different image. + if ensuredImage != nil && ensuredImage.Image != nil { + internalLabels.imageDigest = ensuredImage.Image.Target().Digest.String() + } + // Override it if User is passed if options.User != "" { internalLabels.user = options.User @@ -811,6 +817,8 @@ type internalLabels struct { domainname string // automatically generated stateDir string + // the digest of the image target the container was created from + imageDigest string // network networks []string ipAddress string @@ -919,6 +927,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO return nil, err } + if internalLabels.imageDigest != "" { + m[labels.ImageDigest] = internalLabels.imageDigest + } + if len(internalLabels.mountPoints) > 0 { mounts := dockercompatMounts(internalLabels.mountPoints) jsonMountBytes, err := json.Marshal(mounts) diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go index f87fce0a272..703baa6679a 100644 --- a/pkg/cmd/image/list.go +++ b/pkg/cmd/image/list.go @@ -48,6 +48,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerdutil" "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) @@ -575,15 +576,50 @@ func imagesInUse(ctx context.Context, client *containerd.Client) map[digest.Dige return inUse } for _, container := range containerList { - image, err := container.Image(ctx) - if err != nil { - continue + if dgst, ok := containerImageDigest(ctx, container); ok { + inUse[dgst] = true } - inUse[image.Target().Digest] = true } return inUse } +// containerImageDigest returns the image target a container was created from. +// +// The digest is read from the label nerdctl records at creation time. Resolving the image name +// instead would follow the tag wherever it points now: after `nerdctl tag` moves a tag onto another +// image, the container would be attributed to an image it never ran. Containers created before this +// label existed, or outside nerdctl, still have to be resolved by name. +func containerImageDigest(ctx context.Context, container containerd.Container) (digest.Digest, bool) { + // The already-loaded metadata carries the labels, so this costs no extra round trip. + if info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata); err == nil { + if dgst, ok := pinnedImageDigest(info.Labels); ok { + return dgst, true + } + } + + image, err := container.Image(ctx) + if err != nil { + return "", false + } + return image.Target().Digest, true +} + +// pinnedImageDigest returns the image target digest a container pinned at creation time. An +// unparsable value is treated as absent, so that a hand-edited label degrades to resolving the +// image by name rather than dropping the container from the in-use set. +func pinnedImageDigest(containerLabels map[string]string) (digest.Digest, bool) { + value := containerLabels[labels.ImageDigest] + if value == "" { + return "", false + } + dgst, err := digest.Parse(value) + if err != nil { + log.L.Debugf("ignoring invalid %s label value %q", labels.ImageDigest, value) + return "", false + } + return dgst, true +} + func isAttestationManifestDescriptor(desc ocispec.Descriptor) bool { const manifestReferenceType = "vnd.docker.reference.type" const attestationManifest = "attestation-manifest" diff --git a/pkg/cmd/image/list_test.go b/pkg/cmd/image/list_test.go index da83f8fc769..ccfa2a1338a 100644 --- a/pkg/cmd/image/list_test.go +++ b/pkg/cmd/image/list_test.go @@ -22,6 +22,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/core/images" + + "github.com/containerd/nerdctl/v2/pkg/labels" ) func TestNewViewImageRef(t *testing.T) { @@ -75,3 +77,46 @@ func TestSortByImageRef(t *testing.T) { assert.Equal(t, img.Name, expected[i]) } } + +func TestPinnedImageDigest(t *testing.T) { + t.Parallel() + + const pinned = "sha256:09538a1f51d3ec5af0449a1640937dfdf79b0e9b8c4da5b8a883086d5c1492ef" + + testCases := []struct { + name string + containerLabels map[string]string + expected string + }{ + { + name: "pinned at creation", + containerLabels: map[string]string{labels.ImageDigest: pinned}, + expected: pinned, + }, + { + // Containers created before the label existed, or outside nerdctl, have to be resolved + // by image name instead. + name: "no label", + containerLabels: map[string]string{labels.Platform: "linux/amd64"}, + }, + { + name: "empty label", + containerLabels: map[string]string{labels.ImageDigest: ""}, + }, + { + // Falling back to the name is better than dropping the container from the in-use set. + name: "unparsable label", + containerLabels: map[string]string{labels.ImageDigest: "not-a-digest"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + dgst, ok := pinnedImageDigest(tc.containerLabels) + assert.Equal(t, ok, tc.expected != "") + assert.Equal(t, string(dgst), tc.expected) + }) + } +} diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index f05f091def0..befcf4818d3 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -340,13 +340,28 @@ var defaultCaps = map[string]struct{}{ "CAP_AUDIT_WRITE": {}, } +// containerImage is the image the container was created from, the way Docker identifies it: by +// digest, pinned when the container was created. containerd only records the image name, and a name +// can later be retagged onto another image, so it is not an answer. The name is still the fallback +// for the containers created before that digest was recorded, or created outside nerdctl. +// +// With the containerd image store, the image ID Docker reports here is the digest of the image +// target (moby daemon/containerd/image.go, image.ID(img.Target.Digest)), which is what nerdctl +// pins. +func containerImage(n *native.Container) string { + if dgst := n.Labels[labels.ImageDigest]; dgst != "" { + return dgst + } + return n.Image +} + // ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container. func ContainerFromNative(n *native.Container) (*Container, error) { var hostname string c := &Container{ ID: n.ID, Created: n.CreatedAt.Format(time.RFC3339Nano), - Image: n.Image, + Image: containerImage(n), Name: n.Labels[labels.Name], Driver: n.Snapshotter, // XXX is this always right? what if the container OS is NOT the same as the host OS? @@ -576,7 +591,8 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.State = cs c.Config = &Config{ Labels: n.Labels, - Image: c.Image, + // Docker keeps the reference the user asked for here, and the digest in Image above. + Image: n.Image, } if exposedPortsJSON := n.Labels[labels.ExposedPorts]; exposedPortsJSON != "" { var exposedPorts nat.PortSet diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 17c21f6155c..ea326e7fadf 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -418,6 +418,36 @@ func TestContainerFromNative(t *testing.T) { } } +func TestContainerFromNativeImage(t *testing.T) { + const ( + ref = "example.com/foo:latest" + digest = "sha256:0168606be2318a4b6a9ad9e5a6d9dbf1b0a6d7e2c8c4a1b0e5d3f2a1c0b9e8d7" + ) + + // Docker names the image a container was created from by digest, and keeps the reference the + // user asked for in Config.Image. + pinned, err := ContainerFromNative(&native.Container{ + Container: containers.Container{ + Image: ref, + Labels: map[string]string{labels.ImageDigest: digest}, + }, + Spec: &specs.Spec{}, + }) + assert.NilError(t, err) + assert.Equal(t, pinned.Image, digest) + assert.Equal(t, pinned.Config.Image, ref) + + // A container created before that digest was recorded, or created outside nerdctl, is left + // with the name it has. + unpinned, err := ContainerFromNative(&native.Container{ + Container: containers.Container{Image: ref}, + Spec: &specs.Spec{}, + }) + assert.NilError(t, err) + assert.Equal(t, unpinned.Image, ref) + assert.Equal(t, unpinned.Config.Image, ref) +} + func TestGetCapabilitiesFromNative(t *testing.T) { // Build the full default bounding set for test fixtures. allDefaults := []string{ diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index dd32cca0616..9f689ff7cff 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -87,6 +87,11 @@ const ( // Platform is the normalized platform string like "linux/ppc64le". Platform = Prefix + "platform" + // ImageDigest is the digest of the image target the container was created from. The image name + // stored by containerd can be retagged to point at something else, so it is not enough to tell + // which image a container actually uses. + ImageDigest = Prefix + "image-digest" + // Mounts is the mount points for the container. Mounts = Prefix + "mounts" From bd223ccf09daf3a1975830097bf60a98453b4d28 Mon Sep 17 00:00:00 2001 From: ChengyuZhu6 Date: Wed, 19 Aug 2026 14:47:49 +0800 Subject: [PATCH 757/868] ci: upgrade containerd to v2.4.0-beta.0 Signed-off-by: ChengyuZhu6 --- Dockerfile | 2 +- go.mod | 11 ++++----- go.sum | 43 ++++++++++------------------------- hack/generate-release-note.sh | 2 +- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/Dockerfile b/Dockerfile index d3bd3b66493..4c0765fea69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.3@aad11006b869517fcd3009450b6f82da282e1a9b +ARG CONTAINERD_VERSION=v2.4.0-beta.0@58e6cee9dee67ee87ee2f727d3570009050f9954 ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY diff --git a/go.mod b/go.mod index 9be2683b075..b6371737866 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/containerd/accelerated-container-image v1.4.3 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.4 //gomodjail:unconfined + github.com/containerd/containerd/api v1.12.0-beta.0 + github.com/containerd/containerd/v2 v2.4.0-beta.0 //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 @@ -38,7 +38,7 @@ require ( github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.1 - github.com/fsnotify/fsnotify v1.9.0 //gomodjail:unconfined + github.com/fsnotify/fsnotify v1.10.1 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.2 @@ -71,7 +71,7 @@ require ( golang.org/x/term v0.45.0 //gomodjail:unconfined golang.org/x/text v0.40.0 gotest.tools/v3 v3.5.2 - tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined + tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160 //gomodjail:unconfined ) require ( @@ -102,7 +102,6 @@ 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.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 @@ -111,7 +110,6 @@ require ( github.com/multiformats/go-multibase v0.3.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect - github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -154,7 +152,6 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/google/uuid v1.6.0 // 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 9e332dbacdb..c55bafabb13 100644 --- a/go.sum +++ b/go.sum @@ -16,9 +16,6 @@ github.com/Microsoft/hcsshim v0.15.0-rc.3 h1:ZTNzOp0QwJ1EiL3zopSOawIG0j7zAvzJx0r github.com/Microsoft/hcsshim v0.15.0-rc.3/go.mod h1:VhDiwXgb8cEJxO9H57YL4NNIYqvZKpqvSDcimLyo7m8= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -36,10 +33,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= 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.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI= -github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY= +github.com/containerd/containerd/api v1.12.0-beta.0 h1:0eGYh95iM9nn8Fygpx+96LvYIuc5xOiHvwo5hrsvTlI= +github.com/containerd/containerd/api v1.12.0-beta.0/go.mod h1:/tQDq0fxPDGz9vrSpfhmFMfoR7s/uzvYMCY/qKygl9Y= +github.com/containerd/containerd/v2 v2.4.0-beta.0 h1:uFwtEE0kwGngxOT0WVW3/mozESCVg+8hKUExSis7SLY= +github.com/containerd/containerd/v2 v2.4.0-beta.0/go.mod h1:GOAqkxqN53nSzBcz9Y5jAM9JELRMo67r34O/HDCY4Ro= 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= @@ -109,8 +106,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I= -github.com/erofs/go-erofs v0.3.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= +github.com/erofs/go-erofs v0.3.1 h1:Sux82Jq9yvyYhIoLgSHDp741p/+370HsOj9dAh1+VVs= +github.com/erofs/go-erofs v0.3.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -119,8 +116,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ= github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -166,10 +163,6 @@ github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDp github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= @@ -196,8 +189,8 @@ github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXN github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= -github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= +github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -214,14 +207,10 @@ github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJ github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= github.com/moby/moby/v2 v2.0.0-beta.21 h1:LrUr8ocwGt3nOdPRKmLMKRRPqYqKJP4VbZxT2vZwscs= github.com/moby/moby/v2 v2.0.0-beta.21/go.mod h1:Myh7qqKNMQ1bdk8kRugUA/xhlEUIw/drvN/h0atw4y0= -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.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.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= @@ -258,8 +247,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= 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.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= @@ -317,12 +304,6 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 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= @@ -511,7 +492,7 @@ sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= -tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= +tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160 h1:ZusVLZsIsAXuoxfJNIv3bL2Io7pKQyx0zaMbgRTz+X8= +tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160/go.mod h1:Q9xVPbYCl0qhaLAPYQ7Dra+Bd51FsEUaGQb12MXxVb4= tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/hack/generate-release-note.sh b/hack/generate-release-note.sh index 9d5a49d7f4f..2a53e39e1db 100755 --- a/hack/generate-release-note.sh +++ b/hack/generate-release-note.sh @@ -25,7 +25,7 @@ cat <<-EOX (To be documented) ## Compatible containerd versions -This release of nerdctl is expected to be used with containerd v1.7, v2.0, v2.1, v2.2, or v2.3. +This release of nerdctl is expected to be used with containerd v1.7 or later. Some features may not work with other releases of containerd. ## About the binaries From e7da47550709e0cda49101363eb87b5c8f80af32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 06:14:58 +0000 Subject: [PATCH 758/868] build(deps): bump github.com/containerd/platforms Bumps [github.com/containerd/platforms](https://github.com/containerd/platforms) from 1.0.0-rc.4 to 1.0.0-rc.5. - [Release notes](https://github.com/containerd/platforms/releases) - [Commits](https://github.com/containerd/platforms/compare/v1.0.0-rc.4...v1.0.0-rc.5) --- updated-dependencies: - dependency-name: github.com/containerd/platforms dependency-version: 1.0.0-rc.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 122e40d7052..28fb08dd207 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined - github.com/containerd/platforms v1.0.0-rc.4 //gomodjail:unconfined + github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined 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 diff --git a/go.sum b/go.sum index d82bf1f442b..22caf1db116 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk= github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY= -github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= -github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= +github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= From 6bf82524b8e95b729592201f4c6142e95b54c3d6 Mon Sep 17 00:00:00 2001 From: An Lu Date: Thu, 20 Aug 2026 20:26:04 +0800 Subject: [PATCH 759/868] Fix foreground stdio deadlock when the internal logging process stops consuming `nerdctl run` in the foreground tees container stdout/stderr through a 64 KiB pipe to a forked logging process (`nerdctl _NERDCTL_INTERNAL_LOGGING`) on the same goroutine that drains the container's stdio FIFOs. If that logging process ever stopped consuming (killed, crashed, OOM), the tee write blocked forever: nerdctl kept its own copies of the logger pipe read ends open, so the kernel never delivered EPIPE. The blocked goroutine stopped draining the stdout FIFO, the container's writer blocked behind the full pipe at exactly pipe-capacity-plus-one-chunk bytes, the container never exited, `nerdctl run` never returned, and `nerdctl rm -f` on the wedged container hung as well. Four changes, from the analysis in #5137: - Close the parent's copies of the logger pipe read ends once the logging process has started (the equivalent of containerd's binaryIO.CloseAfterStart), so that logger death turns into EPIPE on the tee instead of an eternal pipe-buffer block. - Make the logger leg of the stdio tee best-effort: on the first failed write, warn and stop writing to the logger, but keep streaming to the attached stdout/stderr. Closing the read ends alone is not enough: the EPIPE would error the io.MultiWriter, abort the io.CopyBuffer that drains the container's stdio FIFO, and the container would still wedge on the undrained FIFO chain behind it. With both changes the attach and the container survive logger death; the log file is what goes incomplete (with a warning on nerdctl's stderr). - In the logging process, do not treat an errored delivery on the task wait channel as a container exit. containerd's client sends Wait RPC failures through the same channel as a synthetic ExitStatus; cancelling the stdio readers on such a delivery silently stopped all logging while the container was still running - and, before the changes above, wedged the foreground attach permanently. Re-arm the wait instead, and close each containerd client once its wait delivers so re-arming does not accumulate open clients. - Fail IO setup when a binary-v2 logging binary exits before signalling readiness (mirroring containerd's n == 0 check); plain binary:// keeps EOF-as-ready for backward compatibility with third-party logging binaries. Fixes #5137 Signed-off-by: An Lu --- pkg/cioutil/container_io.go | 83 ++++++++++++++++++++-- pkg/cioutil/container_io_test.go | 77 +++++++++++++++++++++ pkg/logging/logging.go | 72 ++++++++++++++++--- pkg/logging/logging_test.go | 115 +++++++++++++++++++++++++++++++ 4 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 pkg/cioutil/container_io_test.go diff --git a/pkg/cioutil/container_io.go b/pkg/cioutil/container_io.go index 22dd6b4a0a5..041b5374f8e 100644 --- a/pkg/cioutil/container_io.go +++ b/pkg/cioutil/container_io.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/process" "github.com/containerd/containerd/v2/defaults" "github.com/containerd/containerd/v2/pkg/cio" + "github.com/containerd/log" ) const binaryIOProcTermTimeout = 12 * time.Second // Give logger process 10 seconds for cleanup @@ -52,6 +53,44 @@ var bufPool = sync.Pool{ }, } +// closeOnce wraps f's Close so that extra calls return the first result +// instead of "file already closed". The logger pipe ends below are closed +// individually on the success path, but also sit in the error-path closers +// list of NewContainerIO. +func closeOnce(f *os.File) func() error { + var once sync.Once + var err error + return func() error { + once.Do(func() { err = f.Close() }) + return err + } +} + +// bestEffortWriter forwards writes to w until one fails, then silently +// discards all further writes. The pipe feeding the logging binary is wrapped +// in this before it joins the stdio tee of a foreground container: logging is +// best-effort there, and a dead logging binary (EPIPE once our read ends are +// closed, see NewContainerIO) must not error the whole tee — that would stop +// the copier that drains the container's stdio FIFO and deadlock the +// container. The attach keeps streaming; the log is what goes incomplete. +// https://github.com/containerd/nerdctl/issues/5137 +type bestEffortWriter struct { + w io.Writer + dead bool +} + +func (b *bestEffortWriter) Write(p []byte) (int, error) { + // Only ever called from the single stdio copy goroutine of its stream, so + // no locking is needed. + if !b.dead { + if _, err := b.w.Write(p); err != nil { + b.dead = true + log.L.WithError(err).Warn("writing container output to the logging binary failed; further output will not be logged") + } + } + return len(p), nil +} + func (c *ncio) Config() cio.Config { return c.config } @@ -156,19 +195,23 @@ func NewContainerIO(namespace string, logURI string, tty bool, stdin io.Reader, if err != nil { return nil, err } - closers = append(closers, stdoutr.Close, stdoutw.Close) + closeStdoutR := closeOnce(stdoutr) + closers = append(closers, closeStdoutR, stdoutw.Close) stderrr, stderrw, err := os.Pipe() if err != nil { return nil, err } - closers = append(closers, stderrr.Close, stderrw.Close) + closeStderrR := closeOnce(stderrr) + closers = append(closers, closeStderrR, stderrw.Close) r, w, err := os.Pipe() if err != nil { return nil, err } - closers = append(closers, r.Close, w.Close) + closeR := closeOnce(r) + closeW := closeOnce(w) + closers = append(closers, closeR, closeW) u, err := url.Parse(logURI) if err != nil { @@ -184,18 +227,44 @@ func NewContainerIO(namespace string, logURI string, tty bool, stdin io.Reader, closers = append(closers, func() error { return cmd.Process.Kill() }) // close our side of the pipe after start - if err := w.Close(); err != nil { + if err := closeW(); err != nil { return nil, fmt.Errorf("failed to close write pipe after start: %w", err) } + // Close our copies of the stdio read ends that were handed to the + // logging binary; the child holds its own duplicates via ExtraFiles. + // This is the equivalent of containerd's binaryIO.CloseAfterStart. + // If this process kept the read ends open, a logging binary that + // stops reading (killed, crashed, ...) would never surface as EPIPE + // on the tee writes below: the stdio copy goroutine would block + // forever on the full pipe, stop draining the container's stdout + // FIFO, and deadlock both the container and `nerdctl run` itself + // (including `nerdctl rm -f` of the wedged container). + // https://github.com/containerd/nerdctl/issues/5137 + if err := closeStdoutR(); err != nil { + return nil, fmt.Errorf("failed to close stdout pipe read end after start: %w", err) + } + if err := closeStderrR(); err != nil { + return nil, fmt.Errorf("failed to close stderr pipe read end after start: %w", err) + } + // wait for the logging binary to be ready + // For binary-v2, readiness requires a byte to be written before close. + // For binary, EOF is treated as ready for backward compatibility. b := make([]byte, 1) - if _, err := r.Read(b); err != nil && err != io.EOF { + n, err := r.Read(b) + if err != nil && err != io.EOF { return nil, fmt.Errorf("failed to read from logging binary: %w", err) } + if u.Scheme == "binary-v2" && n == 0 { + return nil, errors.New("logging binary did not call ready (it may have crashed or exited prematurely)") + } + if err := closeR(); err != nil { + return nil, fmt.Errorf("failed to close ready pipe read end: %w", err) + } - stdoutWriters = append(stdoutWriters, stdoutw) - stderrWriters = append(stderrWriters, stderrw) + stdoutWriters = append(stdoutWriters, &bestEffortWriter{w: stdoutw}) + stderrWriters = append(stderrWriters, &bestEffortWriter{w: stderrw}) } streams.Stdout = io.MultiWriter(stdoutWriters...) diff --git a/pkg/cioutil/container_io_test.go b/pkg/cioutil/container_io_test.go new file mode 100644 index 00000000000..8ba699f7c8b --- /dev/null +++ b/pkg/cioutil/container_io_test.go @@ -0,0 +1,77 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cioutil + +import ( + "errors" + "os" + "testing" +) + +type failingWriter struct { + calls int +} + +func (f *failingWriter) Write(p []byte) (int, error) { + f.calls++ + return 0, errors.New("broken pipe") +} + +// TestBestEffortWriter verifies that a failing logger pipe never errors the +// stdio tee: the first failed write disables the writer and every write still +// reports full success, so the copier draining the container's stdio keeps +// running. Regression test for +// https://github.com/containerd/nerdctl/issues/5137 +func TestBestEffortWriter(t *testing.T) { + fw := &failingWriter{} + b := &bestEffortWriter{w: fw} + + for i := 0; i < 3; i++ { + n, err := b.Write([]byte("data")) + if err != nil { + t.Fatalf("write %d: best-effort writer must not return an error, got %v", i, err) + } + if n != 4 { + t.Fatalf("write %d: expected n=4, got %d", i, n) + } + } + if fw.calls != 1 { + t.Fatalf("expected the underlying writer to be abandoned after the first failure, got %d calls", fw.calls) + } +} + +// TestBestEffortWriterClosedPipe exercises the real failure mode: writing to +// an os.Pipe whose read end is closed (EPIPE), as happens when the logging +// binary dies after our copies of its read ends were closed. +func TestBestEffortWriterClosedPipe(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + r.Close() + defer w.Close() + + b := &bestEffortWriter{w: w} + for i := 0; i < 2; i++ { + if n, err := b.Write([]byte("data")); err != nil || n != 4 { + t.Fatalf("write %d: expected (4, nil), got (%d, %v)", i, n, err) + } + } + if !b.dead { + t.Fatal("expected the writer to be marked dead after EPIPE") + } +} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index a93594bd8c0..59bb9cc0db8 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -200,16 +200,37 @@ func getContainerWait(ctx context.Context, address string, config *logging.Confi if err != nil { return nil, err } + // closeAfterDelivery forwards the first delivery from ch and then closes + // the client, so that callers which re-arm the wait (see the wait loop in + // loggingProcessAdapter) do not accumulate open clients. + closeAfterDelivery := func(ch <-chan containerd.ExitStatus) <-chan containerd.ExitStatus { + out := make(chan containerd.ExitStatus, 1) + go func() { + defer close(out) + defer client.Close() + if status, ok := <-ch; ok { + out <- status + } + }() + return out + } con, err := client.LoadContainer(ctx, config.ID) if err != nil { + client.Close() return nil, err } task, err := con.Task(ctx, nil) if err == nil { - return task.Wait(ctx) + exitCh, err := task.Wait(ctx) + if err != nil { + client.Close() + return nil, err + } + return closeAfterDelivery(exitCh), nil } if !errdefs.IsNotFound(err) { + client.Close() return nil, err } @@ -232,16 +253,24 @@ func getContainerWait(ctx context.Context, address string, config *logging.Confi for { select { case <-ctx.Done(): + client.Close() return nil, errors.New("timed out waiting for container task to start") case <-ticker.C: task, err = con.Task(ctx, nil) if err == nil { - return task.Wait(ctx) + exitCh, err := task.Wait(ctx) + if err != nil { + client.Close() + return nil, err + } + return closeAfterDelivery(exitCh), nil } if !errdefs.IsNotFound(err) { + client.Close() return nil, err } if outputSeen() { + client.Close() return alreadyExited(), nil } } @@ -250,6 +279,10 @@ func getContainerWait(ctx context.Context, address string, config *logging.Confi type ContainerWaitFunc func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) +// containerWaitRetryDelay is how long the logger waits before re-arming the +// container wait after the wait channel delivered an error instead of an exit. +const containerWaitRetryDelay = 1 * time.Second + func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore, address string, getContainerWait ContainerWaitFunc, config *logging.Config) error { if err := driver.PreProcess(ctx, dataStore, config); err != nil { return err @@ -374,15 +407,34 @@ func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore, addres // keeps the stdio FIFO write ends open (so the container can be // restarted), so the FIFOs may not reach EOF on exit; without this the // read goroutines, and therefore the logger, could block forever. - exitCh, err := getContainerWait(ctx, address, config, outputSeen) - if err != nil { - // We could not determine when the container exits. Do not cancel the - // readers: they will finish on their own when the FIFO reaches EOF. - // Cancelling here could truncate a still-running container. - log.G(ctx).Errorf("failed to get container task wait channel: %v", err) - return + for { + exitCh, err := getContainerWait(ctx, address, config, outputSeen) + if err != nil { + // We could not determine when the container exits. Do not cancel the + // readers: they will finish on their own when the FIFO reaches EOF. + // Cancelling here could truncate a still-running container. + log.G(ctx).Errorf("failed to get container task wait channel: %v", err) + return + } + status := <-exitCh + if status.Error() == nil { + // The container has exited. + break + } + // The channel delivered a Wait RPC error, not a container exit: + // containerd's client sends Wait failures through the same channel + // as a synthetic ExitStatus (client/task.go). Treating that as an + // exit would cancel the readers, and with them all logging, while + // the container is still running. Re-arm the wait instead. + // https://github.com/containerd/nerdctl/issues/5137 + log.G(ctx).WithError(status.Error()).Warn("error while waiting for container exit; retrying") + select { + case <-ctx.Done(): + // SIGTERM: the goroutine above already cancels the readers. + return + case <-time.After(containerWaitRetryDelay): + } } - <-exitCh stdoutR.Cancel() stderrR.Cancel() }() diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go index ecab183bebc..f5411f7953a 100644 --- a/pkg/logging/logging_test.go +++ b/pkg/logging/logging_test.go @@ -20,6 +20,7 @@ import ( "bufio" "bytes" "context" + "errors" "math/rand" "os" "strings" @@ -148,6 +149,120 @@ func TestLoggingProcessAdapter(t *testing.T) { // stream. The container's stdio FIFOs are modelled with os.Pipe; closing the // write end models the container exiting and containerd closing the FIFO. // Regression test for https://github.com/containerd/nerdctl/issues/5006 + +// TestLoggingProcessAdapterTrailingChunk verifies that the logger forwards all +// of the container's output, including a final chunk that has no trailing +// newline, rather than holding that chunk back until something closes the +// stream. The container's stdio FIFOs are modelled with os.Pipe; closing the +// write end models the container exiting and containerd closing the FIFO. +// Regression test for https://github.com/containerd/nerdctl/issues/5006 + +// TestLoggingProcessAdapterWaitError verifies that the logger does not treat a +// Wait failure as a container exit. containerd's client delivers Wait RPC +// errors through the exit channel as a synthetic ExitStatus carrying an error; +// if the logger cancelled its readers on such a delivery, all logging would +// silently stop while the container keeps running — and, in the foreground +// attach path, wedge `nerdctl run` behind the no-longer-drained logger pipes. +// The logger must instead re-arm the wait and keep reading until a real exit +// arrives. Regression test for +// https://github.com/containerd/nerdctl/issues/5137 +func TestLoggingProcessAdapterWaitError(t *testing.T) { + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + stderrR, stderrW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer stdoutR.Close() + defer stderrR.Close() + defer stderrW.Close() + + driver := &SyncMockDriver{} + config := &logging.Config{ + Stdout: stdoutR, + Stderr: stderrR, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // The first wait channel delivers a Wait RPC error (a synthetic exit); the + // second delivers a real exit once the test has verified that logging + // survived the first delivery. + rearmed := make(chan struct{}) + realExitCh := make(chan containerd.ExitStatus, 1) + var waitCalls int + var getContainerWaitMock ContainerWaitFunc = func(ctx context.Context, address string, config *logging.Config, outputSeen func() bool) (<-chan containerd.ExitStatus, error) { + waitCalls++ + if waitCalls == 1 { + errChan := make(chan containerd.ExitStatus, 1) + errChan <- *containerd.NewExitStatus(255, time.Time{}, errors.New("transient wait RPC failure")) + return errChan, nil + } + close(rearmed) + return realExitCh, nil + } + + done := make(chan error, 1) + go func() { + done <- loggingProcessAdapter(ctx, driver, "testDataStore", "", getContainerWaitMock, config) + }() + + if _, err := stdoutW.Write([]byte("before wait error\n")); err != nil { + t.Fatal(err) + } + + // The logger must re-arm the wait rather than cancel its readers. + select { + case <-rearmed: + case <-time.After(30 * time.Second): + t.Fatal("logger did not re-arm the container wait after the wait channel delivered an error") + } + + // Output produced after the errored delivery must still be logged. + if _, err := stdoutW.Write([]byte("after wait error\n")); err != nil { + t.Fatal(err) + } + deadline := time.Now().Add(30 * time.Second) + for { + driver.mu.Lock() + got := strings.Join(driver.receivedStdout, "") + driver.mu.Unlock() + if strings.Contains(got, "after wait error") { + break + } + if time.Now().After(deadline) { + t.Fatalf("output written after the errored wait delivery was never logged; got stdout: %q", got) + } + time.Sleep(10 * time.Millisecond) + } + + // A real exit must still terminate the logger. Close both write ends so + // the stream readers finish via EOF: on Windows, cancelreader cannot + // cancel a blocked pipe read, so the readers must not be left waiting on + // an open pipe when the exit is delivered. + stdoutW.Close() + stderrW.Close() + realExitCh <- containerd.ExitStatus{} + select { + case err := <-done: + if err != nil { + t.Fatal(err) + } + case <-time.After(30 * time.Second): + t.Fatal("logger did not terminate on the real container exit") + } + + driver.mu.Lock() + defer driver.mu.Unlock() + stdout := strings.Join(driver.receivedStdout, "") + if !strings.Contains(stdout, "before wait error") || !strings.Contains(stdout, "after wait error") { + t.Fatalf("expected stdout to contain output from before and after the errored wait delivery, got: %q", stdout) + } +} + func TestLoggingProcessAdapterTrailingChunk(t *testing.T) { const expected = "'Hello World!\nThere is no newline'" From 1af88ef741fd74204fef1de58d5ca2d91b68fcd4 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Wed, 19 Aug 2026 23:00:24 +0300 Subject: [PATCH 760/868] feat(logging): add --log-file to persist nerdctl's own log nerdctl only reports its diagnostics on the standard error, so nothing survives the process. When a container fails to be created there is no record left to look at, and containerd does not log the client side of the failure either. Add a global --log-file (also log_file in nerdctl.toml and $NERDCTL_LOG_FILE) that appends nerdctl's own log to a file, in addition to the standard error. Every terminal error funnels through log.L.Fatal in main(), so the failure that ends the command is recorded together with everything logged on the way there. Combine with --debug for a full trace. The output is attached as a logrus hook rather than by replacing Logger.Out with an io.MultiWriter: the formatter picks its output style by type-asserting Logger.Out to *os.File, so a MultiWriter would silently change the console format whenever the flag is used. The file is opened in append mode so concurrent invocations can share it. SetLogFile hands the handle back so a library consumer can release it; the CLI does not, since log.L.Fatal exits the process. Fixes #4872 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/helpers/flagutil.go | 5 + cmd/nerdctl/image/image_convert_test.go | 1 + cmd/nerdctl/main.go | 8 ++ cmd/nerdctl/main_test.go | 52 ++++++++++ docs/command-reference.md | 3 + docs/config.md | 2 + pkg/config/config.go | 2 + pkg/logging/file_hook.go | 90 +++++++++++++++++ pkg/logging/file_hook_test.go | 126 ++++++++++++++++++++++++ 9 files changed, 289 insertions(+) create mode 100644 pkg/logging/file_hook.go create mode 100644 pkg/logging/file_hook_test.go diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index 1ebed1f35d1..514651d3235 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -85,6 +85,10 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) if err != nil { return types.GlobalCommandOptions{}, err } + logFile, err := cmd.Flags().GetString("log-file") + if err != nil { + return types.GlobalCommandOptions{}, err + } address, err := cmd.Flags().GetString("address") if err != nil { return types.GlobalCommandOptions{}, err @@ -167,6 +171,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) return types.GlobalCommandOptions{ Debug: debug, DebugFull: debugFull, + LogFile: logFile, Address: address, Namespace: namespace, Snapshotter: snapshotter, diff --git a/cmd/nerdctl/image/image_convert_test.go b/cmd/nerdctl/image/image_convert_test.go index c9ab36ff2e5..266056506a4 100644 --- a/cmd/nerdctl/image/image_convert_test.go +++ b/cmd/nerdctl/image/image_convert_test.go @@ -65,6 +65,7 @@ func addRootFlagsForConvertOptionsTest(t *testing.T, cmd *cobra.Command) { flags := cmd.Flags() flags.Bool("debug", false, "") flags.Bool("debug-full", false, "") + flags.String("log-file", "", "") flags.String("address", "", "") flags.String("namespace", "default", "") flags.String("snapshotter", "", "") diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 17d679f84c8..4d6ad706dca 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -170,6 +170,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode") rootCmd.PersistentFlags().Bool("debug-full", cfg.DebugFull, "debug mode (with full output)") + helpers.AddPersistentStringFlag(rootCmd, "log-file", nil, nil, nil, aliasToBeInherited, cfg.LogFile, "NERDCTL_LOG_FILE", "Append nerdctl's own log to this file, in addition to the standard error") // -a is aliases (conflicts with nerdctl images -a) helpers.AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`) // -n is aliases (conflicts with nerdctl logs -n) @@ -243,6 +244,13 @@ Config file ($NERDCTL_TOML): %s if debug { log.SetLevel(log.DebugLevel.String()) } + if globalOptions.LogFile != "" { + // The handle is deliberately not kept: log.L.Fatal terminates the process, + // so a deferred Close would not run anyway. + if _, err = logging.SetLogFile(globalOptions.LogFile); err != nil { + return err + } + } address := globalOptions.Address if strings.Contains(address, "://") && !strings.HasPrefix(address, "unix://") { return fmt.Errorf("invalid address %q", address) diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index 71bc19d1db5..4400599c1d0 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -22,10 +22,13 @@ import ( "strings" "testing" + "gotest.tools/v3/assert" + "github.com/containerd/containerd/v2/defaults" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -133,6 +136,55 @@ version = 2`), testCase.Run(t) } +// TestLogFile tests https://github.com/containerd/nerdctl/issues/4872 +func TestLogFile(t *testing.T) { + testCase := nerdtest.Setup() + + // Docker has no equivalent of --log-file + testCase.Require = require.Not(nerdtest.Docker) + + const logFile = "nerdctl.log" + + testCase.SubTests = []*test.Case{ + { + Description: "records the failure that is only reported on the standard error", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--log-file", data.Temp().Path(logFile), "non-existent-command") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("unknown subcommand")}, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(data.Temp().Load(logFile), "unknown subcommand"), + "log file must contain the error") + }, + } + }, + }, + { + Description: "appends, so that a previous invocation is not lost", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Fail("--log-file", data.Temp().Path(logFile), "non-existent-command") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--log-file", data.Temp().Path(logFile), "non-existent-command") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Output: func(stdout string, t tig.T) { + assert.Equal(t, strings.Count(data.Temp().Load(logFile), "unknown subcommand"), 2, + "log file must hold both invocations") + }, + } + }, + }, + } + + testCase.Run(t) +} + func TestRootHelpHidesAliasImplementationFlags(t *testing.T) { app, err := newApp() if err != nil { diff --git a/docs/command-reference.md b/docs/command-reference.md index b97eb45b50f..5f5d3887252 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -2012,6 +2012,9 @@ Flags: - Default: the IP address of the host - :nerd_face: `--userns-remap=:`: Support idmapping of containers. This options is only supported on rootful linux for container create and run if a user name and optionally group name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively. Note: `--userns-remap` is not supported for building containers. Nerdctl Build doesn't support userns-remap feature. (format: [:]) - :nerd_face: `--selinux-enabled`: Enable selinux support +- :nerd_face: `--log-file`: Append nerdctl's own log to this file, in addition to the standard error [`$NERDCTL_LOG_FILE`] + - Combine with `--debug` to record a full trace, e.g. to diagnose a failing `nerdctl run` + - The file is appended to, never truncated, so concurrent nerdctl invocations can share it. Rotation is left to `logrotate` or an equivalent The global flags can be also specified in `/etc/nerdctl/nerdctl.toml` (rootful) and `~/.config/nerdctl/nerdctl.toml` (rootless). See [`./config.md`](./config.md). diff --git a/docs/config.md b/docs/config.md index 605a776a68e..d371db50623 100644 --- a/docs/config.md +++ b/docs/config.md @@ -20,6 +20,7 @@ The path can be overridden with `$NERDCTL_TOML`. debug = false debug_full = false +log_file = "/var/log/nerdctl.log" address = "unix:///run/k3s/containerd/containerd.sock" namespace = "k8s.io" snapshotter = "stargz" @@ -39,6 +40,7 @@ selinux_enabled= true |---------------------|------------------------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| | `debug` | `--debug` | | Debug mode | Since 0.16.0 | | `debug_full` | `--debug-full` | | Debug mode (with full output) | Since 0.16.0 | +| `log_file` | `--log-file` | `$NERDCTL_LOG_FILE` | Append nerdctl's own log to this file, in addition to the standard error. Combine with `debug` to record a full trace | Since 2.4.0 | | `address` | `--address`,`--host`,`-a`,`-H` | `$CONTAINERD_ADDRESS` | containerd address | Since 0.16.0 | | `namespace` | `--namespace`,`-n` | `$CONTAINERD_NAMESPACE` | containerd namespace | Since 0.16.0 | | `snapshotter` | `--snapshotter`,`--storage-driver` | `$CONTAINERD_SNAPSHOTTER` | containerd snapshotter | Since 0.16.0 | diff --git a/pkg/config/config.go b/pkg/config/config.go index e967c91393a..337d5c68b98 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,6 +28,7 @@ import ( type Config struct { Debug bool `toml:"debug"` DebugFull bool `toml:"debug_full"` + LogFile string `toml:"log_file,omitempty"` Address string `toml:"address"` Namespace string `toml:"namespace"` Snapshotter string `toml:"snapshotter"` @@ -56,6 +57,7 @@ func New() *Config { return &Config{ Debug: false, DebugFull: false, + LogFile: "", Address: defaults.DefaultAddress, Namespace: namespaces.Default, Snapshotter: defaults.DefaultSnapshotter, diff --git a/pkg/logging/file_hook.go b/pkg/logging/file_hook.go new file mode 100644 index 00000000000..c5677464e40 --- /dev/null +++ b/pkg/logging/file_hook.go @@ -0,0 +1,90 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package logging + +import ( + "fmt" + "io" + "maps" + "os" + "slices" + "strings" + "sync" + + "github.com/containerd/log" +) + +// fileHook mirrors nerdctl's own diagnostic log (not the container logs) to an +// additional writer. +// +// A hook is used rather than log.L.Logger.SetOutput(io.MultiWriter(...)) so that +// the console output keeps its current formatting: the formatter picks its output +// style by type-asserting Logger.Out to *os.File, which an io.MultiWriter is not. +type fileHook struct { + mu sync.Mutex + w io.Writer +} + +// Levels implements the logrus Hook interface. Entries are already filtered against +// the logger level before the hooks are fired, so all levels are accepted here. +func (h *fileHook) Levels() []log.Level { + return []log.Level{ + log.PanicLevel, + log.FatalLevel, + log.ErrorLevel, + log.WarnLevel, + log.InfoLevel, + log.DebugLevel, + log.TraceLevel, + } +} + +// Fire implements the logrus Hook interface. The record format is deliberately +// independent of the console formatter, which varies with TTY detection. +func (h *fileHook) Fire(entry *log.Entry) error { + var sb strings.Builder + sb.WriteString(entry.Time.Format(log.RFC3339NanoFixed)) + sb.WriteString(" ") + sb.WriteString(strings.ToUpper(entry.Level.String())) + sb.WriteString(" ") + sb.WriteString(entry.Message) + for _, k := range slices.Sorted(maps.Keys(entry.Data)) { + fmt.Fprintf(&sb, " %s=%q", k, fmt.Sprint(entry.Data[k])) + } + sb.WriteString("\n") + + h.mu.Lock() + defer h.mu.Unlock() + _, err := io.WriteString(h.w, sb.String()) + return err +} + +// SetLogFile makes nerdctl append its own diagnostic log to path, in addition to +// the current output. The file is opened in append mode, so concurrent nerdctl +// invocations can share it. +// +// The returned io.Closer releases the file. The nerdctl CLI does not use it, as +// log.L.Fatal terminates the process and the hook writes are not buffered, but a +// library consumer has to be able to give the handle back. +func SetLogFile(path string) (io.Closer, error) { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) + if err != nil { + return nil, fmt.Errorf("failed to open log file %q: %w", path, err) + } + log.L.Logger.AddHook(&fileHook{w: f}) + return f, nil +} diff --git a/pkg/logging/file_hook_test.go b/pkg/logging/file_hook_test.go new file mode 100644 index 00000000000..ef46f2c8bd2 --- /dev/null +++ b/pkg/logging/file_hook_test.go @@ -0,0 +1,126 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package logging + +import ( + "errors" + "io" + "maps" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/log" +) + +func TestFileHookFire(t *testing.T) { + stamp := time.Date(2026, 4, 28, 3, 22, 52, 0, time.UTC) + + testCases := []struct { + name string + entry *log.Entry + expected string + }{ + { + name: "message only", + entry: &log.Entry{ + Time: stamp, + Level: log.InfoLevel, + Message: "creating container", + }, + expected: `2026-04-28T03:22:52.000000000Z INFO creating container` + "\n", + }, + { + name: "fields are sorted", + entry: &log.Entry{ + Time: stamp, + Level: log.ErrorLevel, + Message: "failed to create container", + Data: log.Fields{ + "id": "foo", + "error": errors.New("no such image"), + "containerName": "bar", + }, + }, + expected: `2026-04-28T03:22:52.000000000Z ERROR failed to create container ` + + `containerName="bar" error="no such image" id="foo"` + "\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var sb strings.Builder + hook := &fileHook{w: &sb} + assert.NilError(t, hook.Fire(tc.entry)) + assert.Equal(t, sb.String(), tc.expected) + }) + } +} + +func TestFileHookLevels(t *testing.T) { + // All levels must be accepted: entries are filtered against the logger level + // before the hooks are fired. + assert.Equal(t, len((&fileHook{}).Levels()), 7) +} + +func TestSetLogFile(t *testing.T) { + logFile := filepath.Join(t.TempDir(), "nerdctl.log") + assert.NilError(t, os.WriteFile(logFile, []byte("previous invocation\n"), 0o600)) + + savedHooks := log.L.Logger.ReplaceHooks(maps.Clone(log.L.Logger.Hooks)) + savedOut := log.L.Logger.Out + t.Cleanup(func() { + log.L.Logger.ReplaceHooks(savedHooks) + log.L.Logger.SetOutput(savedOut) + }) + + closer, err := SetLogFile(logFile) + assert.NilError(t, err) + // Windows can not remove a file that is still open, and t.TempDir() cleans up + // after this, so the handle has to go back first. + t.Cleanup(func() { _ = closer.Close() }) + // The console output must be left alone, otherwise the formatter stops + // detecting the terminal and downgrades its output style. + assert.Equal(t, log.L.Logger.Out, savedOut) + + log.L.Logger.SetOutput(io.Discard) + log.L.WithField("id", "foo").Error("failed to create container") + + b, err := os.ReadFile(logFile) + assert.NilError(t, err) + got := string(b) + // Opened in append mode, so a concurrent or previous invocation is not lost. + assert.Assert(t, strings.HasPrefix(got, "previous invocation\n"), got) + assert.Assert(t, strings.Contains(got, `ERROR failed to create container id="foo"`), got) + + if runtime.GOOS != "windows" { + st, err := os.Stat(logFile) + assert.NilError(t, err) + assert.Equal(t, st.Mode().Perm(), os.FileMode(0o600)) + } +} + +func TestSetLogFileError(t *testing.T) { + // A directory can not be opened for writing. + _, err := SetLogFile(t.TempDir()) + assert.ErrorContains(t, err, "failed to open log file") +} From 3b9a145804455f0f731e9008b43461d616ae879c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:32:43 +0000 Subject: [PATCH 761/868] build(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 2d953070ac4..ae888e47e66 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -40,7 +40,7 @@ jobs: uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index a610041c83a..d1fbb59a84e 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -242,7 +242,7 @@ jobs: - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} name: "Init (linux): Set up Docker Buildx" - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides # the GitHub runtime token and cache url to BuildKit by itself. diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 67722e1d873..01e3bbef3d6 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -67,7 +67,7 @@ jobs: template://${GUEST} - name: "Init: Set up Docker Buildx" - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides the # GitHub runtime token and cache url to BuildKit by itself. From 44eea59ea26caadc45414866829f160eb6b6649b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:32:45 +0000 Subject: [PATCH 762/868] build(deps): bump github.com/Microsoft/hcsshim Bumps [github.com/Microsoft/hcsshim](https://github.com/Microsoft/hcsshim) from 0.15.0-rc.3 to 0.15.0-rc.4. - [Release notes](https://github.com/Microsoft/hcsshim/releases) - [Commits](https://github.com/Microsoft/hcsshim/compare/v0.15.0-rc.3...v0.15.0-rc.4) --- updated-dependencies: - dependency-name: github.com/Microsoft/hcsshim dependency-version: 0.15.0-rc.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 20 +++++----- go.sum | 117 ++++++++++----------------------------------------------- 2 files changed, 30 insertions(+), 107 deletions(-) diff --git a/go.mod b/go.mod index 122e40d7052..40535b23e66 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.26.3 require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 - github.com/Microsoft/hcsshim v0.15.0-rc.3 + github.com/Microsoft/hcsshim v0.15.0-rc.4 github.com/compose-spec/compose-go/v2 v2.14.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.4 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined @@ -87,10 +87,9 @@ require ( github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect @@ -123,20 +122,19 @@ require ( github.com/tinylib/msgp v1.3.0 // indirect github.com/vbatts/tar-split v0.12.3 // indirect 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.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.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.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.38.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect //gomodjail:unconfined - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/grpc v1.83.0 // indirect //gomodjail:unconfined - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + google.golang.org/protobuf v1.36.12 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect @@ -152,6 +150,8 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/moby/moby/api v1.55.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + go.opentelemetry.io/otel/sdk v1.45.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 d82bf1f442b..9f3182f231f 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,25 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/Microsoft/hcsshim v0.15.0-rc.3 h1:ZTNzOp0QwJ1EiL3zopSOawIG0j7zAvzJx0rBmcR6HJ0= -github.com/Microsoft/hcsshim v0.15.0-rc.3/go.mod h1:VhDiwXgb8cEJxO9H57YL4NNIYqvZKpqvSDcimLyo7m8= +github.com/Microsoft/hcsshim v0.15.0-rc.4 h1:aZFX4LH0S20Lgjq0wG61StIClj7im4yzrxIClkaR8Z8= +github.com/Microsoft/hcsshim v0.15.0-rc.4/go.mod h1:BA9CBztgu4h/6Jsvo1O1M4qjWw09PoYpaEYgexPE578= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ= github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/accelerated-container-image v1.4.4 h1:88mL7plI0lvrzCiU1obhB4CFE8YFWFiqzNipydqiYCM= @@ -102,10 +97,6 @@ github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUE github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erofs/go-erofs v0.3.1 h1:Sux82Jq9yvyYhIoLgSHDp741p/+370HsOj9dAh1+VVs= github.com/erofs/go-erofs v0.3.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs= github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755SVMyzaUQ= @@ -121,8 +112,8 @@ github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxv github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= @@ -133,34 +124,13 @@ 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/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= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -260,7 +230,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -293,7 +262,6 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= @@ -309,22 +277,20 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX 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.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.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.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= 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= @@ -339,7 +305,6 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfP go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -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= @@ -347,12 +312,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -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.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= @@ -360,13 +321,7 @@ 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.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -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-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= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -376,9 +331,6 @@ 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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -388,10 +340,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.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= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -432,10 +381,6 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -444,34 +389,14 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/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.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -482,8 +407,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= From e6045934c7564f224499b8372fcd500758cea8cc Mon Sep 17 00:00:00 2001 From: rainwu Date: Fri, 21 Aug 2026 21:14:53 +0800 Subject: [PATCH 763/868] fix(login): gate hub alias on standard port, normalize bracketed IPv6 Address review feedback on the equivalent-host check: - Only honor the index.docker.io -> registry-1.docker.io alias when the login target uses the standard HTTPS port, so a login to index.docker.io: no longer leaks credentials to registry-1.docker.io. - Normalize port-less callback hosts through url.URL.Hostname so bracketed IPv6 literals (e.g. [::1]) can match registryURL.Hostname. Co-Authored-By: Claude Fable 5 Signed-off-by: rainwu --- pkg/cmd/login/login.go | 13 +++++++++---- pkg/cmd/login/login_test.go | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index 89505e4e891..d6361a14888 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -231,8 +231,9 @@ func loginAuthCreds(ctx context.Context, host string, registryURL *dockerconfigr func isEquivalentRegistryHost(acArg string, registryURL *dockerconfigresolver.RegistryURL) bool { acHost, acPort, err := net.SplitHostPort(acArg) if err != nil { - // acArg carries no port - acHost, acPort = acArg, "" + // acArg carries no port; Hostname strips the brackets of IPv6 + // literals so that "[::1]" can match registryURL.Hostname() + acHost, acPort = (&url.URL{Host: acArg}).Hostname(), "" } // A callback host carrying an explicit non-standard port can only be // equivalent by exact equality, which the caller already checked. @@ -245,8 +246,12 @@ func isEquivalentRegistryHost(acArg string, registryURL *dockerconfigresolver.Re return true } // Docker Hub aliases: "docker.io" logins resolve to index.docker.io, - // while the actual registry endpoint is registry-1.docker.io. - if registryURL.Hostname() == "index.docker.io" && acHost == "registry-1.docker.io" { + // while the actual registry endpoint is registry-1.docker.io. Only + // honor the alias when logging in over the standard HTTPS port, so a + // login to index.docker.io on a non-default port does not leak + // credentials to registry-1.docker.io. + if registryURL.Port() == dockerconfigresolver.StandardHTTPSPort && + registryURL.Hostname() == "index.docker.io" && acHost == "registry-1.docker.io" { return true } return false diff --git a/pkg/cmd/login/login_test.go b/pkg/cmd/login/login_test.go index 956b356c09f..c70ac92ddc7 100644 --- a/pkg/cmd/login/login_test.go +++ b/pkg/cmd/login/login_test.go @@ -54,6 +54,16 @@ func TestLoginAuthCredsAcceptsEquivalentHosts(t *testing.T) { address: "docker.io", acArg: "registry-1.docker.io:443", }, + { + name: "bracketed ipv6 without port", + address: "[::1]", + acArg: "[::1]", + }, + { + name: "ipv6 with standard port", + address: "[::1]", + acArg: "[::1]:443", + }, { name: "mismatched host", address: "harbor.example.io", @@ -72,6 +82,18 @@ func TestLoginAuthCredsAcceptsEquivalentHosts(t *testing.T) { acArg: "harbor.example.io:8443", wantErr: true, }, + { + name: "docker.io alias rejected on non-standard login port", + address: "index.docker.io:8443", + acArg: "registry-1.docker.io", + wantErr: true, + }, + { + name: "docker.io alias with port rejected on non-standard login port", + address: "index.docker.io:8443", + acArg: "registry-1.docker.io:443", + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 97dbdc7fc3ec44a8ffdc18d5bf9c6e86fca2c463 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Thu, 20 Aug 2026 23:51:31 +0900 Subject: [PATCH 764/868] Update RootlessKit (3.1.0) Signed-off-by: Hayato Kiwata --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 | 6 ++++++ docs/rootless.md | 3 ++- extras/rootless/containerd-rootless.sh | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 diff --git a/Dockerfile b/Dockerfile index 4c0765fea69..e2bfd582e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ 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.2@BINARY +ARG ROOTLESSKIT_VERSION=v3.1.0@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 deleted file mode 100644 index d0d72174aeb..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.2 +++ /dev/null @@ -1,6 +0,0 @@ -823ce5dc80fe24da6c1b1e3c37a70fa6e8ba3067636d9a763228031ffeb43dc8 rootlesskit-aarch64.tar.gz -7c403f30aefa6e3faa0843c7d8abbbb0ea2b23e55392958c8e36e883979b6743 rootlesskit-armv7l.tar.gz -9fb855acf3fc23c524c978c21c17e37f374f483152e1ac537a231265e9b3a680 rootlesskit-ppc64le.tar.gz -f23a10d51bcd3c0ee1f22287730c18e2a8094911d436c41bf70fb396d324d88e rootlesskit-riscv64.tar.gz -ba5b8ad5fa39d9b99af7402ebf9eb53549e6cf76453b9636f262a88ac94d04e7 rootlesskit-s390x.tar.gz -f4f2764cdd99db4f3fa715acac9d760c49a5e7c2838f180bdbe3188cec248dfb rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 new file mode 100644 index 00000000000..5fc0011ced7 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 @@ -0,0 +1,6 @@ +42d1c22a34be7bb458f9ae7363d68dd05d553422d95392b44d0be7fa1fd76bf0 rootlesskit-aarch64.tar.gz +39608ccc4ea8cf7e243b569abed5b237339416b830f96e51898957fb9f8c7341 rootlesskit-armv7l.tar.gz +4a10d5fd12c78e569d58b2156f061e1380497416a4db19a123f5097577ddc83a rootlesskit-ppc64le.tar.gz +04eaaac6b855230b1b13f0581438e0aee446dff316f7c32af244ab494cdc042f rootlesskit-riscv64.tar.gz +d277da00fd3ee8d60183e1c6e843519305a06e701f2855d46ffe15d6bad1dff8 rootlesskit-s390x.tar.gz +b1302b7395918266d561b9e3053771253f20761807e042ae80a1868d6e86b71c rootlesskit-x86_64.tar.gz diff --git a/docs/rootless.md b/docs/rootless.md index 4c796dc3fef..dbf3a2452a0 100644 --- a/docs/rootless.md +++ b/docs/rootless.md @@ -155,7 +155,8 @@ Rootless containerd recognizes the following environment variables to configure * `CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR`: the rootlesskit state dir. Defaults to `$XDG_RUNTIME_DIR/containerd-rootless`. * `CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|pasta|gvisor-tap-vsock|lxc-user-nic)`: the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "gvisor-tap-vsock". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM`: the MTU value for the rootlesskit network driver. Defaults to 65520 or 1500, depending on the network driver. -* `CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|implicit|gvisor-tap-vsock)`: the rootlesskit port driver. Defaults to "builtin". +* `CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|pesto|implicit|gvisor-tap-vsock)`: the rootlesskit port driver. Defaults to "builtin". + The "pesto" port driver (experimental, IPv4 only) requires the "pasta" network driver and passt `2026_05_07.1afd4ed` or later, which provides the "pesto" binary. * `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false)`: whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false)`: whether to protect slirp4netns with seccomp. Defaults to "auto". * `CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false)`: whether to launch rootlesskit with the "detach-netns" mode. diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh index 631ba91174f..c7c385ef6ff 100755 --- a/extras/rootless/containerd-rootless.sh +++ b/extras/rootless/containerd-rootless.sh @@ -34,7 +34,8 @@ # * CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR: the rootlesskit state dir. Defaults to "$XDG_RUNTIME_DIR/containerd-rootless". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|pasta|gvisor-tap-vsock|lxc-user-nic): the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "gvisor-tap-vsock". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM: the MTU value for the rootlesskit network driver. Defaults to 65520 or 1500, depending on the network driver. -# * CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|implicit|gvisor-tap-vsock): the rootlesskit port driver. Defaults to "builtin". +# * CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns|pesto|implicit|gvisor-tap-vsock): the rootlesskit port driver. Defaults to "builtin". +# The "pesto" port driver (experimental, IPv4 only) requires the "pasta" network driver and passt `2026_05_07.1afd4ed` or later, which provides the "pesto" binary. # * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false): whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false): whether to protect slirp4netns with seccomp. Defaults to "auto". # * CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false): whether to launch rootlesskit with the "detach-netns" mode. From ffb8baebd5c0ac8e953040579ec127e3f60b56ab Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Fri, 7 Aug 2026 18:41:27 +0900 Subject: [PATCH 765/868] feat: add volume-nocopy Signed-off-by: Jiwoo Ahn --- .../container_run_mount_linux_test.go | 14 +++++++++ docs/command-reference.md | 3 +- pkg/cmd/container/run_mount.go | 2 +- pkg/mountutil/mountutil.go | 1 + pkg/mountutil/mountutil_linux.go | 14 +++++++++ pkg/mountutil/mountutil_linux_test.go | 31 +++++++++++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index 76a204588c1..10e21052c69 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -234,8 +234,12 @@ CMD ["cat", "/mnt/initial_file"] volName := data.Identifier("vol") helpers.Ensure("volume", "create", volName) + noCopyVolName := data.Identifier("nocopy-vol") + helpers.Ensure("volume", "create", noCopyVolName) + data.Labels().Set("img", imgName) data.Labels().Set("vol", volName) + data.Labels().Set("nocopy-vol", noCopyVolName) } testCase.SubTests = []*test.Case{ @@ -263,12 +267,22 @@ CMD ["cat", "/mnt/initial_file"] }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), }, + { + Description: "with volume-nocopy", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + mount := fmt.Sprintf("type=volume,source=%s,target=/mnt,volume-nocopy", data.Labels().Get("nocopy-vol")) + return helpers.Command("run", "--rm", "--mount", mount, data.Labels().Get("img"), "sh", "-c", "test ! -e /mnt/initial_file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", data.Labels().Get("vol")) helpers.Anyhow("rmi", data.Labels().Get("img")) helpers.Anyhow("builder", "prune", "--all", "--force") + helpers.Anyhow("volume", "rm", data.Labels().Get("nocopy-vol")) } testCase.Run(t) diff --git a/docs/command-reference.md b/docs/command-reference.md index ec41c0a61d2..2f838dbec3c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -327,7 +327,8 @@ Volume flags: - :whale: `tmpfs-mode`: File mode of the tmpfs in **octal**. Defaults to `1777` or world-writable. - Options specific to `volume`: - - unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt` + - :whale: `volume-nocopy`: Do not copy existing data from the container into the volume. + - unimplemented options: `volume-label`, `volume-driver`, `volume-opt` - Options specific to `image`: - :whale: `src`, `source`: image reference (mandatory). - :whale: Currently, the image filesystem is mounted read-only. diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 34ab8fa1230..d6bdb97e603 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -319,7 +319,7 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm } // Copying content in AnonymousVolume and namedVolume - if x.Type == "volume" { + if x.Type == mountutil.Volume && !x.VolumeNoCopy { if err := copyExistingContents(target, x.Mount.Source); err != nil { return nil, nil, nil, err } diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index f4ce3bd8f1d..350d4443376 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -51,6 +51,7 @@ type Processed struct { AnonymousVolume string // anonymous volume name Mode string Opts []oci.SpecOpts + VolumeNoCopy bool // ImageMountSnapshot is the snapshotter key of the read-only view for a // type=image mount; empty for other mount types. ImageMountSnapshot string diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 02da626213b..46ed18cb892 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -369,6 +369,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str bindPropagation string bindNonRecursive bool bindRecursive string // "enabled", "disabled", "writable", or "readonly" + volumeNoCopy bool rwOption string tmpfsSize int64 tmpfsMode os.FileMode @@ -404,6 +405,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str log.L.Warn("The mount option \"bind-nonrecursive\" is deprecated; use \"bind-recursive=disabled\" instead") bindNonRecursive = true continue + case "volume-nocopy": + volumeNoCopy = true + continue } } @@ -455,6 +459,11 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str if err != nil { return nil, fmt.Errorf("invalid value for %s: %s", key, value) } + case "volume-nocopy": + volumeNoCopy, err = strconv.ParseBool(value) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %s", key, value) + } case "bind-recursive": // bind-recursive is the Docker (v25) option that supersedes bind-nonrecursive. // https://github.com/docker/cli/pull/4316 @@ -480,6 +489,10 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str } } + if volumeNoCopy && mountType != Volume { + return nil, fmt.Errorf("the option 'volume-nocopy' is only supported for volume mounts") + } + // type=image's source is an image reference resolved later with a containerd // client; validate the intent here. Like Docker, an image mount is always // read-only: a readonly/ro option is accepted for compatibility but the @@ -593,6 +606,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str if err != nil { return nil, err } + res.VolumeNoCopy = volumeNoCopy if rwOption != "" { roOpts, err := readOnlyMountOptions(roMode, ociRuntime) if err != nil { diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 5ee383d7065..016b6b619b4 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -665,3 +665,34 @@ func TestProcessFlagMountImage(t *testing.T) { }) } } + +func TestProcessFlagMountVolumeNoCopy(t *testing.T) { + tests := []struct { + rawSpec string + wants bool + }{ + { + rawSpec: "type=volume,source=TestVolume,target=/mnt,volume-nocopy", + wants: true, + }, + { + rawSpec: "type=volume,source=TestVolume,target=/mnt,volume-nocopy=true", + wants: true, + }, + { + rawSpec: "type=volume,source=TestVolume,target=/mnt,volume-nocopy=false", + wants: false, + }, + } + + for _, tt := range tests { + t.Run(tt.rawSpec, func(t *testing.T) { + got, err := ProcessFlagMount(tt.rawSpec, mockVolumeStore, "") + assert.NilError(t, err) + + assert.Equal(t, got.Type, Volume) + assert.Equal(t, got.Name, "TestVolume") + assert.Equal(t, got.VolumeNoCopy, tt.wants) + }) + } +} From 40da4a42bfbf6dbec4e409027806fb59c6413cfe Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Mon, 24 Aug 2026 21:35:32 +0300 Subject: [PATCH 766/868] fix(image): truncate the output file of save and export `nerdctl save -o FILE` and `nerdctl export -o FILE` open the output with O_CREATE|O_WRONLY and no O_TRUNC. Writing a smaller archive over a bigger one therefore leaves the tail of the bigger one past the end of the new archive. A tar reader stops at the end-of-archive marker, so `nerdctl load` reads such a file without complaining. What is wrong is the artifact: it is larger than the archive it is supposed to hold, and the extra bytes belong to an unrelated image or container, which shows up in anything that checksums the file, accounts for its size, or ships it somewhere. `docker save` replaces the destination wholesale, writing through a temporary file and renaming it. Add O_TRUNC, so that `-o` replaces the file as a shell redirect does. `pkg/healthcheck/log.go` opens a file with the same flags, but seeks to the end and appends on purpose, so it is left alone. Fixes #5159 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/container/container_export.go | 5 +- .../container/container_export_test.go | 59 +++++++++++++++++++ cmd/nerdctl/image/image_save.go | 5 +- cmd/nerdctl/image/image_save_test.go | 48 +++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_export.go b/cmd/nerdctl/container/container_export.go index d7fc7abc580..84b12fc7777 100644 --- a/cmd/nerdctl/container/container_export.go +++ b/cmd/nerdctl/container/container_export.go @@ -68,7 +68,10 @@ func exportAction(cmd *cobra.Command, args []string) error { writer := cmd.OutOrStdout() if output != "" { - f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644) + // O_TRUNC: writing a smaller archive over a bigger one would otherwise leave the tail of + // the bigger one past its end. A tar reader stops at the end-of-archive marker and would + // not notice, but the file would carry the bytes of another container. + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } diff --git a/cmd/nerdctl/container/container_export_test.go b/cmd/nerdctl/container/container_export_test.go index ee25fe2dc9d..dd9945f5da6 100644 --- a/cmd/nerdctl/container/container_export_test.go +++ b/cmd/nerdctl/container/container_export_test.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "testing" "gotest.tools/v3/assert" @@ -157,6 +158,64 @@ func TestExportRunningContainer(t *testing.T) { testCase.Run(t) } +func TestExportReplacesExistingFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("export is not supported on Windows") + } + + testCase := nerdtest.Setup() + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + bigger := data.Identifier("bigger") + smaller := data.Identifier("smaller") + outFile := filepath.Join(data.Temp().Path(), "reused.tar") + + helpers.Ensure("create", "--name", bigger, testutil.NginxAlpineImage) + helpers.Ensure("create", "--name", smaller, testutil.CommonImage) + + // The bigger filesystem first, so that the smaller one written over it has something to + // leave behind. + helpers.Ensure("export", "-o", outFile, bigger) + info, err := os.Stat(outFile) + assert.NilError(t, err) + + data.Labels().Set("bigger", bigger) + data.Labels().Set("smaller", smaller) + data.Labels().Set("outFile", outFile) + data.Labels().Set("biggerSize", strconv.FormatInt(info.Size(), 10)) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Labels().Get("bigger")) + helpers.Anyhow("rm", "-f", data.Labels().Get("smaller")) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("export", "-o", data.Labels().Get("outFile"), data.Labels().Get("smaller")) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + info, err := os.Stat(data.Labels().Get("outFile")) + assert.NilError(t, err) + biggerSize, err := strconv.ParseInt(data.Labels().Get("biggerSize"), 10, 64) + assert.NilError(t, err) + + // The file must hold the smaller archive and nothing else. A tar reader stops at + // the end-of-archive marker, so a tail left over from the bigger archive would go + // unnoticed on read, but the file would still carry the bytes of another + // container. + assert.Assert(t, info.Size() < biggerSize, + "expected the file to shrink to the new archive, still %d of %d bytes", + info.Size(), biggerSize) + }, + } + } + + testCase.Run(t) +} + func TestExportNonexistentContainer(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("export is not supported on Windows") diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 61bcbab54a3..9695f6c69a5 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -95,7 +95,10 @@ func saveAction(cmd *cobra.Command, args []string) error { if err != nil { return err } else if outputPath != "" { - f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0644) + // O_TRUNC: writing a smaller archive over a bigger one would otherwise leave the tail of + // the bigger one past its end. A tar reader stops at the end-of-archive marker and would + // not notice, but the file would carry the bytes of an unrelated image. + f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 7671570d31c..c4135890477 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "testing" @@ -66,6 +67,53 @@ func TestSaveContent(t *testing.T) { testCase.Run(t) } +func TestSaveReplacesExistingFile(t *testing.T) { + nerdtest.Setup() + + const reused = "reused.tar" + + testCase := &test.Case{ + // FIXME: move to busybox for windows? + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + + // A bigger archive first, so that the smaller one written over it has something to + // leave behind. + path := filepath.Join(data.Temp().Path(), reused) + helpers.Ensure("save", "-o", path, testutil.NginxAlpineImage) + info, err := os.Stat(path) + assert.NilError(t, err) + data.Labels().Set("bigger", strconv.FormatInt(info.Size(), 10)) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("save", "-o", filepath.Join(data.Temp().Path(), reused), testutil.CommonImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + info, err := os.Stat(filepath.Join(data.Temp().Path(), reused)) + assert.NilError(t, err) + bigger, err := strconv.ParseInt(data.Labels().Get("bigger"), 10, 64) + assert.NilError(t, err) + + // The file must hold the smaller archive and nothing else. A tar reader stops + // at the end-of-archive marker, so a tail left over from the bigger archive + // would go unnoticed on read, but the file would still carry the bytes of an + // unrelated image. + assert.Assert(t, info.Size() < bigger, + "expected the file to shrink to the new archive, still %d of %d bytes", + info.Size(), bigger) + }, + } + }, + } + + testCase.Run(t) +} + func TestSaveQuiet(t *testing.T) { nerdtest.Setup() From 2ea3e8f81af084c5f1c1b76e06122f0a55e82bdb Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Tue, 4 Aug 2026 19:01:39 +0300 Subject: [PATCH 767/868] feat(image): add --tree flag to image list Expand the Docker v29 view added in #5093 with `--tree`, matching `docker image ls --tree`: the same IMAGE, ID, DISK USAGE, CONTENT SIZE and EXTRA columns, plus one row per platform the image declares, prefixed with the branch glyphs docker/cli uses. Like docker, a platform that an index lists but that was never pulled is shown with zero sizes. The collapsed and the legacy views keep describing only what is in the store, and so does EnsureAllContent, which would otherwise reach out for platforms the user never asked for. A platform row carries its own manifest digest as the ID, its own sizes, and the "U" flag only when a container actually runs that platform. The image row keeps aggregating its platforms and reuses the existing target-digest lookup, so the default view is unchanged. Matching a container to a platform is done on the full platform form, not on the name the row displays: an index can carry several windows/amd64 manifests that differ only by OSVersion, and keying on the displayed name would let a container on one build flag all of them. The container label is normalized first, since platforms.DefaultString does not normalize: on arm64 it carries a variant while the manifest platform normalizes to a bare linux/arm64, and a raw comparison would never match. The rows are sorted on that same full form, so the ones that render identically keep a stable order. Unlike docker/cli, which computes its column widths itself and separates the images with a blank line, the rows go through a tabwriter shared with the header, where a blank line would terminate the column block and misalign every following group. The groups are therefore separated by the glyphs alone. The flag combinations docker rejects in shouldUseTree are rejected here too, with the same messages. They are validated in pkg/cmd/image, where the options are consumed, so that library callers are covered as well: the tree branch takes precedence over the formatter, so Tree together with Format used to print unaligned rows with no header. The CLI validates too, so that the error still surfaces before a containerd connection is attempted. Also read the platform of a multi-platform image from its index descriptor rather than from the image config. The config may be less specific: alpine ships linux/arm/v6 and linux/arm/v7 manifests whose configs both declare a bare "linux/arm", which normalizes to linux/arm/v7 and collapsed the two onto a single key, dropping one platform. That also left it out of the aggregated sizes of the default view. The expected per-platform content sizes are declared in testutil, and the integration test runs against docker as well, only diverging where docker does: `docker pull` has no --all-platforms, and its message for the digests conflict names its internal flag. DISK USAGE is only asserted to cover CONTENT SIZE, because which platforms are unpacked depends on what the rest of the suite did with the shared image store. Closes #5005 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/image/image_list.go | 19 +- cmd/nerdctl/image/image_list_linux_test.go | 254 ++++++++++++++++++++ docs/command-reference.md | 15 ++ pkg/api/types/image_types.go | 2 + pkg/cmd/image/ensure.go | 5 + pkg/cmd/image/list.go | 247 +++++++++++++++++--- pkg/cmd/image/list_test.go | 260 +++++++++++++++++++++ pkg/testutil/images.yaml | 19 +- pkg/testutil/images_linux.go | 27 +++ 9 files changed, 807 insertions(+), 41 deletions(-) create mode 100644 cmd/nerdctl/image/image_list_linux_test.go diff --git a/cmd/nerdctl/image/image_list.go b/cmd/nerdctl/image/image_list.go index 03e854d19f5..267f8a07a00 100644 --- a/cmd/nerdctl/image/image_list.go +++ b/cmd/nerdctl/image/image_list.go @@ -41,6 +41,9 @@ By default (Docker v29 compatible view) the following columns are shown: - CONTENT SIZE: Size of the blobs (such as layer tarballs) in the content store - EXTRA: Flags for the image; "U" means the image is in use by a container +--tree expands multi-platform images: the same columns are shown, with an additional row per +platform the image declares. Platforms that were never pulled are listed with zero sizes. + Passing --format, --quiet, --no-trunc, --digests or --names falls back to the legacy table: - REPOSITORY: Repository - TAG: Tag @@ -74,6 +77,7 @@ Passing --format, --quiet, --no-trunc, --digests or --names falls back to the le cmd.Flags().Bool("digests", false, "Show digests (compatible with Docker, unlike ID)") cmd.Flags().Bool("names", false, "Show image names") cmd.Flags().BoolP("all", "a", true, "(unimplemented yet, always true)") + cmd.Flags().Bool("tree", false, "List multi-platform images as a tree (EXPERIMENTAL)") return cmd } @@ -118,7 +122,11 @@ func listOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, er if err != nil { return nil, err } - return &types.ImageListOptions{ + tree, err := cmd.Flags().GetBool("tree") + if err != nil { + return nil, err + } + options := &types.ImageListOptions{ GOptions: globalOptions, Quiet: quiet, NoTrunc: noTrunc, @@ -128,8 +136,15 @@ func listOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, er Digests: digests, Names: names, All: true, + Tree: tree, Stdout: cmd.OutOrStdout(), - }, nil + } + // Validated here as well as in the logic layer, so that an invalid flag combination is + // reported before a containerd connection is attempted. + if err := image.ValidateListOptions(options); err != nil { + return nil, err + } + return options, nil } diff --git a/cmd/nerdctl/image/image_list_linux_test.go b/cmd/nerdctl/image/image_list_linux_test.go new file mode 100644 index 00000000000..2197741bca7 --- /dev/null +++ b/cmd/nerdctl/image/image_list_linux_test.go @@ -0,0 +1,254 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "errors" + "slices" + "strings" + "testing" + + "github.com/docker/go-units" + "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/mod/tigron/tig" + "github.com/containerd/platforms" + + "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/tabutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// treeImageKey is the testutil registry key of testutil.CommonImage. Its entry declares the +// platforms of the image and the expected content size of each of them. +const treeImageKey = "alpine" + +// treeChildFields splits a per-platform row of `image ls --tree` into its cells, returning nil for +// any other line. The rows are split on whitespace rather than read with tabutil, because tabutil +// indexes the columns by byte offset while the branch glyphs are multi-byte: the tabwriter aligns +// them by rune, so the byte offsets of a child row no longer match the header's. +func treeChildFields(line string) []string { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "├─") && !strings.HasPrefix(trimmed, "└─") { + return nil + } + // ["├─", "linux/amd64", "", "", "", optional "U"] + return strings.Fields(trimmed) +} + +// normalizeTreePlatform renders a platform the way the testutil registry keys it, so that a row can +// be looked up whatever form the tested binary printed it in (docker keeps the "v8" variant of +// linux/arm64, nerdctl normalizes it away). +func normalizeTreePlatform(platform string) string { + parsed, err := platforms.Parse(platform) + if err != nil { + return platform + } + return platforms.Format(platforms.Normalize(parsed)) +} + +func TestImagesTree(t *testing.T) { + nerdtest.Setup() + + commonImage, _ := referenceutil.Parse(testutil.CommonImage) + imageRef := commonImage.FamiliarName() + ":" + commonImage.Tag + hostPlatform := platforms.Format(platforms.Normalize(platforms.DefaultSpec())) + treeHeader := "IMAGE\tID\tDISK USAGE\tCONTENT SIZE\tEXTRA" + + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + if nerdtest.IsDocker() { + // `docker pull` has no --all-platforms, so only the host platform is available there. + helpers.Ensure("pull", "--quiet", commonImage.String()) + return + } + helpers.Ensure("pull", "--quiet", "--all-platforms", commonImage.String()) + }, + SubTests: []*test.Case{ + { + Description: "a row per platform, with the sizes of the content store", + Command: test.Command("images", "--tree", commonImage.String()), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, + "expected a header, an image row and at least one platform row\n") + + tab := tabutil.NewReader(treeHeader) + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "ParseHeader should not fail\n") + + known := testutil.GetTestImagePlatforms(treeImageKey) + foundImage := false + var seen, notPulled []string + for _, line := range lines[1:] { + fields := treeChildFields(line) + if fields == nil { + if image, _ := tab.ReadRow(line, "IMAGE"); image == imageRef { + foundImage = true + } + continue + } + assert.Assert(t, len(fields) >= 5, + "a platform row should have all its columns, got %q\n", line) + + platform := normalizeTreePlatform(fields[1]) + assert.Assert(t, slices.Contains(known, platform), + "unexpected platform %q, the testutil registry knows %v\n", platform, known) + seen = append(seen, platform) + + assert.Equal(t, len(fields[2]), 12, + "a platform row should carry a truncated ID\n") + + diskUsage, err := units.FromHumanSize(fields[3]) + assert.NilError(t, err, "DISK USAGE of %s is %q\n", platform, fields[3]) + contentSize, err := units.FromHumanSize(fields[4]) + assert.NilError(t, err, "CONTENT SIZE of %s is %q\n", platform, fields[4]) + + // DISK USAGE adds the unpacked snapshots on top of the content. Which + // platforms are unpacked depends on what the rest of the suite did with + // the shared image store (TestMultiPlatformRun runs this very image on + // several platforms), so only the invariant can be asserted. + assert.Assert(t, diskUsage >= contentSize, + "DISK USAGE (%d) should cover CONTENT SIZE (%d) of %s\n", + diskUsage, contentSize, platform) + + if contentSize == 0 { + // The index lists every platform of the image, including the ones + // that were never pulled: those have no content to size. + notPulled = append(notPulled, platform) + continue + } + // CONTENT SIZE is the size of the blobs, which is fixed for a given + // image, so it can be checked exactly. + assert.Equal(t, fields[4], units.HumanSizeWithPrecision( + float64(testutil.GetTestImageContentSize(treeImageKey, platform)), 3), + "CONTENT SIZE of %s\n", platform) + } + + assert.Assert(t, foundImage, "we should have found the image row\n") + + // Every platform the index declares is listed, whether it was pulled or not. + slices.Sort(seen) + assert.DeepEqual(t, seen, known) + + if nerdtest.IsDocker() { + // `docker pull` could only fetch the host platform, see Setup. + assert.Assert(t, !slices.Contains(notPulled, hostPlatform), + "the host platform should have been pulled, %v were not\n", notPulled) + return + } + assert.Assert(t, len(notPulled) == 0, + "--all-platforms should have pulled every platform, but %v have no content\n", + notPulled) + }, + } + }, + }, + { + Description: "flags the platform a container runs", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--name", data.Identifier(), + commonImage.String(), "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("images", "--tree", commonImage.String()), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + tab := tabutil.NewReader(treeHeader) + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, "ParseHeader should not fail\n") + + imageInUse := false + var platformsInUse []string + for _, line := range lines[1:] { + fields := treeChildFields(line) + if fields == nil { + // EXTRA is the trailing column, so the row has to be padded + // back to the width of the header to be read. + line = padRow(lines[0], line) + if image, _ := tab.ReadRow(line, "IMAGE"); image == imageRef { + extra, _ := tab.ReadRow(line, "EXTRA") + imageInUse = extra == "U" + } + continue + } + if len(fields) >= 6 && fields[5] == "U" { + platformsInUse = append(platformsInUse, normalizeTreePlatform(fields[1])) + } + } + + assert.Assert(t, imageInUse, "the image row should be flagged as in use\n") + // Only the platform the container actually runs is flagged, not every + // platform of the image. + assert.DeepEqual(t, platformsInUse, []string{hostPlatform}) + }, + } + }, + }, + { + Description: "conflicts with --quiet", + Command: test.Command("images", "--tree", "--quiet"), + Expected: test.Expects(expect.ExitCodeGenericFail, + []error{errors.New("--quiet is not yet supported with --tree")}, nil), + }, + { + Description: "conflicts with --no-trunc", + Command: test.Command("images", "--tree", "--no-trunc"), + Expected: test.Expects(expect.ExitCodeGenericFail, + []error{errors.New("--no-trunc is not yet supported with --tree")}, nil), + }, + { + Description: "conflicts with --format", + Command: test.Command("images", "--tree", "--format", "json"), + Expected: test.Expects(expect.ExitCodeGenericFail, + []error{errors.New("--format is not yet supported with --tree")}, nil), + }, + { + Description: "conflicts with --digests", + Command: test.Command("images", "--tree", "--digests"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // docker names its internal flag in that message, nerdctl names the real one. + message := "--digests is not yet supported with --tree" + if nerdtest.IsDocker() { + message = "--show-digest is not yet supported with --tree" + } + return test.Expects(expect.ExitCodeGenericFail, []error{errors.New(message)}, nil)(data, helpers) + }, + }, + { + Description: "conflicts with --names", + // --names is a nerdctl-specific flag; Docker does not support it. + Require: require.Not(nerdtest.Docker), + Command: test.Command("images", "--tree", "--names"), + Expected: test.Expects(expect.ExitCodeGenericFail, + []error{errors.New("--names is not yet supported with --tree")}, nil), + }, + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index ec41c0a61d2..697b3edc722 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -843,6 +843,20 @@ By default (Docker v29 compatible view) the columns are `IMAGE`, `ID`, `DISK USA Passing `--format`, `--quiet`, `--no-trunc`, `--digests` or `--names` falls back to the legacy table (`REPOSITORY`, `TAG`, `IMAGE ID`, `CREATED`, `PLATFORM`, `SIZE`, `BLOB SIZE`). +`--tree` keeps the same columns and adds a row per platform the image declares: + +```console +$ nerdctl images --tree +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +nginx:latest 7f553e8bbc89 211MB 67.4MB U +├─ linux/amd64 d9153e78d05e 72.4MB 25.2MB U +├─ linux/arm64 1a2b3c4d5e6f 70.1MB 24.9MB +└─ linux/s390x 2b3c4d5e6f7a 0B 0B +``` + +The `U` flag on a platform row means a container runs that specific platform. Platforms that were +never pulled are listed with zero sizes, like `docker image ls --tree` does. + Usage: `nerdctl images [OPTIONS] [REPOSITORY[:TAG]]` Flags: @@ -863,6 +877,7 @@ Flags: - :whale: `--filter=dangling=true`: Filter images by dangling - :nerd_face: `--filter=reference=`: Filter images by reference (Matches both docker compatible wildcard pattern and regexp match) - :nerd_face: `--names`: Show image names +- :whale: `--tree`: List multi-platform images as a tree (EXPERIMENTAL). Cannot be combined with `--quiet`, `--no-trunc`, `--digests`, `--format` or `--names`. ### :whale: nerdctl pull diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 503f2a3c776..1410b75c140 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -43,6 +43,8 @@ type ImageListOptions struct { Names bool // All (unimplemented yet, always true) All bool + // Tree list multi-platform images as a tree, with a row per platform + Tree bool } // ImageConvertOptions specifies options for `nerdctl image convert`. diff --git a/pkg/cmd/image/ensure.go b/pkg/cmd/image/ensure.go index c3315e58c29..ac0dae8302c 100644 --- a/pkg/cmd/image/ensure.go +++ b/pkg/cmd/image/ensure.go @@ -52,6 +52,11 @@ func EnsureAllContent(ctx context.Context, client *containerd.Client, srcName st imagesList, _ := read(ctx, provider, snapshotter, img.Target) // Iterate through the list for _, i := range imagesList { + // An index also lists the platforms that were never pulled. Ensuring their content would + // mean fetching a platform the user never asked for, so keep to what is in the store. + if !i.available { + continue + } if platMC.Match(i.platform) { err = ensureOne(ctx, client, srcName, img.Target, i.platform, options) if err != nil { diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go index 703baa6679a..9c8510ba172 100644 --- a/pkg/cmd/image/list.go +++ b/pkg/cmd/image/list.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "os" + "slices" "sort" "strings" "text/tabwriter" @@ -54,6 +55,9 @@ import ( // ListCommandHandler `List` and print images matching filters in `options`. func ListCommandHandler(ctx context.Context, client *containerd.Client, options *types.ImageListOptions) error { + if err := ValidateListOptions(options); err != nil { + return err + } imageList, err := List(ctx, client, options.Filters, options.NameAndRefFilter) if err != nil { return err @@ -61,6 +65,34 @@ func ListCommandHandler(ctx context.Context, client *containerd.Client, options return printImages(ctx, client, imageList, options) } +// ValidateListOptions rejects option combinations the list views cannot honor, mirroring the +// checks docker/cli makes in shouldUseTree. Unlike the implicit choice between the default and the +// legacy view, Tree is an explicit request, so silently falling back would be surprising. +// +// It is enforced here, where the options are actually consumed, so that library callers get the +// error too. The CLI calls it as well, so that the error surfaces before a containerd connection +// is attempted. +func ValidateListOptions(options *types.ImageListOptions) error { + if !options.Tree { + return nil + } + for _, conflict := range []struct { + set bool + flag string + }{ + {options.Quiet, "--quiet"}, + {options.NoTrunc, "--no-trunc"}, + {options.Digests, "--digests"}, + {options.Format != "", "--format"}, + {options.Names, "--names"}, + } { + if conflict.set { + return fmt.Errorf("%s is not yet supported with --tree", conflict.flag) + } + } + return nil +} + // List queries containerd client to get image list and only returns those matching given filters. // // Supported filters: @@ -221,23 +253,26 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima // In-use detection requires a container scan, so only pay for it in the new view where the // EXTRA column is rendered. var inUse map[digest.Digest]bool + var inUseByPlatform map[platformRef]bool if newView { - inUse = imagesInUse(ctx, client) + inUse, inUseByPlatform = imagesInUse(ctx, client) sortByImageRef(finalImageList) } printer := &imagePrinter{ - w: w, - quiet: options.Quiet, - noTrunc: options.NoTrunc, - digestsFlag: digestsFlag, - namesFlag: options.Names, - newView: newView, - inUse: inUse, - tmpl: tmpl, - client: client, - provider: containerdutil.NewProvider(client), - snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter), + w: w, + quiet: options.Quiet, + noTrunc: options.NoTrunc, + digestsFlag: digestsFlag, + namesFlag: options.Names, + newView: newView, + tree: options.Tree, + inUse: inUse, + inUseByPlatform: inUseByPlatform, + tmpl: tmpl, + client: client, + provider: containerdutil.NewProvider(client), + snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter), } for _, img := range finalImageList { @@ -252,13 +287,14 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima } type imagePrinter struct { - w io.Writer - quiet, noTrunc, digestsFlag, namesFlag, newView bool - inUse map[digest.Digest]bool // image target -> referenced by at least one container - tmpl *template.Template - client *containerd.Client - provider content.Provider - snapshotter snapshots.Snapshotter + w io.Writer + quiet, noTrunc, digestsFlag, namesFlag, newView, tree bool + inUse map[digest.Digest]bool // image target -> referenced by at least one container + inUseByPlatform map[platformRef]bool // image target + platform -> run by at least one container + tmpl *template.Template + client *containerd.Client + provider content.Provider + snapshotter snapshots.Snapshotter } type image struct { @@ -266,6 +302,13 @@ type image struct { size int64 platform platforms.Platform config *ocispec.Descriptor + // manifestDigest is the digest of the platform-specific manifest itself, used as the per-platform + // ID in the tree view. For a single-platform image it is the image target digest. + manifestDigest digest.Digest + // available reports whether the content of that platform is in the store. An index lists every + // platform of the image, including the ones that were never pulled; only the tree view reports + // those, with zero sizes, the way docker does. + available bool } func readManifest(ctx context.Context, provider content.Provider, snapshotter snapshots.Snapshotter, desc ocispec.Descriptor) (*image, error) { @@ -313,10 +356,12 @@ func readManifest(ctx context.Context, provider content.Provider, snapshotter sn } return &image{ - blobSize: blobSize, - size: size, - platform: plt, - config: &manifest.Config, + blobSize: blobSize, + size: size, + platform: plt, + config: &manifest.Config, + manifestDigest: desc.Digest, + available: true, }, nil } @@ -343,7 +388,20 @@ func readIndex(ctx context.Context, provider content.Provider, snapshotter snaps manifest, err := readManifest(ctx, provider, snapshotter, manifestDescriptor) if err != nil { - continue + // The index lists that platform, but its content is not in the store. Keep it as an + // unavailable entry: docker's tree lists those too, with zero sizes. Without a platform + // to name it by there is nothing to report, so drop it. + if manifestDescriptor.Platform == nil { + continue + } + manifest = &image{manifestDigest: manifestDescriptor.Digest} + } + // Prefer the platform declared by the index: it is the authoritative selector, while the + // image config may be less specific. Alpine, for instance, ships linux/arm/v6 and + // linux/arm/v7 manifests whose configs both say a bare "linux/arm", which normalizes to + // linux/arm/v7 and would collapse the two onto a single key, dropping one of them. + if manifestDescriptor.Platform != nil { + manifest.platform = platforms.Normalize(*manifestDescriptor.Platform) } descs[platforms.FormatAll(manifest.platform)] = manifest } @@ -372,11 +430,20 @@ func (x *imagePrinter) printImage(ctx context.Context, img images.Image) error { return err } + if x.tree { + return x.printImageTree(img, candidateImages) + } + if x.newView { return x.printImageCollapsed(img, candidateImages) } for platform, desc := range candidateImages { + // The legacy table describes what is in the store, so leave out the platforms the index + // mentions but that were never pulled (they also carry no config to describe). + if !desc.available { + continue + } if err := x.printImageSinglePlatform(*desc.config, img, desc.blobSize, desc.size, desc.platform); err != nil { log.G(ctx).WithError(err).Debugf("failed to get platform %q of image %q", platform, img.Name) } @@ -469,12 +536,6 @@ func (x *imagePrinter) printImageCollapsed(img images.Image, candidateImages map diskUsage := units.HumanSizeWithPrecision(float64(totalContentSize+totalSnapshotSize), 3) contentSize := units.HumanSizeWithPrecision(float64(totalContentSize), 3) - // The new view always truncates the ID (it never coexists with --no-trunc). - id := img.Target.Digest.String() - if _, hex, ok := strings.Cut(id, ":"); ok && len(hex) >= 12 { - id = hex[:12] - } - extra := "" if x.inUse[img.Target.Digest] { extra = "U" @@ -482,7 +543,7 @@ func (x *imagePrinter) printImageCollapsed(img images.Image, candidateImages map _, err := fmt.Fprintf(x.w, "%s\t%s\t%s\t%s\t%s\n", newViewImageRef(img.Name), - id, + shortImageID(img.Target.Digest), diskUsage, contentSize, extra, @@ -490,6 +551,72 @@ func (x *imagePrinter) printImageCollapsed(img images.Image, candidateImages map return err } +// Tree branch prefixes for the per-platform rows, matching docker/cli's tree view. +const ( + treeBranch = "├─ " + treeBranchLast = "└─ " +) + +// printImageTree renders `nerdctl images --tree` for one image: the collapsed row, followed by one +// row per platform present in the content store. +// +// Unlike docker/cli, which computes its column widths itself and can afford a blank line between +// images, the rows here go through a tabwriter shared with the header: a blank line would terminate +// its column block and misalign every following group, so the groups are separated by the branch +// glyphs alone. +func (x *imagePrinter) printImageTree(img images.Image, candidateImages map[string]*image) error { + if err := x.printImageCollapsed(img, candidateImages); err != nil { + return err + } + + children := make([]*image, 0, len(candidateImages)) + for _, candidate := range candidateImages { + children = append(children, candidate) + } + // The candidates come from a map, so they have to be ordered. Sorting on the full form rather + // than on the displayed one keeps that order stable even for the platforms that render + // identically, such as two windows/amd64 manifests differing only by OSVersion. + slices.SortFunc(children, func(a, b *image) int { + return strings.Compare(platforms.FormatAll(a.platform), platforms.FormatAll(b.platform)) + }) + + for i, child := range children { + branch := treeBranch + if i == len(children)-1 { + branch = treeBranchLast + } + // The displayed name drops the OSVersion, so it cannot serve as identity: an index may + // carry several windows/amd64 manifests that differ only by it. Match on the full form. + platform := platforms.Format(child.platform) + extra := "" + if x.inUseByPlatform[platformRef{img.Target.Digest, platforms.FormatAll(child.platform)}] { + extra = "U" + } + // Same size semantics as the collapsed row, for this platform alone. + if _, err := fmt.Fprintf(x.w, "%s%s\t%s\t%s\t%s\t%s\n", + branch, + platform, + shortImageID(child.manifestDigest), + units.HumanSizeWithPrecision(float64(child.blobSize+child.size), 3), + units.HumanSizeWithPrecision(float64(child.blobSize), 3), + extra, + ); err != nil { + return err + } + } + return nil +} + +// shortImageID renders a digest the way the Docker v29 views do: the hex part, truncated to 12 +// characters. Those views never coexist with --no-trunc, so the ID is always truncated. +func shortImageID(dgst digest.Digest) string { + id := dgst.String() + if _, hex, ok := strings.Cut(id, ":"); ok && len(hex) >= 12 { + return hex[:12] + } + return id +} + // printImagesLegend writes the right-aligned "In Use" legend for the Docker v29 default view. // Matching Docker, it is only emitted when the output is a terminal with a known width, so it // never pollutes piped or redirected output. @@ -564,23 +691,67 @@ func referenceHasDomain(name string) bool { return host == "localhost" || strings.ContainsAny(host, ".:") } -// imagesInUse returns the set of image target digests that are referenced by at least one -// container (in any state), used to render the Docker v29 "In Use" (U) indicator. Docker matches -// containers to images by digest, so every name pointing at the same target is flagged, not just -// the one the container was created from. -func imagesInUse(ctx context.Context, client *containerd.Client) map[digest.Digest]bool { +// platformRef identifies a single platform of a single image, used to flag the exact manifest a +// container runs in the tree view. +type platformRef struct { + target digest.Digest + // platform is in platforms.FormatAll form: the identity of a platform, unlike the name the + // tree displays, has to keep the OSVersion. + platform string +} + +// imagesInUse returns the image target digests that are referenced by at least one container (in +// any state), used to render the Docker v29 "In Use" (U) indicator. Docker matches containers to +// images by digest, so every name pointing at the same target is flagged, not just the one the +// container was created from. +// +// The second return value narrows this down to the platform each container actually runs, for the +// per-platform rows of the tree view. Both are collected in a single container scan. +func imagesInUse(ctx context.Context, client *containerd.Client) (map[digest.Digest]bool, map[platformRef]bool) { inUse := map[digest.Digest]bool{} + inUseByPlatform := map[platformRef]bool{} containerList, err := client.Containers(ctx) if err != nil { log.G(ctx).WithError(err).Warn("failed to list containers for image in-use detection") - return inUse + return inUse, inUseByPlatform } for _, container := range containerList { if dgst, ok := containerImageDigest(ctx, container); ok { inUse[dgst] = true + inUseByPlatform[platformRef{dgst, containerPlatform(ctx, container)}] = true } } - return inUse + return inUse, inUseByPlatform +} + +// containerPlatform reports the platform a container runs. Containers created outside nerdctl +// (e.g. by ctr or the CRI plugin) carry no platform label; assume the default platform for those, +// as pkg/imgutil/commit and `nerdctl container diff` already do. +func containerPlatform(ctx context.Context, container containerd.Container) string { + platform := "" + // The already-loaded metadata carries the labels, so this costs no extra round trip. + if info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata); err == nil { + platform = info.Labels[labels.Platform] + } + if platform == "" { + platform = platforms.DefaultString() + } + return normalizePlatform(platform) +} + +// normalizePlatform renders a platform string in the full form the manifest platforms are keyed by, +// so that the two can be compared. +// +// It keeps the OSVersion, which is what tells two otherwise identical windows/amd64 manifests +// apart, and normalizes the rest: platforms.DefaultString does not normalize, so on arm64 the +// label can carry a "v8"/"8" variant while the manifest platform normalizes to a bare +// "linux/arm64", and a raw comparison would never match. +func normalizePlatform(platform string) string { + parsed, err := platforms.Parse(platform) + if err != nil { + return platform + } + return platforms.FormatAll(platforms.Normalize(parsed)) } // containerImageDigest returns the image target a container was created from. diff --git a/pkg/cmd/image/list_test.go b/pkg/cmd/image/list_test.go index ccfa2a1338a..45e48b38199 100644 --- a/pkg/cmd/image/list_test.go +++ b/pkg/cmd/image/list_test.go @@ -17,12 +17,18 @@ package image import ( + "bytes" + "strings" "testing" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/platforms" + "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/labels" ) @@ -120,3 +126,257 @@ func TestPinnedImageDigest(t *testing.T) { }) } } + +func TestValidateListOptions(t *testing.T) { + testCases := []struct { + name string + options types.ImageListOptions + expected string + }{ + { + name: "no conflict without Tree", + options: types.ImageListOptions{Quiet: true, Format: "json"}, + }, + { + name: "no conflict for Tree alone", + options: types.ImageListOptions{Tree: true}, + }, + { + name: "quiet", + options: types.ImageListOptions{Tree: true, Quiet: true}, + expected: "--quiet is not yet supported with --tree", + }, + { + name: "no-trunc", + options: types.ImageListOptions{Tree: true, NoTrunc: true}, + expected: "--no-trunc is not yet supported with --tree", + }, + { + name: "digests", + options: types.ImageListOptions{Tree: true, Digests: true}, + expected: "--digests is not yet supported with --tree", + }, + { + name: "format", + options: types.ImageListOptions{Tree: true, Format: "json"}, + expected: "--format is not yet supported with --tree", + }, + { + name: "names", + options: types.ImageListOptions{Tree: true, Names: true}, + expected: "--names is not yet supported with --tree", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateListOptions(&tc.options) + if tc.expected == "" { + assert.NilError(t, err) + return + } + assert.Error(t, err, tc.expected) + }) + } +} + +func TestShortImageID(t *testing.T) { + testCases := []struct { + name string + dgst digest.Digest + expected string + }{ + { + name: "digest is truncated to 12 hex characters", + dgst: digest.Digest("sha256:" + strings.Repeat("a", 64)), + expected: strings.Repeat("a", 12), + }, + { + name: "a value without an algorithm is left alone", + dgst: digest.Digest("not-a-digest"), + expected: "not-a-digest", + }, + { + name: "a hex part shorter than 12 characters is left alone", + dgst: digest.Digest("sha256:abcd"), + expected: "sha256:abcd", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, shortImageID(tc.dgst), tc.expected) + }) + } +} + +func TestNormalizePlatform(t *testing.T) { + // The platforms are keyed by platforms.FormatAll(platforms.Normalize(...)), so an arm64 + // manifest is keyed as a bare "linux/arm64" while a Windows one keeps its OSVersion. A + // container's label may still carry the arm variant, because platforms.DefaultString does not + // normalize. + testCases := []struct { + platform string + expected string + }{ + {"linux/arm64/v8", "linux/arm64"}, + {"linux/arm64/8", "linux/arm64"}, + {"linux/arm64", "linux/arm64"}, + {"linux/amd64", "linux/amd64"}, + {"linux/arm/v7", "linux/arm/v7"}, + {"linux/armhf", "linux/arm/v7"}, + // the OSVersion is what tells two windows/amd64 manifests apart, so it is kept + {"windows(10.0.20348.2582)/amd64", "windows(10.0.20348.2582)/amd64"}, + {"windows/amd64", "windows/amd64"}, + // an unparsable value is passed through rather than dropped + {"", ""}, + } + for _, tc := range testCases { + t.Run(tc.platform, func(t *testing.T) { + assert.Equal(t, normalizePlatform(tc.platform), tc.expected) + }) + } +} + +// treeTestImage builds a candidate platform entry with sizes chosen to render exactly at the +// 3-significant-digit precision the Docker v29 views use. +func treeTestImage(os, arch string, dgst digest.Digest, blobSize, snapshotSize int64) *image { + return &image{ + blobSize: blobSize, + size: snapshotSize, + platform: platforms.Platform{OS: os, Architecture: arch}, + manifestDigest: dgst, + available: true, + } +} + +func TestPrintImageTree(t *testing.T) { + const ( + targetDigest = digest.Digest("sha256:" + "1111111111111111111111111111111111111111111111111111111111111111") + amd64Digest = digest.Digest("sha256:" + "2222222222222222222222222222222222222222222222222222222222222222") + arm64Digest = digest.Digest("sha256:" + "3333333333333333333333333333333333333333333333333333333333333333") + ) + img := images.Image{ + Name: "docker.io/library/nginx:latest", + Target: ocispec.Descriptor{Digest: targetDigest}, + } + + t.Run("multi-platform image expands into a sorted row per platform", func(t *testing.T) { + var buf bytes.Buffer + printer := &imagePrinter{ + w: &buf, + newView: true, + tree: true, + inUse: map[digest.Digest]bool{targetDigest: true}, + // Only the amd64 manifest is actually run by a container. + inUseByPlatform: map[platformRef]bool{{targetDigest, "linux/amd64"}: true}, + } + // Deliberately insert arm64 first: the candidates come from a map, so the printer has to + // sort them itself to stay deterministic. + candidates := map[string]*image{ + "linux/arm64": treeTestImage("linux", "arm64", arm64Digest, 24_000_000, 45_000_000), + "linux/amd64": treeTestImage("linux", "amd64", amd64Digest, 25_000_000, 47_000_000), + } + + assert.NilError(t, printer.printImageTree(img, candidates)) + + // DISK USAGE is content plus snapshots, CONTENT SIZE is content alone; the parent row + // aggregates both across the platforms. + expected := strings.Join([]string{ + "nginx:latest\t111111111111\t141MB\t49MB\tU", + "├─ linux/amd64\t222222222222\t72MB\t25MB\tU", + "└─ linux/arm64\t333333333333\t69MB\t24MB\t", + }, "\n") + "\n" + assert.Equal(t, buf.String(), expected) + }) + + // Regression test: the displayed name drops the OSVersion, so using it as the identity made a + // container on one Windows build flag every windows/amd64 row of the index. + t.Run("windows platforms differing only by OSVersion are told apart", func(t *testing.T) { + var buf bytes.Buffer + printer := &imagePrinter{ + w: &buf, + newView: true, + tree: true, + inUse: map[digest.Digest]bool{targetDigest: true}, + // A container runs the older build only. + inUseByPlatform: map[platformRef]bool{{targetDigest, "windows(10.0.20348.2582)/amd64"}: true}, + } + candidates := map[string]*image{ + "windows(10.0.26100.1)/amd64": { + blobSize: 24_000_000, + size: 45_000_000, + platform: platforms.Platform{OS: "windows", Architecture: "amd64", OSVersion: "10.0.26100.1"}, + manifestDigest: arm64Digest, + available: true, + }, + "windows(10.0.20348.2582)/amd64": { + blobSize: 25_000_000, + size: 47_000_000, + platform: platforms.Platform{OS: "windows", Architecture: "amd64", OSVersion: "10.0.20348.2582"}, + manifestDigest: amd64Digest, + available: true, + }, + } + + assert.NilError(t, printer.printImageTree(img, candidates)) + + // Both rows render as windows/amd64, but only the one the container runs is flagged, and + // the order is stable because the sort uses the full platform. + expected := strings.Join([]string{ + "nginx:latest\t111111111111\t141MB\t49MB\tU", + "├─ windows/amd64\t222222222222\t72MB\t25MB\tU", + "└─ windows/amd64\t333333333333\t69MB\t24MB\t", + }, "\n") + "\n" + assert.Equal(t, buf.String(), expected) + }) + + t.Run("a platform listed by the index but never pulled has no size", func(t *testing.T) { + var buf bytes.Buffer + printer := &imagePrinter{ + w: &buf, + newView: true, + tree: true, + inUse: map[digest.Digest]bool{}, + inUseByPlatform: map[platformRef]bool{}, + } + candidates := map[string]*image{ + "linux/amd64": treeTestImage("linux", "amd64", amd64Digest, 25_000_000, 47_000_000), + // Listed by the index, but its content is not in the store: no config, no sizes. + "linux/arm64": { + platform: platforms.Platform{OS: "linux", Architecture: "arm64"}, + manifestDigest: arm64Digest, + }, + } + + assert.NilError(t, printer.printImageTree(img, candidates)) + + expected := strings.Join([]string{ + "nginx:latest\t111111111111\t72MB\t25MB\t", + "├─ linux/amd64\t222222222222\t72MB\t25MB\t", + "└─ linux/arm64\t333333333333\t0B\t0B\t", + }, "\n") + "\n" + assert.Equal(t, buf.String(), expected) + }) + + t.Run("single-platform image gets a single closing branch", func(t *testing.T) { + var buf bytes.Buffer + printer := &imagePrinter{ + w: &buf, + newView: true, + tree: true, + inUse: map[digest.Digest]bool{}, + inUseByPlatform: map[platformRef]bool{}, + } + candidates := map[string]*image{ + "linux/amd64": treeTestImage("linux", "amd64", targetDigest, 25_000_000, 47_000_000), + } + + assert.NilError(t, printer.printImageTree(img, candidates)) + + expected := strings.Join([]string{ + "nginx:latest\t111111111111\t72MB\t25MB\t", + "└─ linux/amd64\t111111111111\t72MB\t25MB\t", + }, "\n") + "\n" + assert.Equal(t, buf.String(), expected) + }) +} diff --git a/pkg/testutil/images.yaml b/pkg/testutil/images.yaml index 4e51e332237..2499fbb83f4 100644 --- a/pkg/testutil/images.yaml +++ b/pkg/testutil/images.yaml @@ -8,15 +8,32 @@ alpine: schemaversion: 2 mediatype: "application/vnd.docker.distribution.manifest.list.v2+json" digest: "sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a" - variants: ["linux/amd64", "linux/arm64"] + variants: ["linux/386", "linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64", "linux/ppc64le", "linux/s390x"] manifests: linux/amd64: mediatype: "application/vnd.docker.distribution.manifest.v2+json" manifest: "sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310" config: "sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c" raw: "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MiwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6NDlmMzU2ZmE0NTEzNjc2YzVlMjJlM2E4NDA0YWFkNmM3MjYyY2M3YWFlZDE1MzQxNDU4MjY1MzIwNzg2YzU4YyIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI4MTE5NDcsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OmNhM2NkNDJhN2M5NTI1ZjZjZTNkNjRjMWE3MDk4MjYxM2E4MjM1ZjBjYzA1N2VjOTI0NDA1MjkyMTg1M2VmMTUiCiAgICAgIH0KICAgXQp9" + contentsize: 2813947 linux/arm64: manifest: "sha256:071fa5de01a240dbef5be09d69f8fef2f89d68445d9175393773ee389b6f5935" + contentsize: 2713919 + linux/arm/v6: + manifest: "sha256:cba24b50b9d81704968f65455897a3a519568b236c174c9040135c5afee5dc54" + contentsize: 2624114 + linux/arm/v7: + manifest: "sha256:59b46c319f3b66dfda96faafd0c6959e9b2f409792d0236204f270dfd0235960" + contentsize: 2426106 + linux/386: + manifest: "sha256:e10c13a5af47b1f2f5e3fbb9355fa82bc1234567a83a549d61d15697131e6e66" + contentsize: 2820800 + linux/ppc64le: + manifest: "sha256:f3a907bc0278ea0de7ddafcbca3c9a63cee253a4698eb248a4416d46fc906dbd" + contentsize: 2815221 + linux/s390x: + manifest: "sha256:44f0cac18b69c3867be12e78766393adf801560a102fe0113bb4abc981acf9bf" + contentsize: 2604591 busybox: ref: "ghcr.io/containerd/busybox" diff --git a/pkg/testutil/images_linux.go b/pkg/testutil/images_linux.go index 87d371961e7..c587493e3a5 100644 --- a/pkg/testutil/images_linux.go +++ b/pkg/testutil/images_linux.go @@ -19,6 +19,7 @@ package testutil import ( _ "embed" "fmt" + "slices" "sync" "go.yaml.in/yaml/v3" @@ -34,6 +35,9 @@ type manifestInfo struct { Manifest string `yaml:"manifest,omitempty"` MediaType string `yaml:"mediatype,omitempty"` Raw string `yaml:"raw,omitempty"` + // ContentSize is the sum of the blob sizes of this platform (its manifest, config and layers), + // which is what `nerdctl images` reports as CONTENT SIZE. + ContentSize int64 `yaml:"contentsize,omitempty"` } type TestImage struct { @@ -119,3 +123,26 @@ func GetTestImageRaw(key, platform string) string { } return pd.Raw } + +// GetTestImageContentSize returns the expected CONTENT SIZE of one platform of a test image, in +// bytes: the sum of the sizes of its manifest, config and layer blobs. +func GetTestImageContentSize(key, platform string) int64 { + im := lookup(key) + pd, ok := im.Manifests[platform] + if !ok { + panic(fmt.Sprintf("platform %s not found for image %s", platform, key)) + } + return pd.ContentSize +} + +// GetTestImagePlatforms returns the platforms declared for a test image, sorted, in the normalized +// form the image listing prints them (e.g. "linux/arm64", not "linux/arm64/v8"). +func GetTestImagePlatforms(key string) []string { + im := lookup(key) + platformz := make([]string, 0, len(im.Manifests)) + for platform := range im.Manifests { + platformz = append(platformz, platform) + } + slices.Sort(platformz) + return platformz +} From 6df4b8a8c9d4596e3b3b1e6b1268f23dc1a5bb7d Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Tue, 25 Aug 2026 19:12:11 -0500 Subject: [PATCH 768/868] /var/lib/journal -> /var/log/journal Signed-off-by: Aaron Paterson --- pkg/cmd/container/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index e2f89159d95..7cb8dfd5391 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -616,7 +616,7 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage, {Type: "tmpfs", Source: "tmpfs", Destination: "/run"}, {Type: "tmpfs", Source: "tmpfs", Destination: "/run/lock"}, {Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"}, - {Type: "tmpfs", Source: "tmpfs", Destination: "/var/lib/journal"}, + {Type: "tmpfs", Source: "tmpfs", Destination: "/var/log/journal"}, }), ) stopSignal = "SIGRTMIN+3" From 89c2e65197f68e9baf742a65b2bf9b641aa7381c Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Fri, 28 Aug 2026 14:51:38 +0400 Subject: [PATCH 769/868] fix: preserve equals signs in image label filter values Signed-off-by: Immanuel Tikhonov --- pkg/imgutil/filtering.go | 2 +- pkg/imgutil/filtering_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/imgutil/filtering.go b/pkg/imgutil/filtering.go index 30764f163c6..c7ffeb3f49e 100644 --- a/pkg/imgutil/filtering.go +++ b/pkg/imgutil/filtering.go @@ -63,7 +63,7 @@ type Filter func([]images.Image) ([]images.Image, error) func ParseFilters(filters []string) (*Filters, error) { f := &Filters{Labels: make(map[string]string)} for _, filter := range filters { - tempFilterToken := strings.Split(filter, "=") + tempFilterToken := strings.SplitN(filter, "=", 3) switch len(tempFilterToken) { case 1: return nil, fmt.Errorf("invalid filter %q", filter) diff --git a/pkg/imgutil/filtering_test.go b/pkg/imgutil/filtering_test.go index 7d82cb2ce60..7b0b3995f74 100644 --- a/pkg/imgutil/filtering_test.go +++ b/pkg/imgutil/filtering_test.go @@ -25,6 +25,15 @@ import ( "github.com/containerd/containerd/v2/core/images" ) +func TestParseFiltersLabelValueContainingEquals(t *testing.T) { + filters, err := ParseFilters([]string{"label=example.payload=a=b"}) + assert.NilError(t, err) + assert.DeepEqual(t, filters.Labels, map[string]string{"example.payload": "a=b"}) + + _, err = ParseFilters([]string{"dangling=true=garbage"}) + assert.Error(t, err, `invalid filter "dangling=true=garbage"`) +} + func TestApplyFilters(t *testing.T) { tests := []struct { name string From 308014c41e79155464da367ad09502ea3fe858fc Mon Sep 17 00:00:00 2001 From: Saleh Date: Mon, 31 Aug 2026 10:33:22 +0300 Subject: [PATCH 770/868] dockercompat: key inspect Networks by real CNI network name nerdctl inspect keyed NetworkSettings.Networks by a synthesized "unknown-" name (e.g. "unknown-eth0") instead of the network the endpoint actually belongs to. go-cni names the i-th attached network's interface "eth" in the order the networks were configured, and that ordered list is recorded in the nerdctl/networks spec annotation. Resolve each interface back to its network name through that list, falling back to the previous "unknown-" key when there is no match (host networking, an interface not created by CNI, or a missing networks annotation). Fixes #2999 Signed-off-by: Saleh --- pkg/inspecttypes/dockercompat/dockercompat.go | 34 +++++++- .../dockercompat/dockercompat_test.go | 77 +++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index d3e1a8ba040..ae5503990c0 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -805,7 +805,7 @@ func statusFromNative(x containerd.Status, labels map[string]string) string { } } -func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings, error) { +func networkSettingsFromNative(n *native.NetNS, spec *specs.Spec) (*NetworkSettings, error) { res := &NetworkSettings{ Networks: make(map[string]*NetworkEndpointSettings), } @@ -815,6 +815,19 @@ func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings return res, nil } + // CNI names the interface for the i-th network "eth" (see go-cni + // getIfName), in the order the container's networks were attached. Recover + // that ordered list from the spec annotations so each endpoint can be keyed + // by its real network name instead of a synthesized "unknown-*" placeholder. + var networks []string + if spec != nil { + if networksJSON := spec.Annotations[labels.Networks]; networksJSON != "" { + if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil { + return nil, fmt.Errorf("failed to parse networks annotation %q: %w", networksJSON, err) + } + } + } + var primary *NetworkEndpointSettings for _, x := range n.Interfaces { if x.Interface.Flags&net.FlagLoopback != 0 { @@ -844,9 +857,7 @@ func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings nes.GlobalIPv6PrefixLen = ones } } - // TODO: set CNI name when possible - fakeDockerNetworkName := fmt.Sprintf("unknown-%s", x.Name) - res.Networks[fakeDockerNetworkName] = nes + res.Networks[cniNetworkName(x.Name, networks)] = nes nports, err := convertToNatPort(n.PortMappings) if err != nil { @@ -871,6 +882,21 @@ func networkSettingsFromNative(n *native.NetNS, _ *specs.Spec) (*NetworkSettings return res, nil } +// cniNetworkName maps a container interface name to the CNI network it belongs +// to. go-cni names the i-th network's interface "" (defaulting to +// "eth0", "eth1", ...; see go-cni getIfName), so an "eth" interface resolves +// to networks[i]. Anything that does not fit that scheme (host networking, an +// interface not created by CNI, or a missing/short networks list) falls back to +// the historical "unknown-" key. +func cniNetworkName(ifName string, networks []string) string { + if idx, ok := strings.CutPrefix(ifName, cni.DefaultPrefix); ok { + if i, err := strconv.Atoi(idx); err == nil && i >= 0 && i < len(networks) { + return networks[i] + } + } + return fmt.Sprintf("unknown-%s", ifName) +} + func cpuSettingsFromNative(sp *specs.Spec) (*CPUSettings, error) { res := &CPUSettings{} if sp.Linux != nil && sp.Linux.Resources != nil && sp.Linux.Resources.CPU != nil { diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 6fe15a6822b..83ae7b1cca5 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -784,6 +784,83 @@ func TestNetworkSettingsFromNative(t *testing.T) { }, }, }, + // Given native.NetNS whose eth0 maps to a named CNI network, Return + // NetworkSettings keyed by the real network name rather than "unknown-*". + // UseCase: Inspect a Running Container attached to a named network (issue #2999) + { + name: "Given NetNS with eth0 and a networks annotation, Return NetworkSettings keyed by network name", + n: &native.NetNS{ + Interfaces: []native.NetInterface{ + { + Interface: net.Interface{ + Index: 2, + MTU: 1500, + Name: "eth0", + Flags: net.FlagUp, + }, + HardwareAddr: "fa:b9:e3:9f:67:1b", + Flags: []string{}, + Addrs: []string{"10.4.0.50/24"}, + }, + }, + }, + s: &specs.Spec{ + Annotations: map[string]string{ + labels.Networks: `["bridge"]`, + }, + }, + expected: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{ + "bridge": { + IPAddress: "10.4.0.50", + IPPrefixLen: 24, + MacAddress: "fa:b9:e3:9f:67:1b", + }, + }, + }, + }, + // Given native.NetNS with eth0/eth1 and two networks, Return each + // endpoint keyed by its network name, matched in networks-list order. + { + name: "Given NetNS with eth0/eth1 and two networks, Return NetworkSettings keyed by network names", + n: &native.NetNS{ + Interfaces: []native.NetInterface{ + { + Interface: net.Interface{Index: 2, MTU: 1500, Name: "eth0", Flags: net.FlagUp}, + HardwareAddr: "fa:b9:e3:9f:67:1b", + Flags: []string{}, + Addrs: []string{"10.4.0.50/24"}, + }, + { + Interface: net.Interface{Index: 3, MTU: 1500, Name: "eth1", Flags: net.FlagUp}, + HardwareAddr: "fa:b9:e3:9f:67:1c", + Flags: []string{}, + Addrs: []string{"10.5.0.60/24"}, + }, + }, + }, + s: &specs.Spec{ + Annotations: map[string]string{ + labels.Networks: `["bridge","mynet"]`, + }, + }, + expected: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{ + "bridge": { + IPAddress: "10.4.0.50", + IPPrefixLen: 24, + MacAddress: "fa:b9:e3:9f:67:1b", + }, + "mynet": { + IPAddress: "10.5.0.60", + IPPrefixLen: 24, + MacAddress: "fa:b9:e3:9f:67:1c", + }, + }, + }, + }, } for _, tc := range testcase { From 569b456e917e34978076b149517478ac57f5a326 Mon Sep 17 00:00:00 2001 From: MsfPablo Date: Mon, 31 Aug 2026 18:55:59 +0200 Subject: [PATCH 771/868] docs: fix typos in command reference, cosign and gpu docs - command-reference.md: complete the truncated word in the --rdt-class description ("container wit" -> "container with"), matching the flag's actual usage string in cmd/nerdctl/container/container_run.go. - cosign.md: "capibility" -> "capability". - gpu.md: "ouptut" -> "output". Signed-off-by: MsfPablo --- docs/command-reference.md | 2 +- docs/cosign.md | 2 +- docs/gpu.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index b97eb45b50f..9ea30cbc009 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -240,7 +240,7 @@ Resource flags: Intel RDT flags: -- :nerd_face: `--rdt-class=CLASS`: Name of the RDT class (or CLOS) to associate the container wit +- :nerd_face: `--rdt-class=CLASS`: Name of the RDT class (or CLOS) to associate the container with User flags: diff --git a/docs/cosign.md b/docs/cosign.md index 3aa3cb0af75..ef81bb9c708 100644 --- a/docs/cosign.md +++ b/docs/cosign.md @@ -92,7 +92,7 @@ INFO[0003] cosign: failed to verify signature ## Cosign in Compose -> Cosign support in Compose is also experimental and implemented based on Compose's [extension](https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension) capibility. +> Cosign support in Compose is also experimental and implemented based on Compose's [extension](https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension) capability. cosign is supported in `nerdctl compose up|run|push|pull`. You can use cosign in Compose by adding the following fields in your compose yaml. These fields are _per service_, and you can enable only `verify` or only `sign` (or both). diff --git a/docs/gpu.md b/docs/gpu.md index 536e7c50cd3..cd6df2c9de2 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -90,7 +90,7 @@ is used to request a CDI device: nerdctl run --device=nvidia.com/gpu=all ``` -Ensure that the NVIDIA (or AMD) Container Toolkit is installed and the requested CDI devices are present in the ouptut of `nvidia-ctk cdi list` (or `amd-ctk cdi list` for AMD GPUs): +Ensure that the NVIDIA (or AMD) Container Toolkit is installed and the requested CDI devices are present in the output of `nvidia-ctk cdi list` (or `amd-ctk cdi list` for AMD GPUs): ``` $ nvidia-ctk cdi list From 5e4b973fb081785ea42406c10c169a2d50d7e507 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:32:33 +0000 Subject: [PATCH 772/868] build(deps): bump github.com/containerd/containerd/api Bumps the containerd group with 1 update: [github.com/containerd/containerd/api](https://github.com/containerd/containerd). Updates `github.com/containerd/containerd/api` from 1.12.0-beta.0 to 1.12.0-rc.0 - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/api/v1.12.0-beta.0...api/v1.12.0-rc.0) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/api dependency-version: 1.12.0-rc.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: containerd ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 779ccf0918b..f43f2e02409 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/containerd/accelerated-container-image v1.4.4 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.12.0-beta.0 + github.com/containerd/containerd/api v1.12.0-rc.0 github.com/containerd/containerd/v2 v2.4.0-beta.0 //gomodjail:unconfined github.com/containerd/continuity v0.5.0 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 @@ -115,7 +115,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined - github.com/sirupsen/logrus v1.9.4 // indirect + github.com/sirupsen/logrus v1.10.2 // indirect github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect diff --git a/go.sum b/go.sum index 43aa97833f6..367e1f25065 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.12.0-beta.0 h1:0eGYh95iM9nn8Fygpx+96LvYIuc5xOiHvwo5hrsvTlI= -github.com/containerd/containerd/api v1.12.0-beta.0/go.mod h1:/tQDq0fxPDGz9vrSpfhmFMfoR7s/uzvYMCY/qKygl9Y= +github.com/containerd/containerd/api v1.12.0-rc.0 h1:Uu/0brBDz8nW/Vo+g8ah/iJkhZML+1z+X44g8q70MVw= +github.com/containerd/containerd/api v1.12.0-rc.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc= github.com/containerd/containerd/v2 v2.4.0-beta.0 h1:uFwtEE0kwGngxOT0WVW3/mozESCVg+8hKUExSis7SLY= github.com/containerd/containerd/v2 v2.4.0-beta.0/go.mod h1:GOAqkxqN53nSzBcz9Y5jAM9JELRMo67r34O/HDCY4Ro= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= @@ -81,8 +81,6 @@ github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooY 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= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= @@ -228,8 +226,6 @@ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -243,8 +239,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+x github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -262,8 +258,8 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= From 5092e2b321564fc8b76864eb1f457a1a056b4640 Mon Sep 17 00:00:00 2001 From: Vladislav Lapin <51929896+loglapa@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:24:27 +0400 Subject: [PATCH 773/868] ci: print daemon logs after test failures Signed-off-by: Vladislav Lapin <51929896+loglapa@users.noreply.github.com> --- .github/workflows/job-test-in-host.yml | 28 ++++++++++++++++++++++++++ .github/workflows/job-test-in-lima.yml | 26 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index d1fbb59a84e..6cea990986b 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -333,3 +333,31 @@ jobs: INPUTS_IPV6: ${{ inputs.ipv6 }} CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER: ${{ inputs.target == 'rootless-port-slirp4netns' && 'slirp4netns' || '' }} CONTAINERD_ROOTLESS_ROOTLESSKIT_IPV6: ${{ inputs.ipv6 && 'true' || '' }} + + - if: ${{ failure() && contains(inputs.runner, 'ubuntu') && inputs.target != '' && env.SHOULD_RUN == 'yes' }} + name: "Post (linux): print daemon logs" + env: + INPUTS_TARGET: ${{ inputs.target }} + run: | + echo "::group::containerd and buildkit system service logs" + sudo journalctl \ + --boot \ + --no-pager \ + --lines=1000 \ + --unit=containerd.service \ + --unit=buildkit.service \ + --unit=test-integration-buildkit-nerdctl-test.service || true + echo "::endgroup::" + + if [[ "${INPUTS_TARGET}" == rootless* ]]; then + echo "::group::rootless daemon logs" + journalctl \ + --user \ + --boot \ + --no-pager \ + --lines=1000 \ + --unit=containerd.service \ + --unit=buildkit.service \ + --unit=test-integration-buildkit-nerdctl-test.service || true + echo "::endgroup::" + fi diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index 01e3bbef3d6..bc939d0541e 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -127,3 +127,29 @@ jobs: else lima bash -c 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" WITH_SUDO=true && ./hack/test-integration.sh -test.only-flaky=true' fi + + - if: ${{ failure() }} + name: "Post: print daemon logs" + run: | + echo "::group::containerd and buildkit system service logs" + lima sudo journalctl \ + --boot \ + --no-pager \ + --lines=1000 \ + --unit=containerd.service \ + --unit=buildkit.service \ + --unit=test-integration-buildkit-nerdctl-test.service || true + echo "::endgroup::" + + if [ "$TARGET" = "rootless" ]; then + echo "::group::rootless daemon logs" + lima journalctl \ + --user \ + --boot \ + --no-pager \ + --lines=1000 \ + --unit=containerd.service \ + --unit=buildkit.service \ + --unit=test-integration-buildkit-nerdctl-test.service || true + echo "::endgroup::" + fi From 9978f407177102aff11b1ebcc32463cb986c306e Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Tue, 1 Sep 2026 17:28:48 +0500 Subject: [PATCH 774/868] build: print the image ID with --quiet when the image is not loaded When buildkitd runs with the containerd worker, the built image does not need loading into the image store, and `nerdctl build -q` printed nothing: the digest was only printed by the load path taken with the OCI worker. Request a buildkit metadata file in quiet mode too, and print the digest from it after a successful build, so that `build -q` consistently outputs the image identifier. Fixes #2015 Signed-off-by: Dean Chen <862469039@qq.com> --- cmd/nerdctl/builder/builder_build_test.go | 33 +++++++++++++++ pkg/cmd/builder/build.go | 32 +++++++++++--- pkg/cmd/builder/build_test.go | 51 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index b7e3a384b54..f42d079fa68 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -507,6 +507,39 @@ CMD ["echo", "nerdctl-build-test-string"] testCase.Run(t) } +func TestBuildQuiet(t *testing.T) { + nerdtest.Setup() + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + + testCase := &test.Case{ + Require: nerdtest.Build, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + // Regardless of whether the buildkit worker loads the image into the image store + // or not, `build -q` must print the image identifier on stdout. + // https://github.com/containerd/nerdctl/issues/2015 + imageID := strings.TrimSpace(helpers.Capture("build", "-q", "-t", data.Identifier(), data.Temp().Path())) + assert.Assert(helpers.T(), regexp.MustCompile(`^sha256:[0-9a-f]{64}$`).MatchString(imageID), + "expected `build -q` to output a valid image ID, got %q", imageID) + data.Labels().Set("imageID", imageID) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // The image ID printed by `build -q` must be usable to run the built image. + return helpers.Command("run", "--rm", data.Labels().Get("imageID")) + }, + + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), + } + + testCase.Run(t) +} + func TestBuildWithLabels(t *testing.T) { nerdtest.Setup() diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go index 9cd9ce15d5f..33075308352 100644 --- a/pkg/cmd/builder/build.go +++ b/pkg/cmd/builder/build.go @@ -106,13 +106,29 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder return err } - if options.IidFile != "" { + if metaFile != "" { id, err := getDigestFromMetaFile(metaFile) if err != nil { - return err - } - if err := filesystem.WriteFile(options.IidFile, []byte(id), 0644); err != nil { - return err + // A missing digest is fatal when the user explicitly asked for an iidfile, but not + // in quiet mode: the requested output may legitimately have no image digest + // (e.g. `--output type=local`). + if options.IidFile != "" { + return err + } + log.L.WithError(err).Debug("failed to get the image digest from the build metadata file") + } else { + if options.IidFile != "" { + if err := filesystem.WriteFile(options.IidFile, []byte(id), 0644); err != nil { + return err + } + } + // In quiet mode, the digest of a loaded image is printed by loadImage. + // When the image does not need loading (e.g. buildkitd with the containerd worker), + // print the digest here instead, so that `nerdctl build -q` outputs the image ID. + // https://github.com/containerd/nerdctl/issues/2015 + if options.Quiet && !needsLoading { + fmt.Fprintln(options.Stdout, id) + } } } @@ -452,7 +468,11 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option log.L.Warn("ignoring deprecated flag: '--rm=false'") } - if options.IidFile != "" { + // The metadata file is needed to obtain the image digest: when --iidfile is passed, + // and in quiet mode when the image is not loaded (e.g. buildkitd with the containerd worker), + // in which case the digest is not printed by the load path. + // https://github.com/containerd/nerdctl/issues/2015 + if options.IidFile != "" || (options.Quiet && !needsLoading) { file, err := os.CreateTemp("", "buildkit-meta-*") if err != nil { return "", nil, false, "", nil, cleanup, err diff --git a/pkg/cmd/builder/build_test.go b/pkg/cmd/builder/build_test.go index 6566fdd46d4..df2e82ed492 100644 --- a/pkg/cmd/builder/build_test.go +++ b/pkg/cmd/builder/build_test.go @@ -18,6 +18,7 @@ package builder import ( "fmt" + "os" "path/filepath" "runtime" "testing" @@ -281,3 +282,53 @@ func TestGetEffectiveSourcePolicyFile(t *testing.T) { }) } } + +func TestGetDigestFromMetaFile(t *testing.T) { + t.Parallel() + + const digest = "sha256:e2c8f34a2e73f9e11c93de402b9797adf95bab1e5ffb845b2cbe18f0e19dd0f1" + + tests := []struct { + name string + content string + expected string + wantErr bool + }{ + { + name: "digest present", + content: fmt.Sprintf(`{"containerimage.digest": %q}`, digest), + expected: digest, + }, + { + name: "digest missing", + content: `{"containerimage.config.digest": "whatever"}`, + wantErr: true, + }, + { + name: "invalid json", + content: `{`, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "meta.json") + assert.NilError(t, os.WriteFile(path, []byte(tc.content), 0o600)) + + id, err := getDigestFromMetaFile(path) + if tc.wantErr { + assert.Assert(t, err != nil) + } else { + assert.NilError(t, err) + assert.Equal(t, id, tc.expected) + } + + // The metadata file is a temporary file, and must be removed once read. + _, err = os.Stat(path) + assert.Assert(t, os.IsNotExist(err)) + }) + } +} From c3c38c0fe8ed3c7a4343660129381389988c355d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:31:44 +0000 Subject: [PATCH 775/868] build(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.0 to 1.83.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.83.0...v1.83.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.83.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f43f2e02409..9c0279b7be4 100644 --- a/go.mod +++ b/go.mod @@ -132,7 +132,7 @@ require ( golang.org/x/mod v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect //gomodjail:unconfined - google.golang.org/grpc v1.83.0 // indirect + google.golang.org/grpc v1.83.1 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 367e1f25065..657e31b2adb 100644 --- a/go.sum +++ b/go.sum @@ -389,8 +389,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= -google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 7da5153396579dc35a34b3a327be17523de27f67 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Fri, 7 Aug 2026 19:04:05 +0300 Subject: [PATCH 776/868] feat(system): add nerdctl system df Add `nerdctl system df`, the equivalent of `docker system df`, reporting how much disk space the images, containers and local volumes of the current namespace use, plus the BuildKit build cache. The sizes follow the Docker v29 definitions for the containerd image store: - the size of an image is the content present in the content store plus its unpacked snapshots, which is the same value `nerdctl images` shows as DISK USAGE, - the SIZE of the Images row counts every snapshot and every blob once, so it is the space really taken on disk rather than the sum of the image sizes, - an image is active when a container references it, and only the part of an unused image that no other image shares is reclaimable, - containers contribute their read-write layer only, and everything not running is reclaimable, - a volume is active when a container mounts it, counted once however many paths it is mounted at, as the LINKS column of Docker is a count of containers, - a build cache record is reclaimable when it is neither in use nor shared. An image reports when it was built, read from the config of the manifest the platform matcher of this host selects: the platforms of an index are not necessarily built together, and the creation time of the local record only says when the image was pulled or tagged. The record time stays as the fallback for the images that state nothing. Both `--format` and `-v/--verbose` are supported, including the Docker `table TEMPLATE` format, e.g. `table {{.Type}}\t{{.Size}}`: the literal \t and \n are expanded, the chosen columns get a header that names them whatever the template does to the values below, and the rows stay aligned. That helper lives in pkg/formatter so that the other commands, which all share this gap, can adopt it. Identifiers are shortened for the table output only, so that a custom format stays usable to look a resource up. The work is split the way `system prune` already is: `pkg/cmd/system` orchestrates, and each kind of resource is measured by its own package. Closes #3942 Signed-off-by: Eugene Kalinin --- cmd/nerdctl/system/system.go | 1 + cmd/nerdctl/system/system_df.go | 90 +++++ cmd/nerdctl/system/system_df_linux_test.go | 70 ++++ cmd/nerdctl/system/system_df_test.go | 275 +++++++++++++ docs/command-reference.md | 34 +- pkg/api/types/builder_types.go | 11 + pkg/api/types/diskusage_types.go | 118 ++++++ pkg/api/types/system_types.go | 14 + pkg/cmd/builder/df.go | 106 +++++ pkg/cmd/builder/df_test.go | 106 +++++ pkg/cmd/container/df.go | 158 ++++++++ pkg/cmd/image/df.go | 381 ++++++++++++++++++ pkg/cmd/image/df_test.go | 316 +++++++++++++++ pkg/cmd/image/list.go | 73 ++-- pkg/cmd/image/list_test.go | 8 +- pkg/cmd/system/df.go | 413 +++++++++++++++++++ pkg/cmd/system/df_test.go | 435 +++++++++++++++++++++ pkg/cmd/volume/df.go | 76 ++++ pkg/cmd/volume/df_test.go | 77 ++++ pkg/cmd/volume/rm.go | 41 +- pkg/containerdutil/content.go | 40 ++ pkg/formatter/common.go | 33 ++ pkg/formatter/table_test.go | 93 +++++ 23 files changed, 2928 insertions(+), 41 deletions(-) create mode 100644 cmd/nerdctl/system/system_df.go create mode 100644 cmd/nerdctl/system/system_df_linux_test.go create mode 100644 cmd/nerdctl/system/system_df_test.go create mode 100644 pkg/api/types/diskusage_types.go create mode 100644 pkg/cmd/builder/df.go create mode 100644 pkg/cmd/builder/df_test.go create mode 100644 pkg/cmd/container/df.go create mode 100644 pkg/cmd/image/df.go create mode 100644 pkg/cmd/image/df_test.go create mode 100644 pkg/cmd/system/df.go create mode 100644 pkg/cmd/system/df_test.go create mode 100644 pkg/cmd/volume/df.go create mode 100644 pkg/cmd/volume/df_test.go create mode 100644 pkg/formatter/table_test.go diff --git a/cmd/nerdctl/system/system.go b/cmd/nerdctl/system/system.go index dee993f45f7..d460baac071 100644 --- a/cmd/nerdctl/system/system.go +++ b/cmd/nerdctl/system/system.go @@ -33,6 +33,7 @@ func Command() *cobra.Command { } // versionCommand is not here cmd.AddCommand( + dfCommand(), EventsCommand(), InfoCommand(), pruneCommand(), diff --git a/cmd/nerdctl/system/system_df.go b/cmd/nerdctl/system/system_df.go new file mode 100644 index 00000000000..467b812225a --- /dev/null +++ b/cmd/nerdctl/system/system_df.go @@ -0,0 +1,90 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package system + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/system" +) + +func dfCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "df [flags]", + Short: "Show nerdctl disk usage", + Args: cobra.NoArgs, + RunE: dfAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().BoolP("verbose", "v", false, "Show detailed information on space usage") + cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}'") + return cmd +} + +func dfOptions(cmd *cobra.Command) (types.SystemDfOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.SystemDfOptions{}, err + } + + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return types.SystemDfOptions{}, err + } + + format, err := cmd.Flags().GetString("format") + if err != nil { + return types.SystemDfOptions{}, err + } + + buildkitHost, err := builder.GetBuildkitHost(cmd, globalOptions.Namespace) + if err != nil { + log.L.WithError(err).Warn("BuildKit is not running. The build cache usage will be reported as empty.") + buildkitHost = "" + } + + return types.SystemDfOptions{ + Stdout: cmd.OutOrStdout(), + Stderr: cmd.ErrOrStderr(), + GOptions: globalOptions, + Format: format, + Verbose: verbose, + BuildKitHost: buildkitHost, + }, nil +} + +func dfAction(cmd *cobra.Command, _ []string) error { + options, err := dfOptions(cmd) + if err != nil { + return err + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return system.Df(ctx, client, options) +} diff --git a/cmd/nerdctl/system/system_df_linux_test.go b/cmd/nerdctl/system/system_df_linux_test.go new file mode 100644 index 00000000000..f6795d2ba44 --- /dev/null +++ b/cmd/nerdctl/system/system_df_linux_test.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 system + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// TestSystemDfVolumes covers the Local Volumes row, which the rest of TestSystemDf cannot: a volume +// is only counted once a container mounts it, and the target of a mount is written differently on +// each platform. +func TestSystemDfVolumes(t *testing.T) { + testCase := nerdtest.Setup() + + // The counts are only meaningful when nothing else is running against the same namespace. + testCase.NoParallel = true + + testCase.SubTests = []*test.Case{ + { + Description: "mounted volume is active", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set(baselineLabel, helpers.Capture("system", "df")) + helpers.Ensure("volume", "create", data.Identifier()) + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "-v", fmt.Sprintf("%s:/volume", data.Identifier()), + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier()) + }, + Command: test.Command("system", "df"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + // The volume is created by this test and the container it runs mounts it, + // so both counts went up by it. + dfGrewBy(t, data, stdout, "Local Volumes", totalColumn, 1) + dfGrewBy(t, data, stdout, "Local Volumes", activeColumn, 1) + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/system/system_df_test.go b/cmd/nerdctl/system/system_df_test.go new file mode 100644 index 00000000000..404e24c4766 --- /dev/null +++ b/cmd/nerdctl/system/system_df_test.go @@ -0,0 +1,275 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package system + +import ( + "encoding/json" + "strconv" + "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/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" +) + +// dfRow returns the columns of the `nerdctl system df` summary row of the given type. The type is +// matched as a prefix because "Local Volumes" contains a space. +func dfRow(t tig.T, stdout, rowType string) []string { + for line := range strings.SplitSeq(stdout, "\n") { + if columns, ok := strings.CutPrefix(line, rowType); ok { + return strings.Fields(columns) + } + } + t.Log(stdout) + t.FailNow() + return nil +} + +// dfTotal and dfActive are the TOTAL and ACTIVE columns of a summary row. +func dfTotal(t tig.T, stdout, rowType string) string { + return dfRow(t, stdout, rowType)[0] +} + +func dfActive(t tig.T, stdout, rowType string) string { + return dfRow(t, stdout, rowType)[1] +} + +// baselineLabel holds the output of `system df` from before the test created anything. +const baselineLabel = "df-baseline" + +// dfGrewBy asserts that a column of a summary row went up by n since the baseline. Only the +// difference a test makes can be asserted: `nerdtest.Private` gives nerdctl a namespace of its own, +// but docker has none, so its daemon still holds whatever the other tests left behind. +func dfGrewBy(t tig.T, data test.Data, stdout, rowType string, column, n int) { + base := data.Labels().Get(baselineLabel) + before, err := strconv.Atoi(dfRow(t, base, rowType)[column]) + assert.NilError(t, err, base) + after, err := strconv.Atoi(dfRow(t, stdout, rowType)[column]) + assert.NilError(t, err, stdout) + assert.Equal(t, after, before+n, stdout) +} + +// The columns dfGrewBy counts, in the order `system df` prints them. +const ( + totalColumn = iota + activeColumn +) + +// dfReclaimable is the RECLAIMABLE column, which carries a percentage as a second field. +func dfReclaimable(t tig.T, stdout, rowType string) string { + return strings.Join(dfRow(t, stdout, rowType)[3:], " ") +} + +func TestSystemDf(t *testing.T) { + testCase := nerdtest.Setup() + + // The counts are only meaningful when nothing else is running against the same namespace. + testCase.NoParallel = true + + testCase.SubTests = []*test.Case{ + { + Description: "empty namespace", + // Docker has no namespaces, so there is no way to get a guaranteed empty daemon. + Require: require.All(nerdtest.Private, require.Not(nerdtest.Docker)), + Command: test.Command("system", "df"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "TYPE"), stdout) + for _, rowType := range []string{"Images", "Containers", "Local Volumes"} { + assert.Equal(t, dfTotal(t, stdout, rowType), "0", stdout) + assert.Equal(t, dfActive(t, stdout, rowType), "0", stdout) + } + // The build cache is not namespaced, so it is not asserted on here. + assert.Assert(t, strings.Contains(stdout, "Build Cache"), stdout) + }, + } + }, + }, + { + Description: "running container", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set(baselineLabel, helpers.Capture("system", "df")) + helpers.Ensure("run", "-d", "--name", data.Identifier(), + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("system", "df"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + // The container is created by this test, so it is the one the counts went + // up by. + dfGrewBy(t, data, stdout, "Containers", totalColumn, 1) + dfGrewBy(t, data, stdout, "Containers", activeColumn, 1) + + // The image the container runs is in use, so it is active and nothing of + // it can be reclaimed. Neither holds as a difference: the image may well + // have been pulled and in use already, which is what a shared daemon + // cannot be asked about. + if !nerdtest.IsDocker() { + assert.Equal(t, dfActive(t, stdout, "Images"), "1", stdout) + assert.Equal(t, dfReclaimable(t, stdout, "Images"), "0B (0%)", stdout) + } + }, + } + }, + }, + { + Description: "stopped container is reclaimable", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set(baselineLabel, helpers.Capture("system", "df")) + helpers.Ensure("run", "-d", "--name", data.Identifier(), + testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("stop", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("system", "df"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + // A stopped container is still counted, it just is not active any more, + // so its space can be reclaimed. + dfGrewBy(t, data, stdout, "Containers", totalColumn, 1) + dfGrewBy(t, data, stdout, "Containers", activeColumn, 0) + + // The image is no longer held by a running container, but it is still + // referenced by it, so it stays active. + if !nerdtest.IsDocker() { + assert.Equal(t, dfActive(t, stdout, "Images"), "1", stdout) + } + }, + } + }, + }, + { + Description: "unused image is reclaimable", + Require: require.All(nerdtest.Private, require.Not(nerdtest.Docker)), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + }, + Command: test.Command("system", "df"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Equal(t, dfTotal(t, stdout, "Images"), "1", stdout) + assert.Equal(t, dfActive(t, stdout, "Images"), "0", stdout) + // Nothing else holds the layers, so practically the whole image can be + // reclaimed. It falls just short of the total rather than matching it, + // because the index listing the manifests is on disk, and Docker counts + // it in the total while charging no single image for it. + _, percent, ok := strings.Cut(dfReclaimable(t, stdout, "Images"), " ") + assert.Assert(t, ok, stdout) + value, err := strconv.Atoi(strings.Trim(percent, "(%)")) + assert.NilError(t, err, stdout) + assert.Assert(t, value >= 99, stdout) + }, + } + }, + }, + { + Description: "verbose", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("system", "df", "--verbose"), + Expected: test.Expects(0, nil, expect.All( + expect.Contains("Images space usage:"), + expect.Contains("SHARED SIZE"), + expect.Contains("UNIQUE SIZE"), + expect.Contains("Containers space usage:"), + expect.Contains("LOCAL VOLUMES"), + expect.Contains("Local Volumes space usage:"), + expect.Contains("LINKS"), + expect.Contains("Build cache usage:"), + )), + }, + { + Description: "format json", + Require: nerdtest.Private, + Command: test.Command("system", "df", "--format", "json"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + var types []string + for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { + row := map[string]string{} + assert.NilError(t, json.Unmarshal([]byte(line), &row), line) + types = append(types, row["Type"]) + } + assert.DeepEqual(t, types, + []string{"Images", "Containers", "Local Volumes", "Build Cache"}) + }, + } + }, + }, + { + Description: "format template", + Require: nerdtest.Private, + Command: test.Command("system", "df", "--format", "{{.Type}}"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: expect.Equals("Images\nContainers\nLocal Volumes\nBuild Cache\n"), + } + }, + }, + { + Description: "format table template", + Require: nerdtest.Private, + Command: test.Command("system", "df", "--format", `table {{.Type}}\t{{.Size}}`), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Equal(t, len(lines), 5, stdout) + // The header names only the requested columns, and the \t the shell passed + // through literally became a real column separator. + assert.Equal(t, strings.Join(strings.Fields(lines[0]), " "), "TYPE SIZE", stdout) + assert.Equal(t, strings.Fields(lines[1])[0], "Images", stdout) + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index f7fd1e39572..c9dcab85ea4 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -97,6 +97,7 @@ - [:whale: nerdctl events](#whale-nerdctl-events) - [:whale: nerdctl info](#whale-nerdctl-info) - [:whale: nerdctl version](#whale-nerdctl-version) + - [:whale: nerdctl system df](#whale-nerdctl-system-df) - [:whale: nerdctl system prune](#whale-nerdctl-system-prune) - [Stats](#stats) - [:whale: nerdctl stats](#whale-nerdctl-stats) @@ -1599,6 +1600,38 @@ Flags: - :whale: `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}` +### :whale: nerdctl system df + +Show nerdctl disk usage + +Usage: `nerdctl system df [OPTIONS]` + +Flags: + +- :whale: `-v, --verbose`: Show detailed information on space usage +- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`. + `table` prints the default columns, and `table TEMPLATE` (e.g. `table {{.Type}}\t{{.Size}}`) + prints the columns of the template with a header and aligned columns. + +The images, containers and volumes are reported for the current namespace only. The build cache is +not namespaced by containerd; it is reported for the BuildKit host associated with the namespace, +and shows up as empty when BuildKit is not running. + +The sizes follow Docker v29: the size of an image is the content present in the content store plus +its unpacked snapshots, and the `SIZE` column of the `Images` row counts anything shared between +images only once, so it is smaller than the sum of the individual image sizes. + +Example: + +```console +$ nerdctl system df +TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 17 1 18.25GB 17.26GB (94%) +Containers 3 3 169.2MB 0B (0%) +Local Volumes 4 3 798.6GB 22.62MB (0%) +Build Cache 44 0 0B 0B +``` + ### :whale: nerdctl system prune Remove unused data @@ -2063,7 +2096,6 @@ Builder: Others: -- `docker system df` - `docker context` - Swarm commands are unimplemented and will not be implemented: `docker swarm|node|service|config|secret|stack *` - Plugin commands are unimplemented and will not be implemented: `docker plugin *` diff --git a/pkg/api/types/builder_types.go b/pkg/api/types/builder_types.go index 0d9445be505..944c7a5f8a0 100644 --- a/pkg/api/types/builder_types.go +++ b/pkg/api/types/builder_types.go @@ -78,6 +78,17 @@ type BuilderBuildOptions struct { SourcePolicyFile string } +// BuilderDiskUsageOptions specifies options for querying the build cache disk usage. +type BuilderDiskUsageOptions struct { + Stderr io.Writer + // GOptions is the global options + GOptions GlobalCommandOptions + // BuildKitHost is the buildkit host + BuildKitHost string + // Verbose requests the individual build cache records, not just the totals + Verbose bool +} + // BuilderPruneOptions specifies options for `nerdctl builder prune`. type BuilderPruneOptions struct { Stderr io.Writer diff --git a/pkg/api/types/diskusage_types.go b/pkg/api/types/diskusage_types.go new file mode 100644 index 00000000000..26430cdcb2e --- /dev/null +++ b/pkg/api/types/diskusage_types.go @@ -0,0 +1,118 @@ +/* + 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 types + +import "time" + +// DiskUsage is the disk usage of a single containerd namespace, as reported by `nerdctl system df`. +// The build cache is not namespaced by containerd; it is scoped by the BuildKit host instead. +type DiskUsage struct { + Images ImageDiskUsage + Containers ContainerDiskUsage + Volumes VolumeDiskUsage + BuildCache BuildCacheDiskUsage +} + +// ImageDiskUsage is the disk usage of the images of a namespace. +// +// TotalSize is the deduplicated total: every snapshot and every content blob is counted once, even +// when it is shared by several images. It is therefore not the sum of the Size of Items. +type ImageDiskUsage struct { + TotalCount int64 + ActiveCount int64 + TotalSize int64 + Reclaimable int64 + // Items is only populated when the verbose output was requested + Items []ImageDiskUsageItem +} + +// ImageDiskUsageItem is the disk usage of a single image. +type ImageDiskUsageItem struct { + ID string + Repository string + Tag string + CreatedAt time.Time + // Size is the content present in the content store plus the unpacked snapshots + Size int64 + // SharedSize is the part of Size that is also used by at least one other image + SharedSize int64 + // Containers is the number of containers created from this image + Containers int64 +} + +// ContainerDiskUsage is the disk usage of the containers of a namespace. +type ContainerDiskUsage struct { + TotalCount int64 + ActiveCount int64 + TotalSize int64 + Reclaimable int64 + // Items is only populated when the verbose output was requested + Items []ContainerDiskUsageItem +} + +// ContainerDiskUsageItem is the disk usage of a single container. +type ContainerDiskUsageItem struct { + ID string + Image string + Command string + LocalVolumes int64 + // SizeRw is the size of the read-write layer, without the size of the image + SizeRw int64 + CreatedAt time.Time + Status string + Names string +} + +// VolumeDiskUsage is the disk usage of the local volumes of a namespace. +type VolumeDiskUsage struct { + TotalCount int64 + ActiveCount int64 + TotalSize int64 + Reclaimable int64 + // Items is only populated when the verbose output was requested + Items []VolumeDiskUsageItem +} + +// VolumeDiskUsageItem is the disk usage of a single volume. +type VolumeDiskUsageItem struct { + Name string + // Links is the number of containers referencing this volume + Links int64 + Size int64 +} + +// BuildCacheDiskUsage is the disk usage of the BuildKit build cache. +type BuildCacheDiskUsage struct { + TotalCount int64 + ActiveCount int64 + TotalSize int64 + Reclaimable int64 + // Items is only populated when the verbose output was requested + Items []BuildCacheDiskUsageItem +} + +// BuildCacheDiskUsageItem is the disk usage of a single build cache record. +type BuildCacheDiskUsageItem struct { + ID string + CacheType string + Size int64 + CreatedAt time.Time + LastUsedAt *time.Time + UsageCount int + InUse bool + Shared bool +} diff --git a/pkg/api/types/system_types.go b/pkg/api/types/system_types.go index bfadba7a057..4a0aeaa873e 100644 --- a/pkg/api/types/system_types.go +++ b/pkg/api/types/system_types.go @@ -41,6 +41,20 @@ type SystemEventsOptions struct { Filters []string } +// SystemDfOptions specifies options for `nerdctl system df`. +type SystemDfOptions struct { + Stdout io.Writer + Stderr io.Writer + // GOptions is the global options + GOptions GlobalCommandOptions + // Format the output using the given Go template, e.g, '{{json .}} + Format string + // Verbose shows detailed information on space usage + Verbose bool + // BuildKitHost the address of BuildKit host + BuildKitHost string +} + // SystemPruneOptions specifies options for `nerdctl system prune`. type SystemPruneOptions struct { Stdout io.Writer diff --git a/pkg/cmd/builder/df.go b/pkg/cmd/builder/df.go new file mode 100644 index 00000000000..c5de945572b --- /dev/null +++ b/pkg/cmd/builder/df.go @@ -0,0 +1,106 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package builder + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os/exec" + + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" +) + +// DiskUsage reports how much disk space the BuildKit build cache uses. +// +// A record is active while a build holds it, and a record can be reclaimed when it is neither in use +// nor shared with another BuildKit worker, which is what `nerdctl builder prune` would free. +func DiskUsage(ctx context.Context, options types.BuilderDiskUsageOptions) (types.BuildCacheDiskUsage, error) { + records, err := diskUsageRecords(ctx, options) + if err != nil { + return types.BuildCacheDiskUsage{}, err + } + return aggregateDiskUsage(records, options.Verbose), nil +} + +// aggregateDiskUsage totals the build cache records the way Docker does. +func aggregateDiskUsage(records []buildkitutil.UsageInfo, verbose bool) types.BuildCacheDiskUsage { + du := types.BuildCacheDiskUsage{} + + for _, record := range records { + du.TotalCount++ + du.TotalSize += record.Size + if record.InUse { + du.ActiveCount++ + } + if !record.InUse && !record.Shared { + du.Reclaimable += record.Size + } + + if verbose { + du.Items = append(du.Items, types.BuildCacheDiskUsageItem{ + ID: record.ID, + CacheType: string(record.RecordType), + Size: record.Size, + CreatedAt: record.CreatedAt, + LastUsedAt: record.LastUsedAt, + UsageCount: record.UsageCount, + InUse: record.InUse, + Shared: record.Shared, + }) + } + } + + return du +} + +// diskUsageRecords runs `buildctl du` and decodes its output. Unlike `buildctl prune`, which streams +// one JSON object per pruned record, `buildctl du` applies the template to the whole result at once, +// so the output is a single JSON array. +func diskUsageRecords(ctx context.Context, options types.BuilderDiskUsageOptions) ([]buildkitutil.UsageInfo, error) { + buildctlBinary, err := buildkitutil.BuildctlBinary() + if err != nil { + return nil, err + } + buildctlArgs := buildkitutil.BuildctlBaseArgs(options.BuildKitHost) + buildctlArgs = append(buildctlArgs, "du", "--format={{json .}}") + + buildctlCmd := exec.CommandContext(ctx, buildctlBinary, buildctlArgs...) + log.G(ctx).Debugf("running %v", buildctlCmd.Args) + buildctlCmd.Stderr = options.Stderr + stdout := &bytes.Buffer{} + buildctlCmd.Stdout = stdout + if err := buildctlCmd.Run(); err != nil { + return nil, fmt.Errorf("failed to run %v: %w", buildctlCmd.Args, err) + } + + return parseDiskUsageRecords(stdout.Bytes()) +} + +// parseDiskUsageRecords decodes the JSON array `buildctl du --format={{json .}}` prints. An empty +// build cache is rendered as "null", which decodes into no records at all. +func parseDiskUsageRecords(output []byte) ([]buildkitutil.UsageInfo, error) { + var records []buildkitutil.UsageInfo + if err := json.Unmarshal(bytes.TrimSpace(output), &records); err != nil { + return nil, fmt.Errorf("failed to decode the output of buildctl du: %w", err) + } + return records, nil +} diff --git a/pkg/cmd/builder/df_test.go b/pkg/cmd/builder/df_test.go new file mode 100644 index 00000000000..d788bcf34f2 --- /dev/null +++ b/pkg/cmd/builder/df_test.go @@ -0,0 +1,106 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package builder + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" +) + +// buildctl du renders the whole result with one template pass, so the output is a JSON array, not +// the stream of objects buildctl prune produces. +const buildctlDuOutput = `[{"id":"n3vkjqf4tzxkgxwjdgm0e5vpm","mutable":false,"inUse":true,"size":102400,` + + `"createdAt":"2026-08-01T10:00:00Z","lastUsedAt":"2026-08-04T10:00:00Z","usageCount":2,` + + `"description":"pulled from docker.io/library/alpine:latest","recordType":"regular","shared":false},` + + `{"id":"xk3f4tzqjn0e5vpmgxwjdgm0e","mutable":false,"inUse":false,"size":2048,` + + `"createdAt":"2026-08-02T10:00:00Z","lastUsedAt":null,"usageCount":0,` + + `"description":"local source for context","recordType":"source.local","shared":false},` + + `{"id":"gm0e5vpmxk3f4tzqjn0egxwj","mutable":false,"inUse":false,"size":4096,` + + `"createdAt":"2026-08-03T10:00:00Z","lastUsedAt":null,"usageCount":0,` + + `"description":"shared with another worker","recordType":"regular","shared":true}] +` + +func TestParseDiskUsageRecords(t *testing.T) { + t.Parallel() + + records, err := parseDiskUsageRecords([]byte(buildctlDuOutput)) + assert.NilError(t, err) + assert.Equal(t, len(records), 3) + assert.Equal(t, records[0].ID, "n3vkjqf4tzxkgxwjdgm0e5vpm") + assert.Equal(t, records[0].InUse, true) + assert.Equal(t, records[0].UsageCount, 2) + assert.Assert(t, records[0].LastUsedAt != nil) + assert.Equal(t, string(records[1].RecordType), "source.local") + assert.Assert(t, records[1].LastUsedAt == nil) + assert.Equal(t, records[2].Shared, true) +} + +func TestParseDiskUsageRecordsEmpty(t *testing.T) { + t.Parallel() + + // An empty build cache is rendered by the Go template as "null". + records, err := parseDiskUsageRecords([]byte("null\n")) + assert.NilError(t, err) + assert.Equal(t, len(records), 0) +} + +func TestParseDiskUsageRecordsInvalid(t *testing.T) { + t.Parallel() + + _, err := parseDiskUsageRecords([]byte("not json")) + assert.ErrorContains(t, err, "buildctl du") +} + +func TestBuildCacheDiskUsageAggregation(t *testing.T) { + t.Parallel() + + records, err := parseDiskUsageRecords([]byte(buildctlDuOutput)) + assert.NilError(t, err) + + du := aggregateDiskUsage(records, true) + assert.Equal(t, du.TotalCount, int64(3)) + assert.Equal(t, du.TotalSize, int64(102400+2048+4096)) + // Only the record a build holds is active. + assert.Equal(t, du.ActiveCount, int64(1)) + // Neither the in-use record nor the one shared with another worker can be reclaimed. + assert.Equal(t, du.Reclaimable, int64(2048)) + assert.Equal(t, len(du.Items), 3) + assert.Equal(t, du.Items[0].CacheType, "regular") +} + +func TestBuildCacheDiskUsageWithoutVerbose(t *testing.T) { + t.Parallel() + + records, err := parseDiskUsageRecords([]byte(buildctlDuOutput)) + assert.NilError(t, err) + + du := aggregateDiskUsage(records, false) + assert.Equal(t, du.TotalCount, int64(3)) + // The individual records are only carried when they are going to be printed. + assert.Equal(t, len(du.Items), 0) +} + +func TestBuildCacheDiskUsageNoRecords(t *testing.T) { + t.Parallel() + + du := aggregateDiskUsage([]buildkitutil.UsageInfo{}, true) + assert.DeepEqual(t, du, types.BuildCacheDiskUsage{}) +} diff --git a/pkg/cmd/container/df.go b/pkg/cmd/container/df.go new file mode 100644 index 00000000000..a1642379a11 --- /dev/null +++ b/pkg/cmd/container/df.go @@ -0,0 +1,158 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/errdefs" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" + "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/labels" + "github.com/containerd/nerdctl/v2/pkg/mountutil" +) + +// DiskUsage reports how much disk space the containers of the current namespace use. +// +// Like Docker, only the read-write layer is counted: the layers coming from the image belong to the +// image, not to the container. Every container is counted, including the stopped ones, and the space +// of everything that is not running can be reclaimed. +func DiskUsage(ctx context.Context, client *containerd.Client, _ types.GlobalCommandOptions, verbose bool) (types.ContainerDiskUsage, error) { + du := types.ContainerDiskUsage{} + + containers, err := client.Containers(ctx) + if err != nil { + return du, err + } + + snapshottersCache := map[string]snapshots.Snapshotter{} + for _, c := range containers { + info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) + if err != nil { + // There is no guarantee that a container we just listed still exists. + if errdefs.IsNotFound(err) { + log.G(ctx).Debugf("container %q is gone - ignoring", c.ID()) + continue + } + return du, err + } + + snapshotter, ok := snapshottersCache[info.Snapshotter] + if !ok { + snapshotter = containerdutil.SnapshotService(client, info.Snapshotter) + snapshottersCache[info.Snapshotter] = snapshotter + } + + var sizeRw int64 + if info.SnapshotKey != "" { + // Only the read-write layer is wanted, so ask the snapshotter for that one snapshot + // rather than walking the chain: a parent missing from the image the container was + // created from says nothing about the size of the container, and must not fail the + // report over it. + usage, err := snapshotter.Usage(ctx, info.SnapshotKey) + if err != nil { + // The read-write layer is the container, so a NotFound here means it was removed + // while we were measuring it. + if errdefs.IsNotFound(err) && containerIsGone(ctx, client, c.ID()) { + log.G(ctx).Debugf("container %q is gone - ignoring", c.ID()) + continue + } + return du, fmt.Errorf("failed to get the size of container %q: %w", c.ID(), err) + } + sizeRw = usage.Size + } + + status := formatter.ContainerStatus(ctx, c) + + du.TotalCount++ + du.TotalSize += sizeRw + if isActiveStatus(status) { + du.ActiveCount++ + } else { + du.Reclaimable += sizeRw + } + + if verbose { + item := types.ContainerDiskUsageItem{ + ID: c.ID(), + Image: info.Image, + LocalVolumes: localVolumes(ctx, info.Labels), + SizeRw: sizeRw, + CreatedAt: info.CreatedAt, + Status: status, + Names: containerutil.GetContainerName(info.Labels), + } + if spec, err := c.Spec(ctx); err != nil { + log.G(ctx).WithError(err).Debugf("failed to get the spec of container %q", c.ID()) + } else { + item.Command = formatter.InspectContainerCommand(spec, true, true) + } + du.Items = append(du.Items, item) + } + } + + return du, nil +} + +// containerIsGone reports whether a container no longer exists, asking the container store rather +// than any metadata that was read before. +func containerIsGone(ctx context.Context, client *containerd.Client, id string) bool { + _, err := client.ContainerService().Get(ctx, id) + return errdefs.IsNotFound(err) +} + +// isActiveStatus reports whether a container occupies space that cannot be reclaimed. Docker treats +// the running, paused and restarting containers as active; the status strings are the ones produced +// by formatter.ContainerStatus. +func isActiveStatus(status string) bool { + for _, prefix := range []string{"Up", "Paused", "Pausing", "Restarting"} { + if strings.HasPrefix(status, prefix) { + return true + } + } + return false +} + +// localVolumes returns the number of named and anonymous volumes a container mounts. +func localVolumes(ctx context.Context, containerLabels map[string]string) int64 { + mountsJSON := labels.GetMount(containerLabels) + if mountsJSON == "" { + return 0 + } + var mounts []dockercompat.MountPoint + if err := json.Unmarshal([]byte(mountsJSON), &mounts); err != nil { + log.G(ctx).WithError(err).Debug("failed to parse the mounts of a container") + return 0 + } + var count int64 + for _, m := range mounts { + if m.Type == mountutil.Volume { + count++ + } + } + return count +} diff --git a/pkg/cmd/image/df.go b/pkg/cmd/image/df.go new file mode 100644 index 00000000000..0e0821a41dc --- /dev/null +++ b/pkg/cmd/image/df.go @@ -0,0 +1,381 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "time" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/identity" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/errdefs" + "github.com/containerd/log" + "github.com/containerd/platforms" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" + "github.com/containerd/nerdctl/v2/pkg/imgutil" +) + +// DiskUsage reports how much disk space the images of the current namespace use. +// +// The definitions follow Docker v29 with the containerd image store +// (moby/moby daemon/disk_usage.go and daemon/containerd/service.go): +// +// - the size of an image is the content present in the content store plus its unpacked snapshots, +// - TotalSize counts every snapshot and every blob once, even when several images share it, +// - an image is active when at least one container references it, +// - the reclaimable space is the size that is unique to the images no container references. +func DiskUsage(ctx context.Context, client *containerd.Client, gOptions types.GlobalCommandOptions, verbose bool) (types.ImageDiskUsage, error) { + du := types.ImageDiskUsage{} + + imageList, err := List(ctx, client, nil, nil) + if err != nil { + return du, err + } + + // An unknown in-use state is not a detail we may round off: it would report every image as + // inactive, and all of its unique bytes as reclaimable. + containers, _, err := imagesInUse(ctx, client) + if err != nil { + return du, err + } + + var ( + contentStore = client.ContentStore() + provider = containerdutil.NewProvider(client) + snapshotter = containerdutil.SnapshotService(client, gOptions.Snapshotter) + ) + + // The image store may hold several names for the same target (repo:tag, repo@digest, and under + // the k8s.io namespace the config digest as well). Docker reports one entry per target, so + // collapse them here, otherwise every count and every size would be multiplied. + uniqueImages := uniqueByTarget(imageList) + + collected := make([]*imageContents, 0, len(uniqueImages)) + index := newDiskUsageIndex() + + for _, img := range uniqueImages { + // The content that is legitimately absent (another platform, an unreadable attestation) + // is skipped inside readImageContents, so an error here means the image could not be + // measured at all. Skipping it would silently understate TotalCount and TotalSize. + contents, err := readImageContents(ctx, contentStore, provider, img) + if err != nil { + return du, fmt.Errorf("failed to compute the disk usage of image %q: %w", img.Name, err) + } + collected = append(collected, contents) + index.add(contents, func(chainID digest.Digest) int64 { + return snapshotUsage(ctx, snapshotter, chainID) + }) + } + + du.TotalCount = int64(len(collected)) + du.TotalSize = index.total() + + for _, contents := range collected { + size, sharedSize := index.sizes(contents) + + inUse := containers[contents.image.Target.Digest] + if inUse > 0 { + du.ActiveCount++ + } else { + du.Reclaimable += size - sharedSize + } + + if verbose { + repository, tag := imgutil.ParseRepoTag(contents.image.Name) + du.Items = append(du.Items, types.ImageDiskUsageItem{ + ID: contents.image.Target.Digest.String(), + Repository: repository, + Tag: tag, + CreatedAt: contents.createdAt(), + Size: size, + SharedSize: sharedSize, + Containers: inUse, + }) + } + } + + return du, nil +} + +// imageContents is what a single image occupies on disk: the chain IDs of its unpacked snapshots +// (across every platform) and the blobs of its content that are locally present. +type imageContents struct { + image images.Image + chainIDs []digest.Digest + // blobs is the content of the manifests of the image. Docker sizes an image by walking its + // manifests, so the index listing them is not part of what a single image is charged for. + blobs map[digest.Digest]int64 + // indexBlobs is the content of those indexes. It is on disk, so the total counts it, but no + // image is charged for it. + indexBlobs map[digest.Digest]int64 + // created is when the image was built, as stated by its config. It is nil when no config says. + created *time.Time +} + +// createdAt is when the image was built. Docker reports the "created" of the image config; the +// creation time of the local image record only says when it was pulled or tagged, which would show +// an old image as brand new. It is the fallback for the images that do not state one. +func (contents *imageContents) createdAt() time.Time { + if contents.created != nil { + return *contents.created + } + return contents.image.CreatedAt +} + +// diskUsageIndex records, for every snapshot and every blob, how many images hold it and how large +// it is. That is what makes the deduplicated total and the per-image shared size computable without +// a second pass over the content store. +type diskUsageIndex struct { + layerCount map[digest.Digest]int + blobCount map[digest.Digest]int + layerSize map[digest.Digest]int64 + blobSize map[digest.Digest]int64 + // indexSize is the content no single image is charged for. It is deduplicated by digest like + // the rest, it just never contributes to a per-image size, so it needs no count. + indexSize map[digest.Digest]int64 +} + +func newDiskUsageIndex() *diskUsageIndex { + return &diskUsageIndex{ + layerCount: map[digest.Digest]int{}, + blobCount: map[digest.Digest]int{}, + layerSize: map[digest.Digest]int64{}, + blobSize: map[digest.Digest]int64{}, + indexSize: map[digest.Digest]int64{}, + } +} + +// add accounts for one image. usage is only called the first time a snapshot is seen, so a snapshot +// shared by many images is measured once. +func (index *diskUsageIndex) add(contents *imageContents, usage func(digest.Digest) int64) { + for _, chainID := range contents.chainIDs { + index.layerCount[chainID]++ + if _, ok := index.layerSize[chainID]; !ok { + index.layerSize[chainID] = usage(chainID) + } + } + for dgst, size := range contents.blobs { + index.blobCount[dgst]++ + index.blobSize[dgst] = size + } + maps.Copy(index.indexSize, contents.indexBlobs) +} + +// total is the disk space the images take together, counting everything they share only once. +func (index *diskUsageIndex) total() int64 { + var total int64 + for chainID := range index.layerCount { + total += index.layerSize[chainID] + } + for dgst := range index.blobCount { + total += index.blobSize[dgst] + } + for _, size := range index.indexSize { + total += size + } + return total +} + +// sizes returns what one image occupies, and how much of that is also held by another image. +func (index *diskUsageIndex) sizes(contents *imageContents) (size, sharedSize int64) { + for _, chainID := range contents.chainIDs { + size += index.layerSize[chainID] + if index.layerCount[chainID] > 1 { + sharedSize += index.layerSize[chainID] + } + } + for dgst, blob := range contents.blobs { + size += blob + if index.blobCount[dgst] > 1 { + sharedSize += blob + } + } + return size, sharedSize +} + +// readImageContents walks everything reachable from the image target that is present in the content +// store, collecting the blobs on the way and deriving the chain IDs from the image configs. +func readImageContents(ctx context.Context, store content.Store, provider content.Provider, img images.Image) (*imageContents, error) { + contents := &imageContents{ + image: img, + blobs: map[digest.Digest]int64{}, + indexBlobs: map[digest.Digest]int64{}, + } + + var manifestDescs []ocispec.Descriptor + if err := containerdutil.WalkPresentChildren(ctx, store, img.Target, func(_ context.Context, desc ocispec.Descriptor) error { + if images.IsIndexType(desc.MediaType) { + contents.indexBlobs[desc.Digest] = desc.Size + return nil + } + contents.blobs[desc.Digest] = desc.Size + if images.IsManifestType(desc.MediaType) { + manifestDescs = append(manifestDescs, desc) + } + return nil + }); err != nil { + return nil, err + } + + seen := map[digest.Digest]struct{}{} + var built []buildTime + for _, desc := range manifestDescs { + config, err := readConfig(ctx, provider, desc) + if err != nil { + // Attestation manifests and manifests whose config we cannot read carry no rootfs. + // Their content is still accounted for above, they just contribute no snapshot. + log.G(ctx).WithError(err).Debugf("no rootfs for manifest %q of image %q", desc.Digest, img.Name) + continue + } + if !isAttestationManifestDescriptor(desc) { + built = append(built, buildTime{ + platform: manifestPlatform(desc, config), + created: config.Created, + }) + } + for _, chainID := range identity.ChainIDs(config.RootFS.DiffIDs) { + if _, ok := seen[chainID]; ok { + continue + } + seen[chainID] = struct{}{} + contents.chainIDs = append(contents.chainIDs, chainID) + } + } + contents.created = hostBuildTime(built) + + return contents, nil +} + +// buildTime is when one platform of an image was built. created is nil when the config of that +// platform states no build time, which it is free not to. +type buildTime struct { + platform ocispec.Platform + created *time.Time +} + +// hostBuildTime picks the build time to report for a multi-platform image. The platforms of an +// index are not necessarily built together, so report the one this host would run, as Docker does +// by reading the config of the manifest its platform matcher selects. The platform decides which +// config answers, so a host manifest saying nothing is an answer too: it leaves the caller with the +// creation time of the local record rather than with the build time of another architecture. +func hostBuildTime(built []buildTime) *time.Time { + matcher := platforms.Default() + best := -1 + for i, candidate := range built { + if !matcher.Match(candidate.platform) { + continue + } + if best == -1 || matcher.Less(candidate.platform, built[best].platform) { + best = i + } + } + if best >= 0 { + return built[best].created + } + + // No platform of the image runs here (an image pulled for another architecture, say). Any + // build time describes the image better than none. + for _, candidate := range built { + if candidate.created != nil { + return candidate.created + } + } + return nil +} + +// manifestPlatform reports the platform of a manifest. The descriptor is authoritative: it is what +// an index selects a platform by, and it can be more specific than the config, which may declare a +// bare "linux/arm" for what the index calls linux/arm/v6 and linux/arm/v7. +func manifestPlatform(desc ocispec.Descriptor, config *ocispec.Image) ocispec.Platform { + if desc.Platform != nil { + return platforms.Normalize(*desc.Platform) + } + return platforms.Normalize(ocispec.Platform{ + OS: config.OS, + Architecture: config.Architecture, + Variant: config.Variant, + }) +} + +// readConfig returns the image config referenced by the given manifest. +func readConfig(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) (*ocispec.Image, error) { + manifestData, err := containerdutil.ReadBlob(ctx, provider, desc) + if err != nil { + return nil, err + } + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestData, &manifest); err != nil { + return nil, err + } + + configData, err := containerdutil.ReadBlob(ctx, provider, manifest.Config) + if err != nil { + return nil, err + } + var config ocispec.Image + if err := json.Unmarshal(configData, &config); err != nil { + return nil, err + } + + return &config, nil +} + +// snapshotUsage returns the size of a single snapshot, or 0 when the image is not unpacked. +func snapshotUsage(ctx context.Context, snapshotter snapshots.Snapshotter, chainID digest.Digest) int64 { + usage, err := snapshotter.Usage(ctx, chainID.String()) + if err != nil { + if !errdefs.IsNotFound(err) { + log.G(ctx).WithError(err).Debugf("failed to get the usage of snapshot %q", chainID) + } + return 0 + } + return usage.Size +} + +// uniqueByTarget collapses the names pointing at the same target into a single image, preferring a +// tagged name so that the verbose output shows something more useful than " ". +func uniqueByTarget(imageList []images.Image) []images.Image { + var ( + unique = make([]images.Image, 0, len(imageList)) + index = map[digest.Digest]int{} + ) + for _, img := range imageList { + i, ok := index[img.Target.Digest] + if !ok { + index[img.Target.Digest] = len(unique) + unique = append(unique, img) + continue + } + if _, tag := imgutil.ParseRepoTag(unique[i].Name); tag == "" { + if _, tag := imgutil.ParseRepoTag(img.Name); tag != "" { + unique[i] = img + } + } + } + return unique +} diff --git a/pkg/cmd/image/df_test.go b/pkg/cmd/image/df_test.go new file mode 100644 index 00000000000..72d66714313 --- /dev/null +++ b/pkg/cmd/image/df_test.go @@ -0,0 +1,316 @@ +/* + 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 ( + "slices" + "testing" + "time" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "gotest.tools/v3/assert" + + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/platforms" +) + +func testDigest(name string) digest.Digest { + return digest.FromString(name) +} + +// newTestContents builds an image whose snapshots are named by chainIDs and whose blobs are the +// given name/size pairs. +func newTestContents(name string, chainIDs []string, blobs map[string]int64) *imageContents { + contents := &imageContents{ + image: images.Image{ + Name: name, + Target: ocispec.Descriptor{Digest: testDigest(name)}, + }, + blobs: map[digest.Digest]int64{}, + } + for _, chainID := range chainIDs { + contents.chainIDs = append(contents.chainIDs, testDigest(chainID)) + } + for blob, size := range blobs { + contents.blobs[testDigest(blob)] = size + } + return contents +} + +// snapshotSizes turns a name-keyed table into the usage callback diskUsageIndex.add expects. +func snapshotSizes(sizes map[string]int64) func(digest.Digest) int64 { + byDigest := map[digest.Digest]int64{} + for name, size := range sizes { + byDigest[testDigest(name)] = size + } + return func(chainID digest.Digest) int64 { + return byDigest[chainID] + } +} + +func TestDiskUsageIndexSingleImage(t *testing.T) { + t.Parallel() + + usage := snapshotSizes(map[string]int64{"layer-a": 100, "layer-b": 200}) + contents := newTestContents("solo", []string{"layer-a", "layer-b"}, map[string]int64{ + "manifest": 5, + "config": 10, + }) + + index := newDiskUsageIndex() + index.add(contents, usage) + + size, sharedSize := index.sizes(contents) + assert.Equal(t, size, int64(315)) + // Nothing is shared when there is only one image. + assert.Equal(t, sharedSize, int64(0)) + assert.Equal(t, index.total(), int64(315)) +} + +func TestDiskUsageIndexChargesNoImageForTheIndex(t *testing.T) { + t.Parallel() + + usage := snapshotSizes(map[string]int64{"layer-a": 100}) + contents := newTestContents("multi", []string{"layer-a"}, map[string]int64{ + "manifest": 5, + "config": 10, + }) + contents.indexBlobs = map[digest.Digest]int64{testDigest("index"): 2} + + index := newDiskUsageIndex() + index.add(contents, usage) + + // The index listing the manifests is on disk, so the total counts it, but Docker sizes an + // image by walking its manifests and never charges it for the index that lists them. + size, sharedSize := index.sizes(contents) + assert.Equal(t, size, int64(115)) + assert.Equal(t, sharedSize, int64(0)) + assert.Equal(t, index.total(), int64(117)) +} + +func TestDiskUsageIndexSharedLayers(t *testing.T) { + t.Parallel() + + usage := snapshotSizes(map[string]int64{"base": 1000, "top-a": 30, "top-b": 40}) + // Two images built on the same base layer, sharing the base blob too. + first := newTestContents("first", []string{"base", "top-a"}, map[string]int64{ + "base-blob": 500, + "config-a": 7, + "manifest-a": 3, + }) + second := newTestContents("second", []string{"base", "top-b"}, map[string]int64{ + "base-blob": 500, + "config-b": 9, + "manifest-b": 4, + }) + + index := newDiskUsageIndex() + index.add(first, usage) + index.add(second, usage) + + firstSize, firstShared := index.sizes(first) + assert.Equal(t, firstSize, int64(1000+30+500+7+3)) + assert.Equal(t, firstShared, int64(1000+500)) + + secondSize, secondShared := index.sizes(second) + assert.Equal(t, secondSize, int64(1000+40+500+9+4)) + assert.Equal(t, secondShared, int64(1000+500)) + + // The shared base layer and the shared blob are counted once in the total. + assert.Equal(t, index.total(), int64(1000+30+40+500+7+3+9+4)) + // The total is what is really on disk, so it is less than the sum of the image sizes. + assert.Assert(t, index.total() < firstSize+secondSize) + + // Only the unique part of an unused image can be reclaimed. + assert.Equal(t, firstSize-firstShared, int64(30+7+3)) +} + +func TestDiskUsageIndexNotUnpacked(t *testing.T) { + t.Parallel() + + // An image that was pulled but never unpacked has no snapshots, so only its content counts. + usage := snapshotSizes(nil) + contents := newTestContents("packed", []string{"layer-a"}, map[string]int64{"config": 12}) + + index := newDiskUsageIndex() + index.add(contents, usage) + + size, sharedSize := index.sizes(contents) + assert.Equal(t, size, int64(12)) + assert.Equal(t, sharedSize, int64(0)) + assert.Equal(t, index.total(), int64(12)) +} + +func TestDiskUsageIndexMeasuresSnapshotsOnce(t *testing.T) { + t.Parallel() + + // A snapshot shared by several images must not be measured again for each of them: on a real + // snapshotter that lookup is a disk walk. + var calls int + usage := func(digest.Digest) int64 { + calls++ + return 100 + } + + index := newDiskUsageIndex() + index.add(newTestContents("first", []string{"base"}, nil), usage) + index.add(newTestContents("second", []string{"base"}, nil), usage) + + assert.Equal(t, calls, 1) + assert.Equal(t, index.total(), int64(100)) +} + +func TestImageContentsCreatedAt(t *testing.T) { + t.Parallel() + + built := time.Date(2020, 3, 1, 12, 0, 0, 0, time.UTC) + pulled := time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC) + + contents := newTestContents("dated", nil, nil) + contents.image.CreatedAt = pulled + + // Without a config saying otherwise, all we know is when the record appeared locally. + assert.Equal(t, contents.createdAt(), pulled) + + // The config is authoritative: pulling an old image must not make it look brand new. + contents.created = &built + assert.Equal(t, contents.createdAt(), built) +} + +// foreignPlatform returns a platform this host does not run. nerdctl is released for linux/s390x +// among others, so no architecture can be hardcoded as the foreign one: a host matches at most one +// of the two candidates below, and a host running neither Linux nor a Linux runtime matches none. +func foreignPlatform(t *testing.T) ocispec.Platform { + t.Helper() + + matcher := platforms.Default() + for _, candidate := range []ocispec.Platform{ + {OS: "linux", Architecture: "amd64"}, + {OS: "linux", Architecture: "arm64"}, + } { + if !matcher.Match(candidate) { + return candidate + } + } + t.Fatalf("no foreign platform for %q", platforms.Format(platforms.DefaultSpec())) + return ocispec.Platform{} +} + +func TestHostBuildTime(t *testing.T) { + t.Parallel() + + var ( + host = platforms.DefaultSpec() + foreign = foreignPlatform(t) + older = time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC) + newer = time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC) + ) + + t.Run("no manifest at all", func(t *testing.T) { + t.Parallel() + assert.Assert(t, hostBuildTime(nil) == nil) + }) + + t.Run("the platform of the host wins", func(t *testing.T) { + t.Parallel() + // The platforms of an index are not necessarily built together, and the order of the + // descriptors says nothing, so the host platform must be picked whatever its position. + built := []buildTime{ + {platform: foreign, created: &older}, + {platform: host, created: &newer}, + } + assert.Equal(t, *hostBuildTime(built), newer) + + slices.Reverse(built) + assert.Equal(t, *hostBuildTime(built), newer) + }) + + t.Run("the platform of the host states no build time", func(t *testing.T) { + t.Parallel() + // The build time is optional. Once the host platform has answered, the answer stands: + // reporting the build time of another architecture would be worse than reporting none. + built := []buildTime{ + {platform: foreign, created: &older}, + {platform: host}, + } + assert.Assert(t, hostBuildTime(built) == nil) + }) + + t.Run("no platform runs here", func(t *testing.T) { + t.Parallel() + // An image pulled for another architecture still describes itself better with a build time + // than with none, wherever among its platforms that time is stated. + built := []buildTime{ + {platform: foreign}, + {platform: foreign, created: &older}, + } + assert.Equal(t, *hostBuildTime(built), older) + + assert.Assert(t, hostBuildTime([]buildTime{{platform: foreign}}) == nil) + }) +} + +func TestManifestPlatform(t *testing.T) { + t.Parallel() + + // alpine ships linux/arm/v6 and linux/arm/v7 manifests whose configs both declare a bare + // "linux/arm", so the descriptor of the index is the one to believe. + desc := ocispec.Descriptor{Platform: &ocispec.Platform{OS: "linux", Architecture: "arm", Variant: "v6"}} + config := &ocispec.Image{Platform: ocispec.Platform{OS: "linux", Architecture: "arm"}} + assert.Equal(t, platforms.Format(manifestPlatform(desc, config)), "linux/arm/v6") + + // A single-platform image has no index to declare a platform, so the config answers. + assert.Equal(t, platforms.Format(manifestPlatform(ocispec.Descriptor{}, &ocispec.Image{ + Platform: ocispec.Platform{OS: "linux", Architecture: "amd64"}, + })), "linux/amd64") +} + +func TestUniqueByTarget(t *testing.T) { + t.Parallel() + + shared := ocispec.Descriptor{Digest: testDigest("shared")} + other := ocispec.Descriptor{Digest: testDigest("other")} + + imageList := []images.Image{ + // The same target under a digest reference, a tag, and a bare config digest, as the k8s.io + // namespace ends up storing it. + {Name: "example.com/foo@" + shared.Digest.String(), Target: shared}, + {Name: "example.com/foo:latest", Target: shared}, + {Name: shared.Digest.String(), Target: shared}, + {Name: "example.com/bar:v1", Target: other}, + } + + unique := uniqueByTarget(imageList) + assert.Equal(t, len(unique), 2) + // A tagged name is preferred, so the verbose output is not needlessly " ". + assert.Equal(t, unique[0].Name, "example.com/foo:latest") + assert.Equal(t, unique[1].Name, "example.com/bar:v1") +} + +func TestUniqueByTargetKeepsUntagged(t *testing.T) { + t.Parallel() + + dangling := ocispec.Descriptor{Digest: testDigest("dangling")} + imageList := []images.Image{ + {Name: "example.com/foo@" + dangling.Digest.String(), Target: dangling}, + } + + unique := uniqueByTarget(imageList) + assert.Equal(t, len(unique), 1) + assert.Equal(t, unique[0].Name, imageList[0].Name) +} diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go index 9c8510ba172..221ade1aa0a 100644 --- a/pkg/cmd/image/list.go +++ b/pkg/cmd/image/list.go @@ -42,6 +42,7 @@ import ( "github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/platforms" @@ -252,10 +253,15 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima // In-use detection requires a container scan, so only pay for it in the new view where the // EXTRA column is rendered. - var inUse map[digest.Digest]bool + var inUse map[digest.Digest]int64 var inUseByPlatform map[platformRef]bool if newView { - inUse, inUseByPlatform = imagesInUse(ctx, client) + var err error + if inUse, inUseByPlatform, err = imagesInUse(ctx, client); err != nil { + // The indicator decorates the listing, so failing to compute it must not fail the + // listing itself. Unlike the disk usage accounting, nothing here is unsafe to act on. + log.G(ctx).WithError(err).Warn("the in-use indicator is unavailable") + } sortByImageRef(finalImageList) } @@ -289,8 +295,8 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima type imagePrinter struct { w io.Writer quiet, noTrunc, digestsFlag, namesFlag, newView, tree bool - inUse map[digest.Digest]bool // image target -> referenced by at least one container - inUseByPlatform map[platformRef]bool // image target + platform -> run by at least one container + inUse map[digest.Digest]int64 // image target -> number of containers referencing it + inUseByPlatform map[platformRef]bool // image target + platform -> run by at least one container tmpl *template.Template client *containerd.Client provider content.Provider @@ -537,7 +543,7 @@ func (x *imagePrinter) printImageCollapsed(img images.Image, candidateImages map contentSize := units.HumanSizeWithPrecision(float64(totalContentSize), 3) extra := "" - if x.inUse[img.Target.Digest] { + if x.inUse[img.Target.Digest] > 0 { extra = "U" } @@ -700,28 +706,36 @@ type platformRef struct { platform string } -// imagesInUse returns the image target digests that are referenced by at least one container (in -// any state), used to render the Docker v29 "In Use" (U) indicator. Docker matches containers to -// images by digest, so every name pointing at the same target is flagged, not just the one the -// container was created from. +// imagesInUse returns, per image target digest, the number of containers (in any state) referencing +// it. It is used to render the Docker v29 "In Use" (U) indicator and the CONTAINERS column of +// `nerdctl system df --verbose`. Docker matches containers to images by digest, so every name +// pointing at the same target is counted, not just the one the container was created from. // // The second return value narrows this down to the platform each container actually runs, for the // per-platform rows of the tree view. Both are collected in a single container scan. -func imagesInUse(ctx context.Context, client *containerd.Client) (map[digest.Digest]bool, map[platformRef]bool) { - inUse := map[digest.Digest]bool{} +// +// A failure to scan the containers is returned rather than absorbed here, because the callers do +// not agree on what it means: the listing merely loses a column, while the disk usage accounting +// would report every image as inactive and all of its bytes as reclaimable. +func imagesInUse(ctx context.Context, client *containerd.Client) (map[digest.Digest]int64, map[platformRef]bool, error) { + inUse := map[digest.Digest]int64{} inUseByPlatform := map[platformRef]bool{} containerList, err := client.Containers(ctx) if err != nil { - log.G(ctx).WithError(err).Warn("failed to list containers for image in-use detection") - return inUse, inUseByPlatform + return nil, nil, fmt.Errorf("failed to list containers for image in-use detection: %w", err) } for _, container := range containerList { - if dgst, ok := containerImageDigest(ctx, container); ok { - inUse[dgst] = true - inUseByPlatform[platformRef{dgst, containerPlatform(ctx, container)}] = true + dgst, ok, err := containerImageDigest(ctx, container) + if err != nil { + return nil, nil, fmt.Errorf("failed to resolve the image of container %q: %w", container.ID(), err) + } + if !ok { + continue } + inUse[dgst]++ + inUseByPlatform[platformRef{dgst, containerPlatform(ctx, container)}] = true } - return inUse, inUseByPlatform + return inUse, inUseByPlatform, nil } // containerPlatform reports the platform a container runs. Containers created outside nerdctl @@ -760,19 +774,32 @@ func normalizePlatform(platform string) string { // instead would follow the tag wherever it points now: after `nerdctl tag` moves a tag onto another // image, the container would be attributed to an image it never ran. Containers created before this // label existed, or outside nerdctl, still have to be resolved by name. -func containerImageDigest(ctx context.Context, container containerd.Container) (digest.Digest, bool) { +// +// Resolving to no image is reported as such, without an error: a container removed while we list +// it, one created from no image at all, and one whose image is gone all point at an image the +// report does not cover anyway. Any other failure is returned, because a container silently dropped +// here is an image wrongly reported as unused, and its bytes as reclaimable. +func containerImageDigest(ctx context.Context, container containerd.Container) (digest.Digest, bool, error) { // The already-loaded metadata carries the labels, so this costs no extra round trip. - if info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata); err == nil { - if dgst, ok := pinnedImageDigest(info.Labels); ok { - return dgst, true + info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) + if err != nil { + if errdefs.IsNotFound(err) { + return "", false, nil } + return "", false, err + } + if dgst, ok := pinnedImageDigest(info.Labels); ok { + return dgst, true, nil } image, err := container.Image(ctx) if err != nil { - return "", false + if errdefs.IsNotFound(err) { + return "", false, nil + } + return "", false, err } - return image.Target().Digest, true + return image.Target().Digest, true, nil } // pinnedImageDigest returns the image target digest a container pinned at creation time. An diff --git a/pkg/cmd/image/list_test.go b/pkg/cmd/image/list_test.go index 45e48b38199..e9ec6180326 100644 --- a/pkg/cmd/image/list_test.go +++ b/pkg/cmd/image/list_test.go @@ -266,7 +266,7 @@ func TestPrintImageTree(t *testing.T) { w: &buf, newView: true, tree: true, - inUse: map[digest.Digest]bool{targetDigest: true}, + inUse: map[digest.Digest]int64{targetDigest: 1}, // Only the amd64 manifest is actually run by a container. inUseByPlatform: map[platformRef]bool{{targetDigest, "linux/amd64"}: true}, } @@ -297,7 +297,7 @@ func TestPrintImageTree(t *testing.T) { w: &buf, newView: true, tree: true, - inUse: map[digest.Digest]bool{targetDigest: true}, + inUse: map[digest.Digest]int64{targetDigest: 1}, // A container runs the older build only. inUseByPlatform: map[platformRef]bool{{targetDigest, "windows(10.0.20348.2582)/amd64"}: true}, } @@ -336,7 +336,7 @@ func TestPrintImageTree(t *testing.T) { w: &buf, newView: true, tree: true, - inUse: map[digest.Digest]bool{}, + inUse: map[digest.Digest]int64{}, inUseByPlatform: map[platformRef]bool{}, } candidates := map[string]*image{ @@ -364,7 +364,7 @@ func TestPrintImageTree(t *testing.T) { w: &buf, newView: true, tree: true, - inUse: map[digest.Digest]bool{}, + inUse: map[digest.Digest]int64{}, inUseByPlatform: map[platformRef]bool{}, } candidates := map[string]*image{ diff --git a/pkg/cmd/system/df.go b/pkg/cmd/system/df.go new file mode 100644 index 00000000000..35c302b7bb8 --- /dev/null +++ b/pkg/cmd/system/df.go @@ -0,0 +1,413 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package system + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "strconv" + "strings" + "text/tabwriter" + "text/template" + + "github.com/docker/go-units" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/cmd/builder" + "github.com/containerd/nerdctl/v2/pkg/cmd/container" + "github.com/containerd/nerdctl/v2/pkg/cmd/image" + "github.com/containerd/nerdctl/v2/pkg/cmd/volume" + "github.com/containerd/nerdctl/v2/pkg/formatter" + "github.com/containerd/nerdctl/v2/pkg/idgen" +) + +// Df shows how much disk space containerd uses for the images, containers and volumes of the current +// namespace, plus the BuildKit build cache. +func Df(ctx context.Context, client *containerd.Client, options types.SystemDfOptions) error { + du, err := DiskUsage(ctx, client, options) + if err != nil { + return err + } + return printDiskUsage(du, options) +} + +// DiskUsage collects the disk usage of every kind of resource nerdctl manages. +func DiskUsage(ctx context.Context, client *containerd.Client, options types.SystemDfOptions) (types.DiskUsage, error) { + du := types.DiskUsage{} + + var err error + if du.Images, err = image.DiskUsage(ctx, client, options.GOptions, options.Verbose); err != nil { + return du, err + } + if du.Containers, err = container.DiskUsage(ctx, client, options.GOptions, options.Verbose); err != nil { + return du, err + } + if du.Volumes, err = volume.DiskUsage(ctx, client, options.GOptions, options.Verbose); err != nil { + return du, err + } + + // BuildKit is optional. When it is not reachable, the build cache is reported as empty rather + // than omitted, so that the shape of the output does not depend on the daemons that happen to + // be running. + if options.BuildKitHost != "" { + du.BuildCache, err = builder.DiskUsage(ctx, types.BuilderDiskUsageOptions{ + Stderr: options.Stderr, + GOptions: options.GOptions, + BuildKitHost: options.BuildKitHost, + Verbose: options.Verbose, + }) + if err != nil { + log.G(ctx).WithError(err).Warn("failed to get the build cache disk usage") + du.BuildCache = types.BuildCacheDiskUsage{} + } + } + + return du, nil +} + +// dfPrintable is a row of the summary table. +type dfPrintable struct { + Type string + TotalCount string + Active string + Size string + Reclaimable string +} + +// dfVerbosePrintable is what a `--format` template gets in verbose mode, mirroring Docker. +type dfVerbosePrintable struct { + Images []dfImagePrintable + Containers []dfContainerPrintable + Volumes []dfVolumePrintable + BuildCache []dfBuildCachePrintable +} + +type dfImagePrintable struct { + Repository string + Tag string + ID string + CreatedSince string + Size string + SharedSize string + UniqueSize string + Containers string +} + +type dfContainerPrintable struct { + ID string + Image string + Command string + LocalVolumes string + Size string + RunningFor string + Status string + Names string +} + +type dfVolumePrintable struct { + Name string + Links string + Size string +} + +type dfBuildCachePrintable struct { + ID string + CacheType string + Size string + CreatedSince string + LastUsedSince string + UsageCount string + InUse string + Shared string +} + +// dfFormat is how the output was asked to be rendered. +type dfFormat struct { + // tmpl is the template of a `--format`, or nil for the default columns. + tmpl *template.Template + // header renders the column labels of tmpl, leaving them intact. + header *template.Template + // table tells whether the output is a table: its columns are aligned under a header, and its + // identifiers are shortened because it is meant to be read rather than parsed. + table bool +} + +func printDiskUsage(du types.DiskUsage, options types.SystemDfOptions) error { + var ( + format dfFormat + err error + ) + switch { + case options.Format == "", options.Format == "table": + // The default columns, rendered below. + format.table = true + case options.Format == "raw": + return errors.New("unsupported format: \"raw\"") + case formatter.IsTableFormat(options.Format): + // `table {{.Type}}\t{{.Size}}` picks the columns but keeps the header and the alignment. + format.table = true + format.tmpl, format.header, err = formatter.ParseTableTemplate(options.Format) + default: + format.tmpl, err = formatter.ParseTemplate(options.Format) + } + if err != nil { + return err + } + + if options.Verbose { + return printVerbose(du, options.Stdout, format) + } + return printSummary(du, options.Stdout, format) +} + +// dfHeader labels the columns of the summary. A table format renders its header by running the very +// same template over it, so that the header always describes the columns that were asked for. +var dfHeader = dfPrintable{ + Type: "TYPE", + TotalCount: "TOTAL", + Active: "ACTIVE", + Size: "SIZE", + Reclaimable: "RECLAIMABLE", +} + +func printSummary(du types.DiskUsage, stdout io.Writer, format dfFormat) error { + rows := []dfPrintable{ + { + Type: "Images", + TotalCount: strconv.FormatInt(du.Images.TotalCount, 10), + Active: strconv.FormatInt(du.Images.ActiveCount, 10), + Size: humanSize(du.Images.TotalSize), + Reclaimable: humanReclaimable(du.Images.Reclaimable, du.Images.TotalSize), + }, + { + Type: "Containers", + TotalCount: strconv.FormatInt(du.Containers.TotalCount, 10), + Active: strconv.FormatInt(du.Containers.ActiveCount, 10), + Size: humanSize(du.Containers.TotalSize), + Reclaimable: humanReclaimable(du.Containers.Reclaimable, du.Containers.TotalSize), + }, + { + Type: "Local Volumes", + TotalCount: strconv.FormatInt(du.Volumes.TotalCount, 10), + Active: strconv.FormatInt(du.Volumes.ActiveCount, 10), + Size: humanSize(du.Volumes.TotalSize), + Reclaimable: humanReclaimable(du.Volumes.Reclaimable, du.Volumes.TotalSize), + }, + { + Type: "Build Cache", + TotalCount: strconv.FormatInt(du.BuildCache.TotalCount, 10), + Active: strconv.FormatInt(du.BuildCache.ActiveCount, 10), + Size: humanSize(du.BuildCache.TotalSize), + // Unlike the other kinds, Docker never shows a percentage for the build cache. + Reclaimable: humanSize(du.BuildCache.Reclaimable), + }, + } + + if format.tmpl != nil && !format.table { + for _, row := range rows { + if err := executeTemplate(stdout, format.tmpl, row); err != nil { + return err + } + } + return nil + } + + w := newTabWriter(stdout) + if format.tmpl != nil { + if err := executeTemplate(w, format.header, dfHeader); err != nil { + return err + } + for _, row := range rows { + if err := executeTemplate(w, format.tmpl, row); err != nil { + return err + } + } + return w.Flush() + } + + fmt.Fprintln(w, "TYPE\tTOTAL\tACTIVE\tSIZE\tRECLAIMABLE") + for _, row := range rows { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", row.Type, row.TotalCount, row.Active, row.Size, row.Reclaimable) + } + return w.Flush() +} + +func printVerbose(du types.DiskUsage, stdout io.Writer, format dfFormat) error { + verbose := dfVerbosePrintable{} + + // Docker only shortens the identifiers for its table output (see Format.IsTable in + // docker/cli), so that a custom format stays usable to look a resource up. A table format is + // still a table, whatever columns it asks for. + trunc := format.table + + for _, item := range du.Images.Items { + repository, tag := item.Repository, item.Tag + if repository == "" { + repository = "" + } + if tag == "" { + tag = "" + } + verbose.Images = append(verbose.Images, dfImagePrintable{ + Repository: repository, + Tag: tag, + ID: displayID(item.ID, trunc), + CreatedSince: formatter.TimeSinceInHuman(item.CreatedAt), + Size: humanSize(item.Size), + SharedSize: humanSize(item.SharedSize), + UniqueSize: humanSize(item.Size - item.SharedSize), + Containers: strconv.FormatInt(item.Containers, 10), + }) + } + + for _, item := range du.Containers.Items { + verbose.Containers = append(verbose.Containers, dfContainerPrintable{ + ID: displayID(item.ID, trunc), + Image: item.Image, + Command: item.Command, + LocalVolumes: strconv.FormatInt(item.LocalVolumes, 10), + Size: humanSize(item.SizeRw), + RunningFor: formatter.TimeSinceInHuman(item.CreatedAt), + Status: item.Status, + Names: item.Names, + }) + } + + for _, item := range du.Volumes.Items { + verbose.Volumes = append(verbose.Volumes, dfVolumePrintable{ + Name: item.Name, + Links: strconv.FormatInt(item.Links, 10), + Size: humanSize(item.Size), + }) + } + + for _, item := range du.BuildCache.Items { + lastUsedSince := "" + if item.LastUsedAt != nil { + lastUsedSince = formatter.TimeSinceInHuman(*item.LastUsedAt) + } + // Docker has no column for it, it marks the ID of a record in use with a star instead. + id := displayID(item.ID, trunc) + if item.InUse { + id += "*" + } + verbose.BuildCache = append(verbose.BuildCache, dfBuildCachePrintable{ + ID: id, + CacheType: item.CacheType, + Size: humanSize(item.Size), + CreatedSince: formatter.TimeSinceInHuman(item.CreatedAt), + LastUsedSince: lastUsedSince, + UsageCount: strconv.Itoa(item.UsageCount), + InUse: strconv.FormatBool(item.InUse), + Shared: strconv.FormatBool(item.Shared), + }) + } + + if format.tmpl != nil { + return executeTemplate(stdout, format.tmpl, verbose) + } + + fmt.Fprint(stdout, "Images space usage:\n\n") + w := newTabWriter(stdout) + fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE\tSHARED SIZE\tUNIQUE SIZE\tCONTAINERS") + for _, p := range verbose.Images { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + p.Repository, p.Tag, p.ID, p.CreatedSince, p.Size, p.SharedSize, p.UniqueSize, p.Containers) + } + if err := w.Flush(); err != nil { + return err + } + + fmt.Fprint(stdout, "\nContainers space usage:\n\n") + w = newTabWriter(stdout) + fmt.Fprintln(w, "CONTAINER ID\tIMAGE\tCOMMAND\tLOCAL VOLUMES\tSIZE\tCREATED\tSTATUS\tNAMES") + for _, p := range verbose.Containers { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + p.ID, p.Image, p.Command, p.LocalVolumes, p.Size, p.RunningFor, p.Status, p.Names) + } + if err := w.Flush(); err != nil { + return err + } + + fmt.Fprint(stdout, "\nLocal Volumes space usage:\n\n") + w = newTabWriter(stdout) + fmt.Fprintln(w, "VOLUME NAME\tLINKS\tSIZE") + for _, p := range verbose.Volumes { + fmt.Fprintf(w, "%s\t%s\t%s\n", p.Name, p.Links, p.Size) + } + if err := w.Flush(); err != nil { + return err + } + + fmt.Fprintf(stdout, "\nBuild cache usage: %s\n\n", humanSize(du.BuildCache.TotalSize)) + w = newTabWriter(stdout) + fmt.Fprintln(w, "CACHE ID\tCACHE TYPE\tSIZE\tCREATED\tLAST USED\tUSAGE\tSHARED") + for _, p := range verbose.BuildCache { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + p.ID, p.CacheType, p.Size, p.CreatedSince, p.LastUsedSince, p.UsageCount, p.Shared) + } + return w.Flush() +} + +func newTabWriter(w io.Writer) *tabwriter.Writer { + return tabwriter.NewWriter(w, 4, 8, 4, ' ', 0) +} + +func executeTemplate(w io.Writer, tmpl *template.Template, data any) error { + var b bytes.Buffer + if err := tmpl.Execute(&b, data); err != nil { + return err + } + _, err := fmt.Fprintln(w, b.String()) + return err +} + +func humanSize(size int64) string { + return units.HumanSize(float64(size)) +} + +// humanReclaimable renders the reclaimable space the way Docker does: as a share of the total, when +// there is a total to compare it against. +func humanReclaimable(reclaimable, totalSize int64) string { + if totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", humanSize(reclaimable), (reclaimable*100)/totalSize) + } + return humanSize(reclaimable) +} + +// displayID shortens an identifier for the table output only. A custom format is meant to be +// consumed by something else, and the full identifier is what makes the resource addressable. +func displayID(id string, trunc bool) string { + if !trunc { + return id + } + return truncateID(id) +} + +// truncateID shortens an identifier for display, dropping the digest algorithm when there is one. +func truncateID(id string) string { + if _, hex, ok := strings.Cut(id, ":"); ok { + id = hex + } + return idgen.TruncateID(id) +} diff --git a/pkg/cmd/system/df_test.go b/pkg/cmd/system/df_test.go new file mode 100644 index 00000000000..0edf263cbf2 --- /dev/null +++ b/pkg/cmd/system/df_test.go @@ -0,0 +1,435 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package system + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +func TestHumanReclaimable(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + reclaimable int64 + totalSize int64 + expected string + }{ + { + name: "share of the total", + reclaimable: 940, + totalSize: 1000, + expected: "940B (94%)", + }, + { + name: "nothing reclaimable", + reclaimable: 0, + totalSize: 1000, + expected: "0B (0%)", + }, + { + name: "no total to compare against", + reclaimable: 0, + totalSize: 0, + expected: "0B", + }, + { + name: "everything reclaimable", + reclaimable: 2000, + totalSize: 2000, + expected: "2kB (100%)", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, humanReclaimable(tc.reclaimable, tc.totalSize), tc.expected) + }) + } +} + +func TestTruncateID(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + id string + expected string + }{ + { + name: "digest", + id: "sha256:09538a1f51d3ec5af0449a1640937dfdf79b0e9b8c4da5b8a883086d5c1492ef", + expected: "09538a1f51d3", + }, + { + name: "opaque buildkit id", + id: "n3vkjqf4tzxkgxwjdgm0e5vpm", + expected: "n3vkjqf4tzxk", + }, + { + name: "shorter than the short id length", + id: "abc", + expected: "abc", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, truncateID(tc.id), tc.expected) + }) + } +} + +// fieldsOfRowContaining returns the single-space-joined fields of the first line of out holding +// needle, so that assertions do not depend on how the tabwriter pads the columns. +func fieldsOfRowContaining(out, needle string) string { + for line := range strings.SplitSeq(out, "\n") { + if strings.Contains(line, needle) { + return strings.Join(strings.Fields(line), " ") + } + } + return "" +} + +func testDiskUsage() types.DiskUsage { + createdAt := time.Now().Add(-time.Hour) + lastUsedAt := time.Now().Add(-time.Minute) + + return types.DiskUsage{ + Images: types.ImageDiskUsage{ + TotalCount: 2, + ActiveCount: 1, + TotalSize: 1000, + Reclaimable: 400, + Items: []types.ImageDiskUsageItem{ + { + ID: "sha256:09538a1f51d3ec5af0449a1640937dfdf79b0e9b8c4da5b8a883086d5c1492ef", + Repository: "example.com/foo", + Tag: "latest", + CreatedAt: createdAt, + Size: 800, + SharedSize: 200, + Containers: 1, + }, + { + ID: "sha256:0168606be2317b0d6a3c0b1a2a5b1a2a5b1a2a5b1a2a5b1a2a5b1a2a5b1a2a5b", + CreatedAt: createdAt, + Size: 600, + // A dangling image with no container: everything unique to it is reclaimable. + SharedSize: 200, + }, + }, + }, + Containers: types.ContainerDiskUsage{ + TotalCount: 1, + ActiveCount: 0, + TotalSize: 100, + Reclaimable: 100, + Items: []types.ContainerDiskUsageItem{ + { + ID: "6d3f1c1c1a5b6d3f1c1c1a5b6d3f1c1c1a5b6d3f1c1c1a5b6d3f1c1c1a5b6d3f", + Image: "example.com/foo:latest", + Command: `"sleep 3600"`, + LocalVolumes: 1, + SizeRw: 100, + CreatedAt: createdAt, + Status: "Exited (0) 1 minute ago", + Names: "sleeper", + }, + }, + }, + Volumes: types.VolumeDiskUsage{ + TotalCount: 2, + ActiveCount: 1, + TotalSize: 300, + Reclaimable: 100, + Items: []types.VolumeDiskUsageItem{ + {Name: "data", Links: 1, Size: 200}, + {Name: "orphan", Links: 0, Size: 100}, + }, + }, + BuildCache: types.BuildCacheDiskUsage{ + TotalCount: 1, + ActiveCount: 0, + TotalSize: 500, + Reclaimable: 500, + Items: []types.BuildCacheDiskUsageItem{ + { + ID: "n3vkjqf4tzxkgxwjdgm0e5vpm", + CacheType: "regular", + Size: 500, + CreatedAt: createdAt, + LastUsedAt: &lastUsedAt, + UsageCount: 3, + }, + }, + }, + } +} + +func TestPrintDiskUsageSummary(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{Stdout: stdout}) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + assert.Equal(t, len(lines), 5) + + assert.Assert(t, strings.HasPrefix(lines[0], "TYPE"), lines[0]) + assert.Assert(t, strings.Contains(lines[0], "RECLAIMABLE"), lines[0]) + + assert.Equal(t, strings.Join(strings.Fields(lines[1]), " "), "Images 2 1 1kB 400B (40%)") + assert.Equal(t, strings.Join(strings.Fields(lines[2]), " "), "Containers 1 0 100B 100B (100%)") + assert.Equal(t, strings.Join(strings.Fields(lines[3]), " "), "Local Volumes 2 1 300B 100B (33%)") + // The build cache never gets a percentage, matching Docker. + assert.Equal(t, strings.Join(strings.Fields(lines[4]), " "), "Build Cache 1 0 500B 500B") +} + +func TestPrintDiskUsageEmpty(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(types.DiskUsage{}, types.SystemDfOptions{Stdout: stdout}) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + assert.Equal(t, len(lines), 5) + for _, line := range lines[1:] { + // Without a total there is nothing to take a percentage of. + assert.Assert(t, strings.HasSuffix(line, "0B"), line) + } +} + +func TestPrintDiskUsageFormat(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{ + Stdout: stdout, + Format: "{{.Type}}={{.Size}}", + }) + assert.NilError(t, err) + + assert.Equal(t, stdout.String(), strings.Join([]string{ + "Images=1kB", + "Containers=100B", + "Local Volumes=300B", + "Build Cache=500B", + "", + }, "\n")) +} + +func TestPrintDiskUsageTableFormat(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{ + Stdout: stdout, + // The \t is what a shell passes through literally, so it has to be expanded here. + Format: `table {{.Type}}\t{{.Size}}`, + }) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + assert.Equal(t, len(lines), 5) + // The header is the same template over the column labels, so it names the chosen columns only. + assert.Equal(t, strings.Join(strings.Fields(lines[0]), " "), "TYPE SIZE") + assert.Equal(t, strings.Join(strings.Fields(lines[1]), " "), "Images 1kB") + assert.Equal(t, strings.Join(strings.Fields(lines[4]), " "), "Build Cache 500B") + // The columns are aligned, unlike a bare template. + assert.Assert(t, strings.Contains(lines[1], " "), lines[1]) +} + +func TestPrintDiskUsageTableFormatHeader(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{ + Stdout: stdout, + Format: `table {{lower .Type}}\t{{truncate .Size 2}}`, + }) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + // The header names the columns whatever the template does to the values under them, so the + // functions that transform a value are not applied to the labels. + assert.Equal(t, strings.Join(strings.Fields(lines[0]), " "), "TYPE SIZE") + assert.Equal(t, strings.Join(strings.Fields(lines[1]), " "), "images 1k") +} + +func TestPrintDiskUsageBareTableFormat(t *testing.T) { + t.Parallel() + + // A bare "table" keeps the default columns. + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{Stdout: stdout, Format: "table"}) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + assert.Equal(t, strings.Join(strings.Fields(lines[0]), " "), "TYPE TOTAL ACTIVE SIZE RECLAIMABLE") + assert.Equal(t, len(lines), 5) +} + +func TestPrintDiskUsageFormatJSON(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{Stdout: stdout, Format: "json"}) + assert.NilError(t, err) + + lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") + assert.Equal(t, len(lines), 4) + for _, line := range lines { + var row dfPrintable + assert.NilError(t, json.Unmarshal([]byte(line), &row)) + assert.Assert(t, row.Type != "", line) + } +} + +func TestPrintDiskUsageRawIsUnsupported(t *testing.T) { + t.Parallel() + + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{Stdout: &bytes.Buffer{}, Format: "raw"}) + assert.ErrorContains(t, err, "raw") +} + +func TestPrintDiskUsageVerbose(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{Stdout: stdout, Verbose: true}) + assert.NilError(t, err) + + out := stdout.String() + for _, section := range []string{ + "Images space usage:", + "Containers space usage:", + "Local Volumes space usage:", + "Build cache usage: 500B", + } { + assert.Assert(t, strings.Contains(out, section), out) + } + + // UNIQUE SIZE is what is left once the shared part is taken out of the size. + assert.Equal(t, fieldsOfRowContaining(out, "example.com/foo"), + "example.com/foo latest 09538a1f51d3 About an hour ago 800B 200B 600B 1") + // An image with neither repository nor tag is shown the Docker way. + assert.Equal(t, fieldsOfRowContaining(out, "0168606be231"), + " 0168606be231 About an hour ago 600B 200B 400B 0") + // The build cache record keeps its opaque identifier, only truncated. + assert.Equal(t, fieldsOfRowContaining(out, "n3vkjqf4tzxk"), + "n3vkjqf4tzxk regular 500B About an hour ago About a minute ago 3 false") +} + +func TestPrintDiskUsageVerboseMarksBuildCacheInUse(t *testing.T) { + t.Parallel() + + du := testDiskUsage() + du.BuildCache.Items[0].InUse = true + + stdout := &bytes.Buffer{} + err := printDiskUsage(du, types.SystemDfOptions{Stdout: stdout, Verbose: true}) + assert.NilError(t, err) + + // Docker gives it no column of its own: a record in use is the one whose ID carries a star. + assert.Equal(t, fieldsOfRowContaining(stdout.String(), "n3vkjqf4tzxk"), + "n3vkjqf4tzxk* regular 500B About an hour ago About a minute ago 3 false") + + // A custom format can still ask for it by name. + formatted := &bytes.Buffer{} + err = printDiskUsage(du, types.SystemDfOptions{ + Stdout: formatted, + Verbose: true, + Format: `{{range .BuildCache}}{{.InUse}}{{end}}`, + }) + assert.NilError(t, err) + assert.Equal(t, strings.TrimSpace(formatted.String()), "true") +} + +func TestPrintDiskUsageVerboseKeepsFullIDsForFormat(t *testing.T) { + t.Parallel() + + du := testDiskUsage() + + table := &bytes.Buffer{} + err := printDiskUsage(du, types.SystemDfOptions{Stdout: table, Verbose: true}) + assert.NilError(t, err) + // The table is for reading, so the identifiers are shortened. + assert.Assert(t, strings.Contains(table.String(), "09538a1f51d3"), table.String()) + assert.Assert(t, !strings.Contains(table.String(), du.Images.Items[0].ID), table.String()) + + formatted := &bytes.Buffer{} + err = printDiskUsage(du, types.SystemDfOptions{Stdout: formatted, Verbose: true, Format: "json"}) + assert.NilError(t, err) + + // A custom format is for machines, so the identifiers stay addressable. + var verbose dfVerbosePrintable + assert.NilError(t, json.Unmarshal([]byte(strings.TrimSpace(formatted.String())), &verbose)) + assert.Equal(t, verbose.Images[0].ID, du.Images.Items[0].ID) + assert.Equal(t, verbose.Containers[0].ID, du.Containers.Items[0].ID) + assert.Equal(t, verbose.BuildCache[0].ID, du.BuildCache.Items[0].ID) +} + +func TestPrintDiskUsageVerboseTableFormatShortensIDs(t *testing.T) { + t.Parallel() + + du := testDiskUsage() + stdout := &bytes.Buffer{} + err := printDiskUsage(du, types.SystemDfOptions{ + Stdout: stdout, + Verbose: true, + Format: `table {{range .Images}}{{.ID}}{{end}}`, + }) + assert.NilError(t, err) + + // A table format is still a table, whatever columns it asks for, so Docker shortens its + // identifiers just like those of the default one. + assert.Assert(t, strings.Contains(stdout.String(), "09538a1f51d3"), stdout.String()) + assert.Assert(t, !strings.Contains(stdout.String(), du.Images.Items[0].ID), stdout.String()) +} + +func TestPrintDiskUsageVerboseFormat(t *testing.T) { + t.Parallel() + + stdout := &bytes.Buffer{} + err := printDiskUsage(testDiskUsage(), types.SystemDfOptions{ + Stdout: stdout, + Verbose: true, + Format: "json", + }) + assert.NilError(t, err) + + var verbose dfVerbosePrintable + assert.NilError(t, json.Unmarshal([]byte(strings.TrimSpace(stdout.String())), &verbose)) + assert.Equal(t, len(verbose.Images), 2) + assert.Equal(t, len(verbose.Containers), 1) + assert.Equal(t, len(verbose.Volumes), 2) + assert.Equal(t, len(verbose.BuildCache), 1) + assert.Equal(t, verbose.Images[0].UniqueSize, "600B") +} diff --git a/pkg/cmd/volume/df.go b/pkg/cmd/volume/df.go new file mode 100644 index 00000000000..caadf04bc34 --- /dev/null +++ b/pkg/cmd/volume/df.go @@ -0,0 +1,76 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package volume + +import ( + "context" + "slices" + "strings" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +// DiskUsage reports how much disk space the local volumes of the current namespace use. +// +// A volume is active when at least one container mounts it, and the space of the volumes no +// container mounts can be reclaimed. Note that, unlike `nerdctl volume prune`, this counts the named +// volumes too: Docker reports what `docker volume prune --all` would free. +func DiskUsage(ctx context.Context, client *containerd.Client, gOptions types.GlobalCommandOptions, verbose bool) (types.VolumeDiskUsage, error) { + du := types.VolumeDiskUsage{} + + // The size is what we are after here, so it is always requested. + vols, err := Volumes(gOptions.Namespace, gOptions.DataRoot, gOptions.Address, true, nil) + if err != nil { + return du, err + } + + containers, err := client.Containers(ctx) + if err != nil { + return du, err + } + links, err := usedVolumes(ctx, containers) + if err != nil { + return du, err + } + + for _, v := range vols { + du.TotalCount++ + du.TotalSize += v.Size + if links[v.Name] > 0 { + du.ActiveCount++ + } else { + du.Reclaimable += v.Size + } + + if verbose { + du.Items = append(du.Items, types.VolumeDiskUsageItem{ + Name: v.Name, + Links: links[v.Name], + Size: v.Size, + }) + } + } + + // Volumes comes from a map, so give the verbose output a stable order. + slices.SortFunc(du.Items, func(a, b types.VolumeDiskUsageItem) int { + return strings.Compare(a.Name, b.Name) + }) + + return du, nil +} diff --git a/pkg/cmd/volume/df_test.go b/pkg/cmd/volume/df_test.go new file mode 100644 index 00000000000..8bf794e794d --- /dev/null +++ b/pkg/cmd/volume/df_test.go @@ -0,0 +1,77 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package volume + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestMountedVolumes(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mountsJSON string + expected []string + }{ + { + name: "no mounts", + mountsJSON: "", + }, + { + name: "a named volume and a bind", + mountsJSON: `[{"Type":"volume","Name":"data","Destination":"/data"},` + + `{"Type":"bind","Source":"/host","Destination":"/host"}]`, + expected: []string{"data"}, + }, + { + name: "the same volume at two paths counts once", + mountsJSON: `[{"Type":"volume","Name":"data","Destination":"/data"},` + + `{"Type":"volume","Name":"data","Destination":"/backup"}]`, + expected: []string{"data"}, + }, + { + name: "two volumes", + mountsJSON: `[{"Type":"volume","Name":"data","Destination":"/data"},` + + `{"Type":"volume","Name":"logs","Destination":"/logs"}]`, + expected: []string{"data", "logs"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + names, err := mountedVolumes(tc.mountsJSON) + assert.NilError(t, err) + assert.Equal(t, len(names), len(tc.expected)) + for _, name := range tc.expected { + _, ok := names[name] + assert.Assert(t, ok, name) + } + }) + } +} + +func TestMountedVolumesInvalidJSON(t *testing.T) { + t.Parallel() + + _, err := mountedVolumes(`[{"Type":"volume"`) + assert.Assert(t, err != nil) +} diff --git a/pkg/cmd/volume/rm.go b/pkg/cmd/volume/rm.go index 0a41b3f23a5..0930b9ce16d 100644 --- a/pkg/cmd/volume/rm.go +++ b/pkg/cmd/volume/rm.go @@ -79,8 +79,10 @@ func Remove(ctx context.Context, client *containerd.Client, volumes []string, op return nil } -func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]struct{}, error) { - usedVolumesList := make(map[string]struct{}) +// usedVolumes returns, per volume name, how many containers mount it. Callers that only care about +// whether a volume is used at all can test for the presence of the key. +func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]int64, error) { + usedVolumesList := make(map[string]int64) for _, c := range containers { l, err := c.Labels(ctx) if err != nil { @@ -93,21 +95,34 @@ func usedVolumes(ctx context.Context, containers []containerd.Container) (map[st return nil, err } - mountsJSON := labels.GetMount(l) - if mountsJSON == "" { - continue - } - - var mounts []dockercompat.MountPoint - err = json.Unmarshal([]byte(mountsJSON), &mounts) + names, err := mountedVolumes(labels.GetMount(l)) if err != nil { return nil, err } - for _, m := range mounts { - if m.Type == mountutil.Volume { - usedVolumesList[m.Name] = struct{}{} - } + for name := range names { + usedVolumesList[name]++ } } return usedVolumesList, nil } + +// mountedVolumes returns the distinct volume names of a container, from the JSON-marshalled mounts +// it carries in its labels. The names are deduplicated: a container mounting the same volume at +// several paths is still one reference to it, which is how Docker counts the links of a volume. +func mountedVolumes(mountsJSON string) (map[string]struct{}, error) { + names := make(map[string]struct{}) + if mountsJSON == "" { + return names, nil + } + + var mounts []dockercompat.MountPoint + if err := json.Unmarshal([]byte(mountsJSON), &mounts); err != nil { + return nil, err + } + for _, m := range mounts { + if m.Type == mountutil.Volume { + names[m.Name] = struct{}{} + } + } + return names, nil +} diff --git a/pkg/containerdutil/content.go b/pkg/containerdutil/content.go index 929e60951c9..80c364e00b1 100644 --- a/pkg/containerdutil/content.go +++ b/pkg/containerdutil/content.go @@ -27,8 +27,48 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/errdefs" ) +// WalkPresentChildren calls f for target and for every descriptor reachable from it that is present +// in the content store. Descriptors that are only referenced but not stored locally (for instance +// the layers of an image that was pulled for another platform) are skipped, so that sizes computed +// from the visited descriptors reflect what is actually on disk. +func WalkPresentChildren(ctx context.Context, store content.Store, target ocispec.Descriptor, f func(context.Context, ocispec.Descriptor) error) error { + return images.Walk(ctx, presentChildrenHandler(store, func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + return nil, f(ctx, desc) + }), target) +} + +// presentChildrenHandler wraps h so that it is only called for descriptors present in the store, and +// so that the walk descends into the children of those descriptors. +func presentChildrenHandler(store content.Store, h images.HandlerFunc) images.HandlerFunc { + return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if _, err := store.Info(ctx, desc.Digest); err != nil { + if errdefs.IsNotFound(err) { + return nil, images.ErrSkipDesc + } + return nil, err + } + + children, err := h(ctx, desc) + if err != nil { + return nil, err + } + + c, err := images.Children(ctx, store, desc) + if err != nil { + if errdefs.IsNotFound(err) { + return nil, images.ErrSkipDesc + } + return nil, err + } + + return append(children, c...), nil + } +} + // ContentStore should be called to get a Provider with caching func NewProvider(client *containerd.Client) content.Provider { return &providerWithCache{ diff --git a/pkg/formatter/common.go b/pkg/formatter/common.go index e418dd7d8ef..edf432633e6 100644 --- a/pkg/formatter/common.go +++ b/pkg/formatter/common.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "strings" "text/template" "github.com/docker/cli/templates" @@ -32,6 +33,38 @@ type Flusher interface { Flush() error } +// tableFormatKey introduces the Docker table formats, e.g. `table {{.Type}}\t{{.Size}}`, which +// render a header and aligned columns rather than the raw output of the template. +const tableFormatKey = "table" + +// IsTableFormat reports whether format is a Docker table format: either the bare "table", which +// selects the default columns of a command, or "table " followed by a template. +func IsTableFormat(format string) bool { + return format == tableFormatKey || strings.HasPrefix(format, tableFormatKey+" ") +} + +// ParseTableTemplate parses the template carried by a Docker table format. Like docker/cli, it +// expands the literal `\t` and `\n` a shell would otherwise have to produce itself. +// +// It returns a second template for the header row. A header is rendered by running the very same +// template over the column labels, so a function that transforms a value would rewrite the label +// too and `table {{lower .Type}}` would name the column "type" instead of TYPE. The header template +// therefore replaces those functions by ones leaving their argument alone, as docker/cli does. Only +// `pad` is kept as it is, so that the header stays aligned with its column. +func ParseTableTemplate(format string) (rows, header *template.Template, err error) { + format = strings.TrimSpace(strings.TrimPrefix(format, tableFormatKey)) + format = strings.ReplaceAll(format, `\t`, "\t") + format = strings.ReplaceAll(format, `\n`, "\n") + + if rows, err = ParseTemplate(format); err != nil { + return nil, nil, err + } + if header, err = rows.Clone(); err != nil { + return nil, nil, err + } + return rows, header.Funcs(templates.HeaderFunctions), nil +} + // FormatSlice formats the slice with `--format` flag. // // --format="" (default): JSON diff --git a/pkg/formatter/table_test.go b/pkg/formatter/table_test.go new file mode 100644 index 00000000000..94c744a65a5 --- /dev/null +++ b/pkg/formatter/table_test.go @@ -0,0 +1,93 @@ +/* + 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 formatter + +import ( + "bytes" + "testing" + + "gotest.tools/v3/assert" +) + +func TestIsTableFormat(t *testing.T) { + t.Parallel() + + testCases := []struct { + format string + expected bool + }{ + {format: "table", expected: true}, + {format: `table {{.Type}}`, expected: true}, + {format: `table {{.Type}}\t{{.Size}}`, expected: true}, + {format: "", expected: false}, + {format: "json", expected: false}, + {format: `{{.Type}}`, expected: false}, + // A template that merely starts with the word is not a table format. + {format: `{{.Type}} table`, expected: false}, + {format: "tabled", expected: false}, + } + + for _, tc := range testCases { + t.Run(tc.format, func(t *testing.T) { + t.Parallel() + assert.Equal(t, IsTableFormat(tc.format), tc.expected) + }) + } +} + +func TestParseTableTemplate(t *testing.T) { + t.Parallel() + + // The shell passes \t and \n through literally, so they arrive as two characters and have to + // be expanded, the way docker/cli does. + rows, _, err := ParseTableTemplate(`table {{.A}}\t{{.B}}\n`) + assert.NilError(t, err) + + var b bytes.Buffer + err = rows.Execute(&b, struct{ A, B string }{A: "one", B: "two"}) + assert.NilError(t, err) + assert.Equal(t, b.String(), "one\ttwo\n") +} + +func TestParseTableTemplateHeader(t *testing.T) { + t.Parallel() + + // The functions that transform a value must leave the column labels alone, otherwise the + // header of `table {{lower .A}}` would read "a" instead of naming the column. + rows, header, err := ParseTableTemplate(`table {{lower .A}}\t{{truncate .B 2}}\t{{upper .C}}`) + assert.NilError(t, err) + + type row struct{ A, B, C string } + + var headerOut bytes.Buffer + err = header.Execute(&headerOut, row{A: "NAME", B: "SIZE", C: "Status"}) + assert.NilError(t, err) + assert.Equal(t, headerOut.String(), "NAME\tSIZE\tStatus") + + // The rows themselves still go through the functions they asked for. + var rowOut bytes.Buffer + err = rows.Execute(&rowOut, row{A: "Foo", B: "100B", C: "up"}) + assert.NilError(t, err) + assert.Equal(t, rowOut.String(), "foo\t10\tUP") +} + +func TestParseTableTemplateInvalid(t *testing.T) { + t.Parallel() + + _, _, err := ParseTableTemplate(`table {{.Unclosed`) + assert.Assert(t, err != nil) +} From 83f9981016d03f2a2a941466c2bb063d625eb7ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:34:35 +0000 Subject: [PATCH 777/868] build(deps): bump github.com/containerd/go-cni from 1.1.13 to 1.1.14 Bumps [github.com/containerd/go-cni](https://github.com/containerd/go-cni) from 1.1.13 to 1.1.14. - [Release notes](https://github.com/containerd/go-cni/releases) - [Commits](https://github.com/containerd/go-cni/compare/v1.1.13...v1.1.14) --- updated-dependencies: - dependency-name: github.com/containerd/go-cni dependency-version: 1.1.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 16 ++++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index f43f2e02409..8c0c869e847 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( 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 - github.com/containerd/go-cni v1.1.13 //gomodjail:unconfined + github.com/containerd/go-cni v1.1.14 //gomodjail:unconfined github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined github.com/containerd/log v0.1.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 @@ -150,7 +150,6 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/moby/moby/api v1.55.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect go.opentelemetry.io/otel/sdk v1.45.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 367e1f25065..b4ec48f7504 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= -github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis= -github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= +github.com/containerd/go-cni v1.1.14 h1:jcFWauA5ED2wUHgCdvPB/IyvOtBcXzmgj8LprJ8nJH0= +github.com/containerd/go-cni v1.1.14/go.mod h1:igdwKOd5qpuMIafFcovqefXeThU/s2MoDSOnkCv77fw= github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA= github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQujLw7UQ3w= @@ -79,8 +79,6 @@ 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.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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= @@ -225,7 +223,6 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -252,12 +249,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= @@ -398,7 +391,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= From ee6541264a2a99f9d0221d3cb16756d79550cbab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:34:47 +0000 Subject: [PATCH 778/868] build(deps): bump github.com/containernetworking/cni from 1.3.0 to 1.3.1 Bumps [github.com/containernetworking/cni](https://github.com/containernetworking/cni) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/containernetworking/cni/releases) - [Commits](https://github.com/containernetworking/cni/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: github.com/containernetworking/cni dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f43f2e02409..b0644f55afd 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( 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.3.0 - github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined + github.com/containernetworking/cni v1.3.1 //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 diff --git a/go.sum b/go.sum index 367e1f25065..5a5ca34aeea 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/containerd/ttrpc v1.2.9 h1:ha0ak962T0s3CA/RoZ6S6xiWZQF24GrBaEpiGX1uih github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= 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/cni v1.3.1 h1:gnHlU/YC1bJcS+CSh+pYQKeuu4O4gsMGW1NMaLhkzW4= +github.com/containernetworking/cni v1.3.1/go.mod h1:OtVXL0yXMfQb+p93OC/vRuhjpxsO1gAV0smjt28UQUQ= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= github.com/containers/ocicrypt v1.3.2 h1:MuqHSfiPpGzoAQdgDSX85FgixSgIm9mSDXTLTgugY5E= From 7bc523db486b9243575bbac8bc5eb73d26c1f904 Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Fri, 4 Sep 2026 12:59:43 +0800 Subject: [PATCH 779/868] Implement rootless systemd cgroup discovery Signed-off-by: Feng Wang --- .../rootless/containerd-rootless-setuptool.sh | 2 +- pkg/infoutil/infoutil_linux.go | 12 +++-- pkg/infoutil/rootless_cgroup_linux.go | 46 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 pkg/infoutil/rootless_cgroup_linux.go diff --git a/extras/rootless/containerd-rootless-setuptool.sh b/extras/rootless/containerd-rootless-setuptool.sh index d4caaa941eb..a0537e929c6 100755 --- a/extras/rootless/containerd-rootless-setuptool.sh +++ b/extras/rootless/containerd-rootless-setuptool.sh @@ -112,7 +112,7 @@ cmd_entrypoint_check() { fi INFO "Checking cgroup v2" - controllers="/sys/fs/cgroup/user.slice/user-${id}.slice/user@${id}.service/cgroup.controllers" + controllers="/sys/fs/cgroup$(systemctl --user show --value --property=ControlGroup)/cgroup.controllers" if [ ! -f "${controllers}" ]; then WARNING "Enabling cgroup v2 is highly recommended, see https://rootlesscontaine.rs/getting-started/common/cgroup2/ " else diff --git a/pkg/infoutil/infoutil_linux.go b/pkg/infoutil/infoutil_linux.go index a6839c19ae2..da0af0390f6 100644 --- a/pkg/infoutil/infoutil_linux.go +++ b/pkg/infoutil/infoutil_linux.go @@ -17,6 +17,7 @@ package infoutil import ( + "context" "fmt" "runtime" "strings" @@ -141,9 +142,12 @@ func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) { func mobySysInfo(info *dockercompat.Info) *sysinfo.SysInfo { var mobySysInfoOpts []sysinfo.Opt if info.CgroupDriver == "systemd" && info.CgroupVersion == "2" && rootlessutil.IsRootless() { - g := fmt.Sprintf("/user.slice/user-%d.slice", rootlessutil.ParentEUID()) - mobySysInfoOpts = append(mobySysInfoOpts, sysinfo.WithCgroup2GroupPath(g)) + groupPath, err := systemdUserManagerControlGroup(context.TODO()) + if err != nil { + info.Warnings = append(info.Warnings, fmt.Sprintf("WARNING: Failed to detect rootless systemd cgroup: %v", err)) + groupPath = fmt.Sprintf("/user.slice/user-%d.slice", rootlessutil.ParentEUID()) + } + mobySysInfoOpts = append(mobySysInfoOpts, sysinfo.WithCgroup2GroupPath(groupPath)) } - mobySysInfo := sysinfo.New(mobySysInfoOpts...) - return mobySysInfo + return sysinfo.New(mobySysInfoOpts...) } diff --git a/pkg/infoutil/rootless_cgroup_linux.go b/pkg/infoutil/rootless_cgroup_linux.go new file mode 100644 index 00000000000..4b2523aa99e --- /dev/null +++ b/pkg/infoutil/rootless_cgroup_linux.go @@ -0,0 +1,46 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package infoutil + +import ( + "context" + "fmt" + "strconv" + + "github.com/coreos/go-systemd/v22/dbus" +) + +// systemdUserManagerControlGroup returns the cgroup containing the systemd user +// manager. Runc asks this manager to create rootless container scopes, so its +// ControlGroup is authoritative even when nerdctl runs in another cgroup. +func systemdUserManagerControlGroup(ctx context.Context) (string, error) { + conn, err := dbus.NewUserConnectionContext(ctx) + if err != nil { + return "", fmt.Errorf("connecting to systemd user manager: %w", err) + } + defer conn.Close() + + property, err := conn.GetManagerProperty("ControlGroup") + if err != nil { + return "", fmt.Errorf("getting systemd user manager ControlGroup: %w", err) + } + groupPath, err := strconv.Unquote(property) + if err != nil { + return "", fmt.Errorf("decoding systemd user manager ControlGroup property %q: %w", property, err) + } + return groupPath, nil +} From 40fec562a791adf4db23a31bf404944ef1150f74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:32:27 +0000 Subject: [PATCH 780/868] build(deps): bump docker/setup-qemu-action from 4.2.0 to 4.3.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/96fe6ef7f33517b61c61be40b68a1882f3264fb8...1f40c72289eff860ee54a304f1438e3cff362e0a) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index ae888e47e66..62e602649a0 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -37,7 +37,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e4e4ca6f39..721f304988a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: "Install go" uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: From f99df6318d764862d090c9bce47caf73d2400f08 Mon Sep 17 00:00:00 2001 From: Samran Asif Date: Sat, 5 Sep 2026 15:36:12 +0500 Subject: [PATCH 781/868] test(restart): drop outdated containerd 1.x plugin requirement from restart tests Signed-off-by: Samran Asif --- .../container/container_run_restart_linux_test.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 17761188e39..f27ec74f7ca 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -209,9 +209,6 @@ func TestRunRestart(t *testing.T) { func TestRunRestartWithOnFailure(t *testing.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", "--restart=on-failure:2", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") @@ -238,9 +235,6 @@ func TestRunRestartWithOnFailure(t *testing.T) { func TestRunRestartWithUnlessStopped(t *testing.T) { testCase := nerdtest.Setup() - if !nerdtest.IsDocker() { - 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") @@ -281,8 +275,6 @@ func TestUpdateRestartPolicy(t *testing.T) { // 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) { @@ -313,9 +305,6 @@ func TestUpdateRestartPolicy(t *testing.T) { // and check it can work correctly. func TestAddRestartPolicy(t *testing.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) @@ -354,9 +343,6 @@ func TestAddRestartPolicy(t *testing.T) { func TestRunRestartStatusLabel(t *testing.T) { testCase := nerdtest.Setup() - if !nerdtest.IsDocker() { - 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") From b02668d15fc5f45863608bb358e78b6a96354594 Mon Sep 17 00:00:00 2001 From: Daniel Benjamin Date: Mon, 7 Sep 2026 10:46:05 +0100 Subject: [PATCH 782/868] feat: support image volumes Signed-off-by: Daniel Benjamin --- cmd/nerdctl/compose/compose_up_linux_test.go | 152 +++++++++ docs/compose.md | 5 + pkg/composer/serviceparser/serviceparser.go | 91 +++++- .../serviceparser/serviceparser_test.go | 298 ++++++++++++++++++ pkg/composer/up.go | 24 +- pkg/composer/up_service.go | 37 ++- pkg/composer/up_service_test.go | 89 ++++++ 7 files changed, 669 insertions(+), 27 deletions(-) create mode 100644 pkg/composer/up_service_test.go diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 13a07eabafb..52829cf7f7e 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -17,6 +17,7 @@ package compose import ( + "errors" "fmt" "io" "path/filepath" @@ -1245,6 +1246,157 @@ services: testCase.Run(t) } +func TestComposeImageVolume(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Private + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + containerName := data.Identifier("image-volume") + composeYAML := fmt.Sprintf(` +services: + app: + image: %s + container_name: %s + command: ["sleep", "infinity"] + network_mode: none + volumes: + - type: image + source: %s + target: /website +`, testutil.CommonImage, containerName, testutil.NginxAlpineImage) + composePath := data.Temp().Path("compose.yaml") + data.Temp().Save(composeYAML, "compose.yaml") + + helpers.Anyhow("rmi", "-f", testutil.NginxAlpineImage) + helpers.Command("image", "inspect", testutil.NginxAlpineImage).Run(&test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + }) + helpers.Ensure("compose", "-f", composePath, "up", "-d") + helpers.Ensure("image", "inspect", testutil.NginxAlpineImage) + helpers.Command("inspect", "--format", "{{json .Mounts}}", containerName).Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(`"Type":"image"`), + }) + data.Labels().Set("containerName", containerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "source image files are visible", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("containerName"), "test", "-s", "/website/usr/share/nginx/html/index.html") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "image mount is read only", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("containerName"), "touch", "/website/should-not-exist") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("Read-only file system")}, nil), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "--volumes", "--remove-orphans") + helpers.Anyhow("rm", "-f", data.Identifier("image-volume")) + helpers.Anyhow("rmi", "-f", testutil.NginxAlpineImage) + } + + testCase.Run(t) +} + +func TestComposeImageVolumeServiceNameIsLiteral(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Private + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + projectName := data.Identifier("image-service-source") + containerName := data.Identifier("image-service-source-app") + sourceName := data.Identifier("image-service-source-image") + composeYAML := fmt.Sprintf(` +services: + %s: + image: %s + profiles: [image-source] + app: + image: %s + container_name: %s + command: ["sleep", "infinity"] + network_mode: none + volumes: + - type: image + source: %s + target: /website +`, sourceName, testutil.NginxAlpineImage, testutil.CommonImage, containerName, sourceName) + composePath := data.Temp().Save(composeYAML, "compose.yaml") + + helpers.Anyhow("rmi", "-f", testutil.NginxAlpineImage) + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("containerName", containerName) + data.Labels().Set("projectName", projectName) + data.Labels().Set("sourceName", sourceName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Command("compose", "-p", data.Labels().Get("projectName"), "-f", data.Labels().Get("composeYAML"), "up", "-d").Run(&test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New(data.Labels().Get("sourceName"))}, + }) + return helpers.Command("inspect", data.Labels().Get("containerName")) + } + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-p", data.Identifier("image-service-source"), "-f", data.Temp().Path("compose.yaml"), "down", "--volumes", "--remove-orphans") + helpers.Anyhow("rm", "-f", data.Identifier("image-service-source-app")) + helpers.Anyhow("rmi", "-f", testutil.NginxAlpineImage) + } + + testCase.Run(t) +} + +func TestComposeImageVolumeValidationDoesNotCreateNetwork(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.Private, + require.Not(nerdtest.Docker), + ) + testCase.NoParallel = true + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composeYAML := fmt.Sprintf(` +services: + app: + image: %s + volumes: + - type: image + target: /website +`, testutil.CommonImage) + data.Temp().Save(composeYAML, "compose.yaml") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + projectName := data.Identifier("invalid-image-volume") + helpers.Command("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "up", "-d").Run(&test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("image volume source is missing")}, + }) + return helpers.Command("network", "inspect", projectName+"_default") + } + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + projectName := data.Identifier("invalid-image-volume") + helpers.Anyhow("network", "rm", projectName+"_default") + helpers.Anyhow("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "down", "--volumes", "--remove-orphans") + } + + testCase.Run(t) +} + func TestComposeTmpfsVolume(t *testing.T) { testCase := nerdtest.Setup() diff --git a/docs/compose.md b/docs/compose.md index 02e59be61e9..c48639a612e 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -42,3 +42,8 @@ which was derived from [Docker Compose file version 3 specification](https://doc - `uid`, `gid`: Cannot be specified. The default value is not propagated from `USER` instruction of Dockerfile. The file owner corresponds to the original file on the host. - `mode`: Cannot be specified. The file is mounted as read-only, with permission bits that correspond to the original file on the host. + +#### `services..volumes[].type: image` +- Whole-image mounts are supported. +- `source` is interpreted as an image reference, including when it matches a service name. +- `services..volumes[].image.subpath` is not yet supported. diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index ad5c32c3e3b..58b9393dbd4 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -217,12 +217,18 @@ type Build struct { // TODO: call BuildKit API directly without executing `nerdctl build` } +type ImageMountSource struct { + Source string + Platform string +} + type Service struct { - Image string - PullMode string - Containers []Container // length = replicas - Build *Build - Unparsed *types.ServiceConfig + Image string + PullMode string + Containers []Container // length = replicas + Build *Build + Unparsed *types.ServiceConfig + ImageMountSources []ImageMountSource } func getReplicas(svc types.ServiceConfig) (int, error) { @@ -434,8 +440,24 @@ func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkName return fullNames, nil } +func imageVolumeSources(svc types.ServiceConfig) []ImageMountSource { + imageMountSources := make([]ImageMountSource, 0, len(svc.Volumes)) + for _, volume := range svc.Volumes { + if volume.Type != types.VolumeTypeImage { + continue + } + + imageMountSources = append(imageMountSources, ImageMountSource{ + Source: volume.Source, + Platform: svc.Platform, + }) + } + return imageMountSources +} + func Parse(project *types.Project, svc types.ServiceConfig) (*Service, error) { warnUnknownFields(svc) + imageMountSources := imageVolumeSources(svc) replicas, err := getReplicas(svc) if err != nil { @@ -443,10 +465,11 @@ func Parse(project *types.Project, svc types.ServiceConfig) (*Service, error) { } parsed := &Service{ - Image: svc.Image, - PullMode: "missing", - Containers: make([]Container, replicas), - Unparsed: &svc, + Image: svc.Image, + ImageMountSources: imageMountSources, + PullMode: "missing", + Containers: make([]Container, replicas), + Unparsed: &svc, } if svc.Build == nil { @@ -719,13 +742,22 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e } for _, v := range svc.Volumes { + if v.Type == types.VolumeTypeImage { + mount, err := serviceVolumeConfigToImageMount(v) + if err != nil { + return nil, err + } + c.RunArgs = append(c.RunArgs, "--mount="+mount) + continue + } + vStr, mkdir, err := serviceVolumeConfigToFlagV(v, project) if err != nil { return nil, err } switch v.Type { - case "tmpfs": + case types.VolumeTypeTmpfs: c.RunArgs = append(c.RunArgs, "--tmpfs="+vStr) default: c.RunArgs = append(c.RunArgs, "-v="+vStr) @@ -841,6 +873,45 @@ func servicePortConfigToFlagP(c types.ServicePortConfig) (string, error) { return s, nil } +func serviceVolumeConfigToImageMount(c types.ServiceVolumeConfig) (string, error) { + if c.Source == "" { + return "", errors.New("image volume source is missing") + } + if strings.Contains(c.Source, ",") { + return "", errors.New("image volume source must not contain commas") + } + if c.Target == "" { + return "", errors.New("volume target is missing") + } + if !filepath.IsAbs(c.Target) { + return "", fmt.Errorf("volume target must be an absolute path, got %q", c.Target) + } + if strings.Contains(c.Target, ",") { + return "", errors.New("volume target must not contain commas") + } + if c.Bind != nil { + return "", errors.New("image volume does not support bind options") + } + if c.Volume != nil { + return "", errors.New("image volume does not support volume options") + } + if c.Tmpfs != nil { + return "", errors.New("image volume does not support tmpfs options") + } + if c.Consistency != "" { + return "", errors.New("image volume does not support consistency options") + } + if c.Image != nil && c.Image.SubPath != "" { + return "", errors.New("image.subpath is not yet supported") + } + + mount := fmt.Sprintf("type=%s,source=%s,target=%s", types.VolumeTypeImage, c.Source, c.Target) + if c.ReadOnly { + mount += ",readonly" + } + return mount, nil +} + func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Project) (flagV string, mkdir []string, err error) { if unknown := reflectutil.UnknownNonEmptyFields(&c, "Type", diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index 8b567512097..ec23cdf6142 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -450,6 +450,304 @@ services: } } +func TestServiceVolumeConfigToImageMount(t *testing.T) { + t.Parallel() + + target := "/website" + if runtime.GOOS == "windows" { + target = `C:\website` + } + + testCases := []struct { + name string + volume types.ServiceVolumeConfig + want string + wantErr string + }{ + { + name: "whole image", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + }, + want: fmt.Sprintf("type=image,source=nginx:alpine,target=%s", target), + }, + { + name: "explicit read only", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + ReadOnly: true, + }, + want: fmt.Sprintf("type=image,source=nginx:alpine,target=%s,readonly", target), + }, + { + name: "missing source", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Target: target, + }, + wantErr: "image volume source is missing", + }, + { + name: "missing target", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + }, + wantErr: "volume target is missing", + }, + { + name: "relative target", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: "website", + }, + wantErr: `volume target must be an absolute path, got "website"`, + }, + { + name: "image subpath", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + Image: &types.ServiceVolumeImage{SubPath: "usr/share/nginx/html"}, + }, + wantErr: "image.subpath is not yet supported", + }, + { + name: "bind options", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + Bind: &types.ServiceVolumeBind{}, + }, + wantErr: "image volume does not support bind options", + }, + { + name: "volume options", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + Volume: &types.ServiceVolumeVolume{}, + }, + wantErr: "image volume does not support volume options", + }, + { + name: "tmpfs options", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + Tmpfs: &types.ServiceVolumeTmpfs{}, + }, + wantErr: "image volume does not support tmpfs options", + }, + { + name: "consistency options", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target, + Consistency: "cached", + }, + wantErr: "image volume does not support consistency options", + }, + { + name: "source containing comma", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine,type=bind,source=/", + Target: target, + }, + wantErr: "image volume source must not contain commas", + }, + { + name: "target containing comma", + volume: types.ServiceVolumeConfig{ + Type: types.VolumeTypeImage, + Source: "nginx:alpine", + Target: target + ",type=bind,source=/,target=/host", + }, + wantErr: "volume target must not contain commas", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := serviceVolumeConfigToImageMount(tc.volume) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + assert.NilError(t, err) + assert.Equal(t, got, tc.want) + }) + } +} + +func TestParseImageVolume(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("test is not compatible with windows") + } + + const dockerComposeYAML = ` +services: + foo: + image: alpine + volumes: + - type: image + source: nginx:alpine + target: /website +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + fooSvc, err := project.GetService("foo") + assert.NilError(t, err) + + foo, err := Parse(project, fooSvc) + assert.NilError(t, err) + assert.Equal(t, len(foo.Containers), 1) + assert.Assert(t, in(foo.Containers[0].RunArgs, "--mount=type=image,source=nginx:alpine,target=/website")) + assert.Assert(t, !in(foo.Containers[0].RunArgs, "-v=nginx:alpine:/website")) +} + +func TestParseImageVolumeServiceNameIsLiteral(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("test is not compatible with windows") + } + + testCases := []struct { + name string + referencedService types.ServiceConfig + disabled bool + }{ + { + name: "build service", + referencedService: types.ServiceConfig{ + Name: "builder", + Build: &types.BuildConfig{}, + Platform: "linux/amd64", + }, + }, + { + name: "explicit image", + referencedService: types.ServiceConfig{ + Name: "builder", + Image: "nginx:alpine", + Platform: "linux/amd64", + }, + }, + { + name: "disabled explicit image service", + referencedService: types.ServiceConfig{ + Name: "builder", + Image: "nginx:alpine", + Platform: "linux/amd64", + }, + disabled: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + app := types.ServiceConfig{ + Name: "app", + Image: "alpine", + Platform: "linux/arm64", + Volumes: []types.ServiceVolumeConfig{{ + Type: types.VolumeTypeImage, + Source: "builder", + Target: "/website", + }}, + } + project := &types.Project{ + Name: "project", + Services: types.Services{"app": app}, + } + if tc.disabled { + project.DisabledServices = types.Services{"builder": tc.referencedService} + } else { + project.Services["builder"] = tc.referencedService + } + + parsed, err := Parse(project, app) + assert.NilError(t, err) + assert.Equal(t, parsed.Unparsed.Volumes[0].Source, "builder") + assert.DeepEqual(t, parsed.ImageMountSources, []ImageMountSource{{ + Source: "builder", + Platform: "linux/arm64", + }}) + assert.Assert(t, in(parsed.Containers[0].RunArgs, + "--mount=type=image,source=builder,target=/website")) + assert.Equal(t, project.Services["app"].Volumes[0].Source, "builder") + }) + } +} + +func TestParseImageVolumePreservesOtherVolumeTypes(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("test is not compatible with windows") + } + + const dockerComposeYAML = ` +services: + foo: + image: alpine + volumes: + - type: image + source: nginx:alpine + target: /website + - type: bind + source: /host + target: /bind + - type: volume + source: named + target: /named + - type: volume + target: /anonymous + - type: tmpfs + target: /tmpfs +volumes: + named: +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + fooSvc, err := project.GetService("foo") + assert.NilError(t, err) + + foo, err := Parse(project, fooSvc) + assert.NilError(t, err) + assert.Equal(t, len(foo.Containers), 1) + runArgs := foo.Containers[0].RunArgs + assert.Assert(t, in(runArgs, "--mount=type=image,source=nginx:alpine,target=/website")) + assert.Assert(t, in(runArgs, "-v=/host:/bind")) + assert.Assert(t, in(runArgs, fmt.Sprintf("-v=%s_named:/named", project.Name))) + assert.Assert(t, in(runArgs, "-v=/anonymous")) + assert.Assert(t, in(runArgs, "--tmpfs=/tmpfs")) +} + func TestTmpfsVolumeLongSyntax(t *testing.T) { t.Parallel() diff --git a/pkg/composer/up.go b/pkg/composer/up.go index ec9155331bb..5ef9de44756 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -57,18 +57,6 @@ func (opts UpOptions) recreateStrategy() string { } func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) error { - for shortName := range c.project.Networks { - if err := c.upNetwork(ctx, shortName); err != nil { - return err - } - } - - for shortName := range c.project.Volumes { - if err := c.upVolume(ctx, shortName); err != nil { - return err - } - } - for shortName, secret := range c.project.Secrets { obj := types.FileObjectConfig(secret) if err := validateFileObjectConfig(obj, shortName, "service", c.project); err != nil { @@ -104,6 +92,18 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro return err } + for shortName := range c.project.Networks { + if err := c.upNetwork(ctx, shortName); err != nil { + return err + } + } + + for shortName := range c.project.Volumes { + if err := c.upVolume(ctx, shortName); err != nil { + return err + } + } + // remove orphan containers before the service has be started // FYI: https://github.com/docker/compose/blob/v2.3.4/pkg/compose/create.go#L91-L112 orphans, err := c.getOrphanContainers(ctx, parsedServices) diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index fb9b1ed9fbb..2ab76545e1a 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -103,14 +103,25 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars } func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool, pullModeArg string) error { + pullMode := ps.PullMode + if pullModeArg != "" { + pullMode = pullModeArg + } + if ps.Build != nil && allowBuild { if ps.Build.Force || forceBuild { - return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo) + if err := c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo); err != nil { + return err + } + return c.ensureImageMountSources(ctx, ps, pullMode, quiet) } if ok, err := c.ImageExists(ctx, ps.Image); err != nil { return err } else if !ok { - return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo) + if err := c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo); err != nil { + return err + } + return c.ensureImageMountSources(ctx, ps, pullMode, quiet) } // even when c.ImageExists returns true, we need to call c.EnsureImage // because ps.PullMode can be "always". So no return here. @@ -118,10 +129,26 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser } log.G(ctx).Infof("Ensuring image %s", ps.Image) - if pullModeArg != "" { - return c.EnsureImage(ctx, ps.Image, pullModeArg, ps.Unparsed.Platform, ps, quiet) + if err := c.EnsureImage(ctx, ps.Image, pullMode, ps.Unparsed.Platform, ps, quiet); err != nil { + return err } - return c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet) + return c.ensureImageMountSources(ctx, ps, pullMode, quiet) +} + +func (c *Composer) ensureImageMountSources(ctx context.Context, ps *serviceparser.Service, pullMode string, quiet bool) error { + seen := make(map[serviceparser.ImageMountSource]struct{}) + for _, source := range ps.ImageMountSources { + if _, ok := seen[source]; ok { + continue + } + seen[source] = struct{}{} + + log.G(ctx).Infof("Ensuring image mount source %s", source.Source) + if err := c.EnsureImage(ctx, source.Source, pullMode, source.Platform, ps, quiet); err != nil { + return fmt.Errorf("failed to ensure image %q for image volume: %w", source.Source, err) + } + } + return nil } // upServiceContainer must be called after ensureServiceImage diff --git a/pkg/composer/up_service_test.go b/pkg/composer/up_service_test.go new file mode 100644 index 00000000000..e270fe46c69 --- /dev/null +++ b/pkg/composer/up_service_test.go @@ -0,0 +1,89 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package composer + +import ( + "context" + "errors" + "testing" + + "github.com/compose-spec/compose-go/v2/types" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" +) + +func TestEnsureImageMountSources(t *testing.T) { + t.Parallel() + + type ensureCall struct { + Image string + PullMode string + Platform string + Quiet bool + } + var calls []ensureCall + composer := &Composer{Options: Options{ + EnsureImage: func(_ context.Context, imageName, pullMode, platform string, _ *serviceparser.Service, quiet bool) error { + calls = append(calls, ensureCall{ + Image: imageName, + PullMode: pullMode, + Platform: platform, + Quiet: quiet, + }) + return nil + }, + }} + service := &serviceparser.Service{ + Unparsed: &types.ServiceConfig{}, + ImageMountSources: []serviceparser.ImageMountSource{ + {Source: "nginx:alpine", Platform: "linux/amd64"}, + {Source: "nginx:alpine", Platform: "linux/amd64"}, + {Source: "nginx:alpine", Platform: "linux/arm64"}, + {Source: "caddy:alpine", Platform: "linux/arm64"}, + }, + } + + err := composer.ensureImageMountSources(context.Background(), service, types.PullPolicyAlways, true) + assert.NilError(t, err) + assert.DeepEqual(t, calls, []ensureCall{ + {Image: "nginx:alpine", PullMode: types.PullPolicyAlways, Platform: "linux/amd64", Quiet: true}, + {Image: "nginx:alpine", PullMode: types.PullPolicyAlways, Platform: "linux/arm64", Quiet: true}, + {Image: "caddy:alpine", PullMode: types.PullPolicyAlways, Platform: "linux/arm64", Quiet: true}, + }) +} + +func TestEnsureImageMountSourcesError(t *testing.T) { + t.Parallel() + + sentinel := errors.New("pull failed") + composer := &Composer{Options: Options{ + EnsureImage: func(context.Context, string, string, string, *serviceparser.Service, bool) error { + return sentinel + }, + }} + service := &serviceparser.Service{ + Unparsed: &types.ServiceConfig{}, + ImageMountSources: []serviceparser.ImageMountSource{ + {Source: "nginx:alpine"}, + }, + } + + err := composer.ensureImageMountSources(context.Background(), service, types.PullPolicyMissing, false) + assert.ErrorIs(t, err, sentinel) + assert.ErrorContains(t, err, `failed to ensure image "nginx:alpine" for image volume`) +} From 41923ea48f1734d47ae6ddd36f55feac21b53b74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:32:26 +0000 Subject: [PATCH 783/868] build(deps): bump golang.org/x/crypto in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.55.0 to 0.56.0 - [Commits](https://github.com/golang/crypto/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 806be6d9f63..ffdde1779ca 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.55.0 + golang.org/x/crypto v0.56.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 //gomodjail:unconfined golang.org/x/sys v0.47.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index f3a154e96c0..efecdbe358c 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= From 485500ad855d9cff545655b2920ab60e6948e565 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:33:37 +0000 Subject: [PATCH 784/868] build(deps): bump tags.cncf.io/container-device-interface Bumps [tags.cncf.io/container-device-interface](https://github.com/cncf-tags/container-device-interface) from 1.1.1-0.20260720132747-49ac08dcf160 to 1.1.1. - [Release notes](https://github.com/cncf-tags/container-device-interface/releases) - [Changelog](https://github.com/cncf-tags/container-device-interface/blob/main/RELEASE.md) - [Commits](https://github.com/cncf-tags/container-device-interface/commits/v1.1.1) --- updated-dependencies: - dependency-name: tags.cncf.io/container-device-interface dependency-version: 1.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++------ go.sum | 18 ++++-------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 806be6d9f63..a6f05a9dd1d 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( golang.org/x/term v0.45.0 //gomodjail:unconfined golang.org/x/text v0.41.0 gotest.tools/v3 v3.5.2 - tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160 //gomodjail:unconfined + tags.cncf.io/container-device-interface v1.1.1 //gomodjail:unconfined ) require ( @@ -127,18 +127,14 @@ require ( go.opentelemetry.io/otel v1.45.0 // indirect go.opentelemetry.io/otel/metric v1.45.0 // indirect go.opentelemetry.io/otel/trace v1.45.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.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect //gomodjail:unconfined google.golang.org/grpc v1.83.1 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect - tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect + tags.cncf.io/container-device-interface/specs-go v1.1.1 // indirect ) require go4.org/netipx v0.0.0-20231129151722-fdeea329fbba diff --git a/go.sum b/go.sum index f3a154e96c0..474d6354b00 100644 --- a/go.sum +++ b/go.sum @@ -284,8 +284,6 @@ 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= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= @@ -308,8 +306,6 @@ 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.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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= @@ -387,12 +383,8 @@ google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4J google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= @@ -401,9 +393,7 @@ pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk= sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= -sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160 h1:ZusVLZsIsAXuoxfJNIv3bL2Io7pKQyx0zaMbgRTz+X8= -tags.cncf.io/container-device-interface v1.1.1-0.20260720132747-49ac08dcf160/go.mod h1:Q9xVPbYCl0qhaLAPYQ7Dra+Bd51FsEUaGQb12MXxVb4= -tags.cncf.io/container-device-interface/specs-go v1.1.0 h1:QRZVeAceQM+zTZe12eyfuJuuzp524EKYwhmvLd+h+yQ= -tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= +tags.cncf.io/container-device-interface v1.1.1 h1:YPwQz4xg8PlQ0yT/baR0BtLpTQROe4l6M1yuRgAu1vc= +tags.cncf.io/container-device-interface v1.1.1/go.mod h1:S1PSJWYPD4Iom0/39mvr/VVFCfG0Yt14j20d0OYrY2M= +tags.cncf.io/container-device-interface/specs-go v1.1.1 h1:3xjaytilFeCBVFJsJTaT9uOFahoqJMVuYG7gYqi2+NY= +tags.cncf.io/container-device-interface/specs-go v1.1.1/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= From 9a48d3aa9425cc25d497ebe0d8d606b48ad61cf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:25:56 +0000 Subject: [PATCH 785/868] build(deps): bump the docker group across 1 directory with 3 updates Bumps the docker group with 3 updates in the / directory: [github.com/docker/cli](https://github.com/docker/cli), [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.7.2+incompatible to 29.8.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.7.2...v29.8.0) Updates `github.com/moby/moby/client` from 0.5.1 to 0.6.0 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.6.0/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.5.1...v0.6.0) Updates `github.com/moby/moby/v2` from 2.0.0-beta.21 to 2.0.0-beta.23 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.21...v2.0.0-beta.23) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.8.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/client dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 28 ++++++++++++------------- go.sum | 64 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index a6f05a9dd1d..419e180daaf 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.7.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.8.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -43,8 +43,8 @@ require ( github.com/ipfs/go-cid v0.6.2 github.com/klauspost/compress v1.19.2 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined - github.com/moby/moby/client v0.5.1 - github.com/moby/moby/v2 v2.0.0-beta.21 + github.com/moby/moby/client v0.6.0 + github.com/moby/moby/v2 v2.0.0-beta.23 github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.1 //gomodjail:unconfined @@ -64,7 +64,7 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.55.0 + golang.org/x/crypto v0.56.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 //gomodjail:unconfined golang.org/x/sys v0.47.0 //gomodjail:unconfined @@ -78,14 +78,14 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/cilium/ebpf v0.22.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/go-runc v1.1.0 // indirect + github.com/containerd/go-runc v1.2.1 // indirect github.com/containerd/plugin v1.1.0 // indirect github.com/containerd/ttrpc v1.2.9 // indirect github.com/containers/ocicrypt v1.3.2 // indirect github.com/creack/pty v1.1.24 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -123,14 +123,14 @@ require ( github.com/vbatts/tar-split v0.12.3 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.45.0 // indirect - go.opentelemetry.io/otel/metric v1.45.0 // indirect - go.opentelemetry.io/otel/trace v1.45.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect //gomodjail:unconfined - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/grpc v1.83.2 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12 // indirect lukechampine.com/blake3 v1.3.0 // indirect @@ -145,8 +145,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/moby/moby/api v1.55.0 // indirect - go.opentelemetry.io/otel/sdk v1.45.0 // indirect + github.com/moby/moby/api v1.56.0 // indirect + go.opentelemetry.io/otel/sdk v1.46.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 474d6354b00..9d0f1caec49 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/go-cni v1.1.14 h1:jcFWauA5ED2wUHgCdvPB/IyvOtBcXzmgj8LprJ8nJH0= github.com/containerd/go-cni v1.1.14/go.mod h1:igdwKOd5qpuMIafFcovqefXeThU/s2MoDSOnkCv77fw= -github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA= -github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U= +github.com/containerd/go-runc v1.2.1 h1:TAnah92bVA7dYDZ7Mm9FrOoFQvnLZdL3z3xT7BS19kA= +github.com/containerd/go-runc v1.2.1/go.mod h1:Azy6SkBcIFMSMYiYUuSQhZ25zD3zJEYYbbIVplhYD7M= github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQujLw7UQ3w= github.com/containerd/imgcrypt/v2 v2.0.3/go.mod h1:Sjfd3uOiBWtb1dUo1yS5Z/ZxdfMU7MpBr2pzs9HoRDs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -85,8 +85,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.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= -github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk= +github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= @@ -99,8 +99,8 @@ github.com/fahedouch/go-logrotate v0.3.0 h1:XP+dHIDgWZ1ckz43mG6gl5ASer3PZDVr755S github.com/fahedouch/go-logrotate v0.3.0/go.mod h1:X49m0bvPLkk71MHNCQ1yEfVEw8W/u+qvHa/hOnhCYf4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ= github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -155,8 +155,8 @@ github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXN github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -167,12 +167,12 @@ 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.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.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= -github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= -github.com/moby/moby/v2 v2.0.0-beta.21 h1:LrUr8ocwGt3nOdPRKmLMKRRPqYqKJP4VbZxT2vZwscs= -github.com/moby/moby/v2 v2.0.0-beta.21/go.mod h1:Myh7qqKNMQ1bdk8kRugUA/xhlEUIw/drvN/h0atw4y0= +github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= +github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= +github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= +github.com/moby/moby/v2 v2.0.0-beta.23 h1:vDK8/40mF1U1neLp6XOttWuSDeTMFgIODXFeOsgo2Uw= +github.com/moby/moby/v2 v2.0.0-beta.23/go.mod h1:CtC7n8ozJwwpHGGjNg6md+GBMuYhh7nVwuOmhmYEegY= 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= @@ -268,18 +268,18 @@ github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIx github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.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.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= -go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= -go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= -go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= -go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= -go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= -go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= -go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= -go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= 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= @@ -297,8 +297,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -376,10 +376,10 @@ golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= -google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From bda7a8ae28fab1ce39a689761a7b72be400f0e9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:14:54 +0000 Subject: [PATCH 786/868] build(deps): bump github.com/klauspost/compress from 1.19.2 to 1.20.0 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.2 to 1.20.0. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.19.2...v1.20.0) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 419e180daaf..2bac8e63192 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/fsnotify/fsnotify v1.10.1 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 - github.com/klauspost/compress v1.19.2 + github.com/klauspost/compress v1.20.0 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined github.com/moby/moby/client v0.6.0 github.com/moby/moby/v2 v2.0.0-beta.23 diff --git a/go.sum b/go.sum index 9d0f1caec49..ae3c3ce8163 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ 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/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= -github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From f01971c890f417a5ff6fb816102957499fedc6ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:32:33 +0000 Subject: [PATCH 787/868] build(deps): bump the golang-x group with 2 updates Bumps the golang-x group with 2 updates: [golang.org/x/sync](https://github.com/golang/sync) and [golang.org/x/sys](https://github.com/golang/sys). Updates `golang.org/x/sync` from 0.22.0 to 0.23.0 - [Commits](https://github.com/golang/sync/compare/v0.22.0...v0.23.0) Updates `golang.org/x/sys` from 0.47.0 to 0.48.0 - [Commits](https://github.com/golang/sys/compare/v0.47.0...v0.48.0) --- updated-dependencies: - dependency-name: golang.org/x/sync dependency-version: 0.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 419e180daaf..d07ed26f4a2 100644 --- a/go.mod +++ b/go.mod @@ -66,8 +66,8 @@ require ( go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.56.0 golang.org/x/net v0.58.0 - golang.org/x/sync v0.22.0 //gomodjail:unconfined - golang.org/x/sys v0.47.0 //gomodjail:unconfined + golang.org/x/sync v0.23.0 //gomodjail:unconfined + golang.org/x/sys v0.48.0 //gomodjail:unconfined golang.org/x/term v0.45.0 //gomodjail:unconfined golang.org/x/text v0.41.0 gotest.tools/v3 v3.5.2 diff --git a/go.sum b/go.sum index 9d0f1caec49..c5dd03d2511 100644 --- a/go.sum +++ b/go.sum @@ -323,8 +323,8 @@ 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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -341,8 +341,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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= 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= From 559192a0b2a4c351d8fb1b8b4966be69838d9a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:32:52 +0000 Subject: [PATCH 788/868] build(deps): bump github.com/compose-spec/compose-go/v2 Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.14.0 to 2.15.0. - [Release notes](https://github.com/compose-spec/compose-go/releases) - [Commits](https://github.com/compose-spec/compose-go/compare/v2.14.0...v2.15.0) --- updated-dependencies: - dependency-name: github.com/compose-spec/compose-go/v2 dependency-version: 2.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 419e180daaf..f1dd38b5ec2 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.4 - github.com/compose-spec/compose-go/v2 v2.14.0 //gomodjail:unconfined + github.com/compose-spec/compose-go/v2 v2.15.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.4 github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 9d0f1caec49..07906c78fe0 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ= -github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00= +github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= github.com/containerd/accelerated-container-image v1.4.4 h1:88mL7plI0lvrzCiU1obhB4CFE8YFWFiqzNipydqiYCM= github.com/containerd/accelerated-container-image v1.4.4/go.mod h1:h8+s7FnzpT1fnPqH31JK9LybCqiRd1IgLgA82vtX+Tc= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= From ce52e9fe26807ac0fa9afa969f0454c35144b9cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:32:58 +0000 Subject: [PATCH 789/868] build(deps): bump github.com/containerd/log from 0.1.0 to 0.2.0 Bumps [github.com/containerd/log](https://github.com/containerd/log) from 0.1.0 to 0.2.0. - [Release notes](https://github.com/containerd/log/releases) - [Commits](https://github.com/containerd/log/compare/v0.1.0...v0.2.0) --- updated-dependencies: - dependency-name: github.com/containerd/log dependency-version: 0.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 419e180daaf..7e57cb184c7 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/containerd/fifo v1.1.0 //gomodjail:unconfined github.com/containerd/go-cni v1.1.14 //gomodjail:unconfined github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined - github.com/containerd/log v0.1.0 + github.com/containerd/log v0.2.0 github.com/containerd/nerdctl/mod/tigron v0.0.0 github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 9d0f1caec49..b86d97c2d37 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/containerd/go-runc v1.2.1 h1:TAnah92bVA7dYDZ7Mm9FrOoFQvnLZdL3z3xT7BS1 github.com/containerd/go-runc v1.2.1/go.mod h1:Azy6SkBcIFMSMYiYUuSQhZ25zD3zJEYYbbIVplhYD7M= github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQujLw7UQ3w= github.com/containerd/imgcrypt/v2 v2.0.3/go.mod h1:Sjfd3uOiBWtb1dUo1yS5Z/ZxdfMU7MpBr2pzs9HoRDs= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= +github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk= github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= From 37fec18414a2b5f82becf4e2cf174116dfc99ba4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 10:53:56 +0900 Subject: [PATCH 790/868] go.mod: github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 Signed-off-by: Akihiro Suda --- go.mod | 8 ++++---- go.sum | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 4e1094f5885..dadafd62528 100644 --- a/go.mod +++ b/go.mod @@ -57,19 +57,19 @@ require ( github.com/opencontainers/selinux v1.15.1 github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.1.0 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 //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.5 - golang.org/x/crypto v0.56.0 + golang.org/x/crypto v0.57.0 golang.org/x/net v0.58.0 golang.org/x/sync v0.23.0 //gomodjail:unconfined golang.org/x/sys v0.48.0 //gomodjail:unconfined - golang.org/x/term v0.45.0 //gomodjail:unconfined - golang.org/x/text v0.41.0 + golang.org/x/term v0.46.0 //gomodjail:unconfined + golang.org/x/text v0.42.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.1 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index f493fc6ee02..f80ca21a083 100644 --- a/go.sum +++ b/go.sum @@ -229,8 +229,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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.1.0 h1:6RR+6Y9aml6gIkn4ohcHIpIy6I7Wmyt3iglKKEIb678= -github.com/rootless-containers/rootlesskit/v3 v3.1.0/go.mod h1:oFY5X3mj9mOpi/F+oBaD5ojo6QZhr9JdcpsQ6qepz6Q= +github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 h1:ioTLLDV1EMgjrrs0PYlgZ5Mxri8IbiVwzC6hUAyJwY8= +github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0/go.mod h1:2+6juXfEVqXFICaGaaWkSnyDdhbkWv7WAvUg8hnsKwU= 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= @@ -297,8 +297,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -352,8 +352,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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= 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= @@ -363,16 +363,16 @@ 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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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= From 27f73828850a139054010f78918c6a1dc2eb3a44 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 10:54:50 +0900 Subject: [PATCH 791/868] update RootlessKit (3.2.0-beta.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 diff --git a/Dockerfile b/Dockerfile index e2bfd582e4d..141c1569b0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ 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.1.0@BINARY +ARG ROOTLESSKIT_VERSION=v3.2.0-beta.0@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 deleted file mode 100644 index 5fc0011ced7..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.1.0 +++ /dev/null @@ -1,6 +0,0 @@ -42d1c22a34be7bb458f9ae7363d68dd05d553422d95392b44d0be7fa1fd76bf0 rootlesskit-aarch64.tar.gz -39608ccc4ea8cf7e243b569abed5b237339416b830f96e51898957fb9f8c7341 rootlesskit-armv7l.tar.gz -4a10d5fd12c78e569d58b2156f061e1380497416a4db19a123f5097577ddc83a rootlesskit-ppc64le.tar.gz -04eaaac6b855230b1b13f0581438e0aee446dff316f7c32af244ab494cdc042f rootlesskit-riscv64.tar.gz -d277da00fd3ee8d60183e1c6e843519305a06e701f2855d46ffe15d6bad1dff8 rootlesskit-s390x.tar.gz -b1302b7395918266d561b9e3053771253f20761807e042ae80a1868d6e86b71c rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 new file mode 100644 index 00000000000..ba2201500d1 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 @@ -0,0 +1,6 @@ +eb4c76a73a64e703e2cf634c7c71e4c16dd18952d50d4d4d957154b889dcd7d9 rootlesskit-aarch64.tar.gz +69bd2275ca8b1373def427d40904e3337b0c453444cca1319f1ae94e80375e7a rootlesskit-armv7l.tar.gz +dc83ebd59492c43b167f1a414ae75941124a259493d56a9088c5e362f372d5e4 rootlesskit-ppc64le.tar.gz +b31ef1c9b6b5d7ca3ea013f0cacfa92caf2e54207967e506c8ab4fa712b87915 rootlesskit-riscv64.tar.gz +241e8f2338a4c34a57bbae7b1622ff586922970668091f37a081b5516b6df354 rootlesskit-s390x.tar.gz +2c05852a782fba0c9ff268065c156b8a4a827dc018b5d968ebeb2cc2ee947900 rootlesskit-x86_64.tar.gz From 407df054a40c7ed5f82409b010d80b8cff389f00 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 9 Sep 2026 02:23:29 +0900 Subject: [PATCH 792/868] gomodjail: update to v2.0.1, enforce static analysis in CI gomodjail v2 moves the focus from the dynamic (seccomp) mode to a static analysis gate: `gomodjail analyze` fails if a module annotated `gomodjail:confined` in go.mod can reach a denied capability (filesystem, network, process execution, raw syscalls, OS state modification, or cgo). - Dockerfile: bump GOMODJAIL_VERSION to v2.0.1. `gomodjail pack` is unchanged in v2, so the packed `nerdctl.gomodjail` binary (dynamic mode) keeps working as before. - Makefile: add `lint-gomodjail` / `lint-gomodjail-all` (part of `make lint`) and `fix-gomodjail` (part of `make fix`), and install gomodjail in `install-dev-tools`. The gate is only enforced for linux/amd64 and linux/arm64: these are the only platforms the packed binary is built for, and the only ones the dynamic mode supports. The verdicts are platform-dependent, hence both architectures are analyzed. - CI: add the `gomodjail` job to the lint workflow. - go.mod: `gomodjail fix` downgraded the annotation of the 39 modules that fail the gate to `gomodjail:unconfined`, so that the decision stays visible and reviewable. 57 modules remain confined (24 ok, 33 warnings, 0 violations). The inline annotations are also reformatted to `// gomodjail:...`, the style `gomodjail fix` writes, so that future fixes do not leave go.mod in two styles. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 37 +++++++++++++++++++ Dockerfile | 2 +- Makefile | 53 ++++++++++++++++++++++++--- docs/testing/README.md | 7 ++++ go.mod | 56 ++++++++++++++++++++--------- 5 files changed, 134 insertions(+), 21 deletions(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index f3f39b875a4..d9c9cbf5799 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -55,6 +55,43 @@ jobs: go-version: "1.26" runner: ubuntu-26.04 + # Runs the gomodjail static analysis gate, that verifies that the Go modules annotated + # `gomodjail:confined` in go.mod cannot reach a denied capability (filesystem, network, + # exec, raw syscalls, ...). + # https://github.com/AkihiroSuda/gomodjail + # To run locally, use `make lint-gomodjail-all`. + lint-gomodjail: + name: "gomodjail" + timeout-minutes: 10 + runs-on: ubuntu-26.04 + env: + GOTOOLCHAIN: local + steps: + - name: "Init: checkout" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + + - name: "Init: install go" + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: "1.26" + check-latest: true + # This workflow also runs on push, so the setup-go cache is disabled + # for zizmor's cache-poisoning audit + cache: false + + - name: "Init: install dev-tools" + run: | + echo "::group:: make install-dev-tools" + make install-dev-tools + echo "::endgroup::" + + - name: "Run" + run: | + NO_COLOR=true make lint-gomodjail-all + # Lint for shell and yaml files lint-other: name: "other" diff --git a/Dockerfile b/Dockerfile index 141c1569b0e..79b05a2ba98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug ARG BUILDG_VERSION=v0.5.3@BINARY # Extra deps: gomodjail -ARG GOMODJAIL_VERSION=v0.3.2@c145bb1e36fe0939c5fa0467f2477878dea8e3d9 +ARG GOMODJAIL_VERSION=v2.0.1@5924a4079d0f70459a10973f715238dc336478ea # Test deps # Currently, the Docker Official Images and the test deps are not pinned by the hash diff --git a/Makefile b/Makefile index 1deca854a00..b738a73adb4 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ DOCKER ?= docker GO ?= go GOOS ?= $(shell $(GO) env GOOS) GOARCH ?= $(shell $(GO) env GOARCH) +GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) ifeq ($(GOOS),windows) BIN_EXT := .exe endif @@ -83,9 +84,9 @@ endef ########################## all: binaries -lint: lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses-all +lint: lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-gomodjail-all lint-licenses-all -fix: fix-mod fix-go-all +fix: fix-mod fix-gomodjail fix-go-all # TODO: fix race task and add it test: test-unit # test-unit-race test-unit-bench @@ -175,6 +176,30 @@ lint-mod: && go mod tidy --diff $(call footer, $@) +# gomodjail statically verifies that the modules annotated `gomodjail:confined` in go.mod +# cannot reach a denied capability (filesystem, network, exec, raw syscalls, ...). +# https://github.com/AkihiroSuda/gomodjail +lint-gomodjail: + $(call title, $@: $(GOOS)/$(GOARCH)) +ifeq ($(GOHOSTOS),windows) + @echo "Skipped: gomodjail does not support Windows hosts" +else + @cd $(MAKEFILE_DIR) \ + && gomodjail analyze --goos=$(GOOS) --goarch=$(GOARCH) ./... +endif + $(call footer, $@) + +# The confinement is only enforced for linux/amd64 and linux/arm64, as these are the only +# platforms for which the gomodjail-packed binary is built (see Dockerfile), and the only +# ones supported by the gomodjail dynamic mode. The verdicts are platform-dependent, hence +# both architectures have to be analyzed. +lint-gomodjail-all: + $(call title, $@) + @cd $(MAKEFILE_DIR) \ + && GOOS=linux GOARCH=amd64 make lint-gomodjail \ + && GOOS=linux GOARCH=arm64 make lint-gomodjail + $(call footer, $@) + # FIXME: go-licenses cannot find LICENSE from root of repo when submodule is imported: # https://github.com/google/go-licenses/issues/186 # This is impacting gotest.tools @@ -221,6 +246,19 @@ fix-mod: && go mod tidy $(call footer, $@) +# Downgrades the `gomodjail:confined` annotation of the modules that fail `make lint-gomodjail-all` +# to `gomodjail:unconfined`, so that the annotations in go.mod stay reviewable. +fix-gomodjail: + $(call title, $@) +ifeq ($(GOHOSTOS),windows) + @echo "Skipped: gomodjail does not support Windows hosts" +else + @cd $(MAKEFILE_DIR) \ + && gomodjail fix --goos=linux --goarch=amd64 ./... \ + && gomodjail fix --goos=linux --goarch=arm64 ./... +endif + $(call footer, $@) + ########################## # Development tools installation ########################## @@ -238,6 +276,13 @@ install-dev-tools: && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d + # gomodjail: v2.0.1 (2026-09-09) + # Not installed on Windows hosts: gomodjail does not build there, as its dynamic mode + # is compiled in unconditionally (https://github.com/AkihiroSuda/gomodjail) +ifneq ($(GOHOSTOS),windows) + @cd $(MAKEFILE_DIR) \ + && go install github.com/AkihiroSuda/gomodjail/v2/cmd/gomodjail@5924a4079d0f70459a10973f715238dc336478ea +endif @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) @@ -319,8 +364,8 @@ artifacts: clean install \ uninstall \ clean \ - lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses lint-licenses-all \ - fix-go fix-go-all fix-mod \ + lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-gomodjail lint-gomodjail-all lint-licenses lint-licenses-all \ + fix-go fix-go-all fix-mod fix-gomodjail \ install-dev-tools \ test-unit test-unit-race test-unit-bench \ artifacts diff --git a/docs/testing/README.md b/docs/testing/README.md index cbdc151ec4f..efea60cf4b5 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -31,6 +31,13 @@ eg: or `LINT_COMMIT_RANGE=target_branch..HEAD make lint` +`make lint` also runs [gomodjail](https://github.com/AkihiroSuda/gomodjail) in its static analysis +mode (`make lint-gomodjail-all`), to verify that the modules annotated `gomodjail:confined` in +`go.mod` cannot reach a denied capability (filesystem, network, process execution, raw syscalls, +OS state modification, or cgo). If a dependency bump makes a confined module reach one of those, +`make fix` (or `make fix-gomodjail`) downgrades its annotation to `gomodjail:unconfined`, so that +the decision stays visible in `go.mod`. + ## Unit testing ``` diff --git a/go.mod b/go.mod index 8024b60afc3..c073bcacb48 100644 --- a/go.mod +++ b/go.mod @@ -8,18 +8,18 @@ require ( github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/Microsoft/hcsshim v0.15.0-rc.4 github.com/compose-spec/compose-go/v2 v2.15.0 //gomodjail:unconfined - github.com/containerd/accelerated-container-image v1.4.4 + github.com/containerd/accelerated-container-image v1.4.4 //gomodjail:unconfined 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.12.0-rc.0 + github.com/containerd/containerd/api v1.12.0-rc.0 //gomodjail:unconfined github.com/containerd/containerd/v2 v2.4.0-beta.0 //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 github.com/containerd/go-cni v1.1.14 //gomodjail:unconfined github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined - github.com/containerd/log v0.2.0 - github.com/containerd/nerdctl/mod/tigron v0.0.0 + github.com/containerd/log v0.2.0 //gomodjail:unconfined + github.com/containerd/nerdctl/mod/tigron v0.0.0 //gomodjail:unconfined github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined @@ -29,24 +29,24 @@ require ( github.com/containernetworking/cni v1.3.1 //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/coreos/go-systemd/v22 v22.7.0 //gomodjail:unconfined github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.8.0+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.8.1 + github.com/docker/go-connections v0.8.1 //gomodjail:unconfined github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined - github.com/fluent/fluent-logger-golang v1.10.1 + github.com/fluent/fluent-logger-golang v1.10.1 //gomodjail:unconfined github.com/fsnotify/fsnotify v1.10.1 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/ipfs/go-cid v0.6.2 + github.com/ipfs/go-cid v0.6.2 //gomodjail:unconfined github.com/klauspost/compress v1.20.0 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined - github.com/moby/moby/client v0.6.0 - github.com/moby/moby/v2 v2.0.0-beta.23 - github.com/moby/sys/mount v0.3.5 - github.com/moby/sys/signal v0.7.1 + github.com/moby/moby/client v0.6.0 //gomodjail:unconfined + github.com/moby/moby/v2 v2.0.0-beta.23 //gomodjail:unconfined + github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined + github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined github.com/moby/sys/user v0.4.1 //gomodjail:unconfined github.com/moby/sys/userns v0.2.0 //gomodjail:unconfined github.com/moby/term v0.5.2 //gomodjail:unconfined @@ -54,7 +54,7 @@ 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.1 + github.com/opencontainers/selinux v1.15.1 //gomodjail:unconfined github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 //gomodjail:unconfined @@ -62,68 +62,89 @@ require ( 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 + github.com/yuchanns/srslog v1.1.0 //gomodjail:unconfined go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.57.0 - golang.org/x/net v0.58.0 + golang.org/x/net v0.58.0 //gomodjail:unconfined golang.org/x/sync v0.23.0 //gomodjail:unconfined golang.org/x/sys v0.48.0 //gomodjail:unconfined golang.org/x/term v0.46.0 //gomodjail:unconfined golang.org/x/text v0.42.0 - gotest.tools/v3 v3.5.2 + gotest.tools/v3 v3.5.2 //gomodjail:unconfined tags.cncf.io/container-device-interface v1.1.1 //gomodjail:unconfined ) require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + //gomodjail:unconfined github.com/cilium/ebpf v0.22.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect + //gomodjail:unconfined github.com/containerd/go-runc v1.2.1 // indirect github.com/containerd/plugin v1.1.0 // indirect + //gomodjail:unconfined github.com/containerd/ttrpc v1.2.9 // indirect + //gomodjail:unconfined github.com/containers/ocicrypt v1.3.2 // indirect + //gomodjail:unconfined github.com/creack/pty v1.1.24 // indirect + //gomodjail:unconfined github.com/djherbis/times v1.6.0 // indirect + //gomodjail:unconfined github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + //gomodjail:unconfined github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + //gomodjail:unconfined github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + //gomodjail:unconfined github.com/mattn/go-shellwords v1.0.13 // indirect + //gomodjail:unconfined github.com/miekg/pkcs11 v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect + //gomodjail:unconfined github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect + //gomodjail:unconfined github.com/moby/sys/mountinfo v0.7.2 // 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 github.com/multiformats/go-base36 v0.2.0 // indirect + //gomodjail:unconfined github.com/multiformats/go-multiaddr v0.16.1 // indirect github.com/multiformats/go-multibase v0.3.0 // indirect + //gomodjail:unconfined github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.1.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect + //gomodjail:unconfined github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect //gomodjail:unconfined github.com/sirupsen/logrus v1.10.2 // indirect github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + //gomodjail:unconfined github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + //gomodjail:unconfined github.com/tinylib/msgp v1.3.0 // indirect + //gomodjail:unconfined github.com/vbatts/tar-split v0.12.3 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + //gomodjail:unconfined go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect + //gomodjail:unconfined go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect @@ -133,6 +154,7 @@ require ( google.golang.org/grpc v1.83.2 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12 // indirect + //gomodjail:unconfined lukechampine.com/blake3 v1.3.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.1.1 // indirect ) @@ -144,10 +166,12 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect + //gomodjail:unconfined github.com/google/uuid v1.6.0 // indirect github.com/moby/moby/api v1.56.0 // indirect go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + //gomodjail:unconfined sigs.k8s.io/knftables v0.0.18 // indirect ) From 19f87a69a7f6b8e8ab50cdde8b56b579fe7f7310 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Wed, 9 Sep 2026 20:27:19 +0400 Subject: [PATCH 793/868] fix: match ps --filter keys exactly instead of by prefix Signed-off-by: Immanuel Tikhonov --- .../container/container_list_linux_test.go | 36 ++++++++++ pkg/cmd/container/list_util.go | 14 ++-- pkg/cmd/container/list_util_test.go | 71 +++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 pkg/cmd/container/list_util_test.go diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index 9d23eaa73cd..87c8ebacc1d 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -17,6 +17,7 @@ package container import ( + "errors" "fmt" "slices" "strings" @@ -795,6 +796,41 @@ func containerListFilterSubTests() []*test.Case { } } +// TestContainerListWithInvalidFilter checks that `nerdctl ps --filter` rejects +// unknown filter keys, including ones that merely share a prefix with a +// supported key, the same way Docker does. +func TestContainerListWithInvalidFilter(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "unknown filter sharing a prefix with a supported one is rejected", + // "labels" is not a supported filter; it must not be routed to the + // "label" handler. + Command: test.Command("ps", "-a", "--filter", "labels=foo"), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{ + errors.New("invalid filter 'labels=foo'"), + }, nil), + }, + { + Description: "wholly unknown filter is rejected", + Command: test.Command("ps", "-a", "--filter", "bogus=foo"), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{ + errors.New("invalid filter 'bogus=foo'"), + }, nil), + }, + { + Description: "supported filter without a value is a format error", + Command: test.Command("ps", "-a", "--filter", "label"), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{ + errors.New("bad format of filter (expected name=value)"), + }, nil), + }, + } + + testCase.Run(t) +} + func TestContainerListCheckCreatedTime(t *testing.T) { testCase := nerdtest.Setup() testCase.NoParallel = true diff --git a/pkg/cmd/container/list_util.go b/pkg/cmd/container/list_util.go index a2fdb0a2892..2998340065f 100644 --- a/pkg/cmd/container/list_util.go +++ b/pkg/cmd/container/list_util.go @@ -81,16 +81,20 @@ func (cl *containerFilterContext) foldFilters(ctx context.Context, filters []str {"exited", cl.foldExitedFilter}, } for _, filter := range filters { + // A filter is "key=value"; the key must match a supported filter type + // exactly. Matching on a prefix instead would misroute filters such as + // "labels=x" to the "label" handler, whereas Docker rejects them as + // unknown filters. + key, value, hasValue := strings.Cut(filter, "=") invalidFilter := true for _, folder := range folders { - if !strings.HasPrefix(filter, folder.filterType) { + if key != folder.filterType { continue } - splited := strings.SplitN(filter, "=", 2) - if len(splited) != 2 { - return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", folder.filterType) + if !hasValue { + return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter) } - if err := folder.foldFunc(ctx, filter, splited[1]); err != nil { + if err := folder.foldFunc(ctx, filter, value); err != nil { return err } invalidFilter = false diff --git a/pkg/cmd/container/list_util_test.go b/pkg/cmd/container/list_util_test.go new file mode 100644 index 00000000000..80106fd3c81 --- /dev/null +++ b/pkg/cmd/container/list_util_test.go @@ -0,0 +1,71 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "testing" + + "gotest.tools/v3/assert" +) + +func TestFoldContainerFilters(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("supported filters are accepted", func(t *testing.T) { + t.Parallel() + for _, f := range []string{ + "id=abc", + "name=foo", + "label=env", + "label=env=prod", + "label=com.example.payload=a=b", + "status=running", + "exited=0", + } { + _, err := foldContainerFilters(ctx, nil, []string{f}) + assert.NilError(t, err, "filter %q should be accepted", f) + } + }) + + t.Run("unknown filters are rejected", func(t *testing.T) { + t.Parallel() + // The keys below share a prefix with a supported filter but are not a + // supported filter themselves. Docker rejects each of them with + // "invalid filter ''"; nerdctl used to silently route them to the + // prefix's handler (e.g. "labels=env" behaved like "label=env"). + for _, f := range []string{ + "labels=env", + "name2=foo", + "statuss=running", + "ids=abc", + "volumes=v", + "networkfoo=n", + "totallybogus=x", + } { + _, err := foldContainerFilters(ctx, nil, []string{f}) + assert.ErrorContains(t, err, "invalid filter", "filter %q should be rejected", f) + } + }) + + t.Run("supported filter without a value is a format error", func(t *testing.T) { + t.Parallel() + _, err := foldContainerFilters(ctx, nil, []string{"label"}) + assert.ErrorContains(t, err, "bad format of filter") + }) +} From 1cbadc2069a5b7679fb756fcd850013d5a4d54cd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:31:09 +0900 Subject: [PATCH 794/868] CI: use GitHub's dedicated self-repository syntax Fix zizmor failures observed with zizmor-action v0.6.3 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-flaky.yml | 4 ++-- .github/workflows/workflow-lint.yml | 8 ++++---- .github/workflows/workflow-test.yml | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 92b4e611dce..718f21bc0b0 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -16,7 +16,7 @@ permissions: jobs: test-integration-el: name: "EL${{ inputs.hack }}" - uses: ./.github/workflows/job-test-in-lima.yml + uses: $/.github/workflows/job-test-in-lima.yml strategy: fail-fast: false # EL8 is used for testing compatibility with cgroup v1. @@ -37,7 +37,7 @@ jobs: test-integration-freebsd: name: "FreeBSD" - uses: ./.github/workflows/job-test-in-lima-freebsd.yml + uses: $/.github/workflows/job-test-in-lima-freebsd.yml with: timeout: 15 runner: ubuntu-26.04 diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index f3f39b875a4..b37878cc110 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -17,7 +17,7 @@ jobs: # 3. for canary (if there is a canary go version), does lint for all supported goos lint-go: name: "go${{ inputs.hack }}" - uses: ./.github/workflows/job-lint-go.yml + uses: $/.github/workflows/job-lint-go.yml strategy: fail-fast: false matrix: @@ -49,7 +49,7 @@ jobs: # Run common project checks (commits, licenses, etc) lint-project-checks: name: "project checks" - uses: ./.github/workflows/job-lint-project.yml + uses: $/.github/workflows/job-lint-project.yml with: timeout: 5 go-version: "1.26" @@ -58,7 +58,7 @@ jobs: # Lint for shell and yaml files lint-other: name: "other" - uses: ./.github/workflows/job-lint-other.yml + uses: $/.github/workflows/job-lint-other.yml with: timeout: 5 runner: ubuntu-26.04 @@ -66,7 +66,7 @@ jobs: # Verify we can actually build on all supported platforms, and a bunch of architectures build-for-go: name: "build for${{ inputs.hack }}" - uses: ./.github/workflows/job-build.yml + uses: $/.github/workflows/job-build.yml strategy: fail-fast: false matrix: diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 227a81e6df9..d4a99e66426 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -16,7 +16,7 @@ jobs: test-unit: # Note: inputs.hack is undefined - its purpose is to prevent GitHub Actions from displaying all matrix variants as part of the name. name: "unit${{ inputs.hack }}" - uses: ./.github/workflows/job-test-unit.yml + uses: $/.github/workflows/job-test-unit.yml strategy: fail-fast: false matrix: @@ -42,7 +42,7 @@ jobs: # This job builds the dependency target of the test-image for all supported architectures and cache it in GHA build-dependencies: name: "dependencies${{ inputs.hack }}" - uses: ./.github/workflows/job-test-dependencies.yml + uses: $/.github/workflows/job-test-dependencies.yml strategy: fail-fast: false matrix: @@ -60,7 +60,7 @@ jobs: test-integration-host-linux: name: "in-host${{ inputs.hack }}" - uses: ./.github/workflows/job-test-in-host.yml + uses: $/.github/workflows/job-test-in-host.yml needs: build-dependencies strategy: fail-fast: false @@ -129,7 +129,7 @@ jobs: test-integration-host: name: "in-host${{ inputs.hack }}" - uses: ./.github/workflows/job-test-in-host.yml + uses: $/.github/workflows/job-test-in-host.yml strategy: fail-fast: false matrix: From a4226e35b53b79f3af16b41335964a6b1fc4b53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:33:08 +0000 Subject: [PATCH 795/868] build(deps): bump zizmorcore/zizmor-action from 0.6.2 to 0.6.3 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.2 to 0.6.3. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/3dc1ecc9bcb9e94e9b2c709687979e1298497054...70fb788f84895a7701f5643d103d587e460b5c99) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index b37878cc110..38b672fd0f2 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,7 +92,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From f47617cb8b1f6f7fc61a36b48c87cbe447e82a77 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:10 +0900 Subject: [PATCH 796/868] update BuildKit (0.33.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 diff --git a/Dockerfile b/Dockerfile index 79b05a2ba98..733fb88f7ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.31.2@BINARY +ARG BUILDKIT_VERSION=v0.33.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 deleted file mode 100644 index 83bd5d424c4..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.2 +++ /dev/null @@ -1,2 +0,0 @@ -fbabdb72433a35f5bb646e4cd424bf8567e5d055710cf55840f7af2020640791 buildkit-v0.31.2.linux-amd64.tar.gz -41fba1eed480376934fa4c8177ddd7021036b5168a0eb8e7ab5eccdf75d47a05 buildkit-v0.31.2.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 new file mode 100644 index 00000000000..c8971ff5da8 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 @@ -0,0 +1,2 @@ +b6242896d343100808dcbe37565caf381e0a444a6a83d7255926bb1519248ead buildkit-v0.33.0.linux-amd64.tar.gz +e5acfb5929f967fde3b925ddb39f79fd481a0e96774c641fab3a0e83950d7bfa buildkit-v0.33.0.linux-arm64.tar.gz From cf1caff7fea682c4be3348657544eef7ac8eb6b1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:31 +0900 Subject: [PATCH 797/868] update imgcrypt (2.0.3) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 733fb88f7ee..8d658911e7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ ARG BUILDKIT_VERSION=v0.33.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption -ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda +ARG IMGCRYPT_VERSION=v2.0.3@3cd28929043ba2847633c32b806a6ccda8cd030f # Extra deps: Rootless ARG ROOTLESSKIT_VERSION=v3.2.0-beta.0@BINARY # Extra deps: bypass4netns From ef254df1e3c8886e5ebd9f55568e2eaa31179b31 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:31 +0900 Subject: [PATCH 798/868] update fuse-overlayfs (1.18) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 | 6 ------ Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.18 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 create mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.18 diff --git a/Dockerfile b/Dockerfile index 8d658911e7a..afc282bf404 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ ARG ROOTLESSKIT_VERSION=v3.2.0-beta.0@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS -ARG FUSE_OVERLAYFS_VERSION=v1.17@BINARY +ARG FUSE_OVERLAYFS_VERSION=v1.18@BINARY ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 deleted file mode 100644 index 16001445004..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 +++ /dev/null @@ -1,6 +0,0 @@ -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/fuse-overlayfs-v1.18 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.18 new file mode 100644 index 00000000000..5eea22b241b --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.18 @@ -0,0 +1,6 @@ +82fed736197b2a881a822e5357b488796f654e8371ce8573a1592331510a0133 fuse-overlayfs-aarch64 +3935996774a9c08fe6fa049f49f945d6c68aa343cf6bcb7017126584d59aef5c fuse-overlayfs-armv7l +bf3aec778615cf504679d9b984abe213236906a370343e8ba996a4356eaf81cf fuse-overlayfs-ppc64le +d505d4a0a3bcd80461936281a25ac7e4d75d40d92703e71d492f754b227d88d5 fuse-overlayfs-riscv64 +4245cd090d146836df53772dae870cdd17db8a78374a39875f815b2114a4cce1 fuse-overlayfs-s390x +56b0ae0aeb8abb308b068af2f137ed8d1bd239f4f27e21672ff0def861eea1e8 fuse-overlayfs-x86_64 From b585f02a01a8713f2d77d03de29c6e987403291f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:31 +0900 Subject: [PATCH 799/868] update Nydus (2.4.5) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index afc282bf404..3f0280be51e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ ARG GOMODJAIL_VERSION=v2.0.1@5924a4079d0f70459a10973f715238dc336478ea ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=26.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 -ARG NYDUS_VERSION=v2.4.3 +ARG NYDUS_VERSION=v2.4.5 ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 ARG KUBO_VERSION=v0.42.0 From 8cbdae99a995a414209c0819c027ec7548d54d90 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:31 +0900 Subject: [PATCH 800/868] update soci-snapshotter (0.15.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3f0280be51e..58f223c327d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=26.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 ARG NYDUS_VERSION=v2.4.5 -ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 +ARG SOCI_SNAPSHOTTER_VERSION=0.15.0 ARG KUBO_VERSION=v0.42.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 5319404a84638d62b91daf19cc96b8ef539339fd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:31 +0900 Subject: [PATCH 801/868] update kubo (0.43.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 58f223c327d..3238c2c3e6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG UBUNTU_VERSION=26.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 ARG NYDUS_VERSION=v2.4.5 ARG SOCI_SNAPSHOTTER_VERSION=0.15.0 -ARG KUBO_VERSION=v0.42.0 +ARG KUBO_VERSION=v0.43.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From f65eb8294bd723d0f395b5798f0d384be38fc980 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:57 +0900 Subject: [PATCH 802/868] update kind (0.33.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- hack/provisioning/kube/kind.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/provisioning/kube/kind.sh b/hack/provisioning/kube/kind.sh index bbdca38f2e5..19a05a0efa6 100755 --- a/hack/provisioning/kube/kind.sh +++ b/hack/provisioning/kube/kind.sh @@ -21,7 +21,7 @@ readonly root . "$root/../../scripts/lib.sh" GO_VERSION=1.26 -KIND_VERSION=v0.31.0 +KIND_VERSION=v0.33.0 CNI_PLUGINS_VERSION=v1.9.1 # shellcheck disable=SC2034 CNI_PLUGINS_SHA_AMD64=b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 From c2d05371b34c574da14ed7efe3fc317ff1bd6d7e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:42:57 +0900 Subject: [PATCH 803/868] update containerd 1.7 (1.7.35) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 227a81e6df9..d4e223aa063 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -52,7 +52,7 @@ jobs: - runner: ubuntu-26.04-arm # Additionally build for old containerd on amd - runner: ubuntu-26.04 - containerd-version: v1.7.34 + containerd-version: v1.7.35 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -81,7 +81,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.34 + containerd-version: v1.7.35 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-26.04 @@ -103,7 +103,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.34 + containerd-version: v1.7.35 # ipv6 - runner: ubuntu-26.04 target: rootful From 9b6ea04065c58b3da255eb985e4e66f0eeff863b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 14:18:25 +0900 Subject: [PATCH 804/868] update containerd for Windows (2.4.0-beta.0) Aligns the Windows CI with the containerd version pinned in the Dockerfile. Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index d4e223aa063..b4cf4d68ebf 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -154,8 +154,5 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.1 docker-version: 5:29.6.1-1~ubuntu.26.04~resolute - # 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.5 - windows-containerd-sha: 8724c3a873b4984f5ee092c8f15c1a98ebbb0f968106cf8f5849ea100f3a0236 + windows-containerd-version: 2.4.0-beta.0 + windows-containerd-sha: ae0df8baf1556a292da69b185d4912ddb955ecc2e47d1e0b754233b2df93acc9 From 77ad25bd1d8a94a5bddbe2ec77299b9ff654ce2a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:44:05 +0900 Subject: [PATCH 805/868] update Windows CNI plugins (0.3.3) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- hack/provisioning/windows/cni.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index b4cf4d68ebf..efa9269ab62 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -35,7 +35,7 @@ jobs: # Windows routinely go over 5 minutes timeout: 10 go-version: 1.26 - windows-cni-version: v0.3.1 + windows-cni-version: v0.3.3 linux-cni-version: v1.7.1 linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 @@ -152,7 +152,7 @@ jobs: # https://github.com/containerd/containerd/issues/13254 no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 - windows-cni-version: v0.3.1 + windows-cni-version: v0.3.3 docker-version: 5:29.6.1-1~ubuntu.26.04~resolute windows-containerd-version: 2.4.0-beta.0 windows-containerd-sha: ae0df8baf1556a292da69b185d4912ddb955ecc2e47d1e0b754233b2df93acc9 diff --git a/hack/provisioning/windows/cni.sh b/hack/provisioning/windows/cni.sh index 2c1b90ce40b..964bcf52784 100755 --- a/hack/provisioning/windows/cni.sh +++ b/hack/provisioning/windows/cni.sh @@ -18,7 +18,7 @@ set -o errexit -o errtrace -o functrace -o nounset -o pipefail -WINCNI_VERSION="${WINCNI_VERSION:-v0.3.1}" +WINCNI_VERSION="${WINCNI_VERSION:-v0.3.3}" git config --global advice.detachedHead false From bd0cfc76001ede036fc7b096fdebbcf9f4435831 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:44:05 +0900 Subject: [PATCH 806/868] update CNI plugins used by the unit tests (1.9.1) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index efa9269ab62..de53eda6eae 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -36,8 +36,8 @@ jobs: timeout: 10 go-version: 1.26 windows-cni-version: v0.3.3 - linux-cni-version: v1.7.1 - linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 + linux-cni-version: v1.9.1 + linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 # This job builds the dependency target of the test-image for all supported architectures and cache it in GHA build-dependencies: From 4bc3fd35af2c9f5517951dea83107b223f112a43 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:44:05 +0900 Subject: [PATCH 807/868] update Docker (29.8.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index de53eda6eae..3318d234247 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -153,6 +153,6 @@ jobs: no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.3 - docker-version: 5:29.6.1-1~ubuntu.26.04~resolute + docker-version: 5:29.8.0-1~ubuntu.26.04~resolute windows-containerd-version: 2.4.0-beta.0 windows-containerd-sha: ae0df8baf1556a292da69b185d4912ddb955ecc2e47d1e0b754233b2df93acc9 From f6d5b798c9ad2c980b27818f820c0f307b6c12eb Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 11:52:11 +0900 Subject: [PATCH 808/868] update golangci-lint (2.13.2); silence new errors Some rules and linters are disabled to silence the errors introduced in this release of golangci-lint. The gofumpt `extra-rules` setting is deprecated in favor of the `extra` map; it is spelled out here as the three rules it used to imply, and the reformatting the new gofumpt wants in mod/tigron is applied. Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- .golangci.yml | 26 ++++++++++++++++++++++ Makefile | 4 ++-- mod/tigron/.golangci.yml | 15 ++++++++++++- mod/tigron/Makefile | 4 ++-- mod/tigron/internal/assertive/assertive.go | 12 ++++++---- mod/tigron/internal/logger/logger.go | 4 +++- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ec60c924491..3523c5ca349 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -51,6 +51,10 @@ linters: - pattern: ^os\.ReadFile.*$ pkg: github.com/containerd/nerdctl/v2/pkg msg: use filesystem.ReadFile instead of os.ReadFile + govet: + disable: + # 1 occurrence. Suggests replacing the legacy `reflect.Ptr` alias by `reflect.Pointer`. + - inline staticcheck: checks: # Below is the default set @@ -69,11 +73,18 @@ linters: # 10 occurrences. # Convert if/else-if chain to tagged switch https://staticcheck.dev/docs/checks#QF1003 - "-QF1003" + # 4 occurrences. + # Use fmt.Fprintf instead of WriteString(fmt.Sprintf(...)) https://staticcheck.dev/docs/checks#QF1012 + - "-QF1012" ##### These have been vetted to be disabled. # 55 occurrences. Omit embedded fields from selector expression https://staticcheck.dev/docs/checks#QF1008 # Usefulness is questionable. - "-QF1008" + # 19 occurrences, on non-Linux GOOS only, and all false positives: the rootlessutil + # stubs for those platforms unconditionally return an error, so the callers' error + # checks do look "always true". https://staticcheck.dev/docs/checks#SA4023 + - "-SA4023" revive: enable-all-rules: true @@ -137,6 +148,21 @@ linters: - name: var-naming # 1 occurrence. disabled: true + - name: use-slices-sort + # 19 occurrences. Would replace sort.Strings/sort.Slice by the slices package. + disabled: true + - name: identical-switch-branches + # 7 occurrences. + disabled: true + - name: use-waitgroup-go + # 4 occurrences. Would replace wg.Add()/go/wg.Done() by wg.Go(). + disabled: true + - name: identical-ifelseif-branches + # 1 occurrence. + disabled: true + - name: package-naming + # 1 occurrence (pkg/api/types). Renaming a public package is not worth the churn. + disabled: true ##### P2: nice to have. - name: max-public-structs diff --git a/Makefile b/Makefile index b738a73adb4..a4801fb20b3 100644 --- a/Makefile +++ b/Makefile @@ -264,7 +264,7 @@ endif ########################## install-dev-tools: $(call title, $@) - # golangci: v2.4.0 (2025-08-14) + # golangci: v2.13.2 (2026-08-27) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) @@ -272,7 +272,7 @@ install-dev-tools: # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@43d03392d7dc3746fa776dbddd66dfcccff70651 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d diff --git a/mod/tigron/.golangci.yml b/mod/tigron/.golangci.yml index f58a2d37221..72c3addca54 100644 --- a/mod/tigron/.golangci.yml +++ b/mod/tigron/.golangci.yml @@ -36,6 +36,7 @@ linters: # These are the linters that we know we do not want - cyclop # provided by revive - exhaustruct # does not serve much of a purpose + - exhaustruct_v5 # ibid (exhaustruct was renamed in golangci-lint v2.13) - errcheck # provided by revive - errchkjson # forces handling of json err (eg: prevents _), which is too much - forcetypeassert # provided by revive @@ -55,8 +56,10 @@ linters: - testifylint # no testify - zerologlint # no zerolog - funcorder + - modernize # 8 occurrences. New in golangci-lint v2.13. Not reviewed yet. - noctx - noinlineerr + - perfsprint # 1 occurrence. New in golangci-lint v2.13. Not reviewed yet. - wsl_v5 settings: interfacebloat: @@ -101,6 +104,12 @@ linters: disabled: true - name: var-naming disabled: true + - name: identical-switch-branches + # 2 occurrences. + disabled: true + - name: package-naming + # 1 occurrence (utils). Renaming a public package is not worth the churn. + disabled: true depguard: rules: main: @@ -137,7 +146,11 @@ formatters: no-prefix-comments: true custom-order: true gofumpt: - extra-rules: true + # Formerly `extra-rules: true`, which is deprecated and covers exactly these three. + extra: + group-params: true + clothe-returns: true + balance-calls: true golines: max-len: 120 tab-len: 4 diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index 3613ade3ba8..51a3feba08a 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -164,14 +164,14 @@ up: ########################## install-dev-tools: $(call title, $@) - # golangci: v2.4.0 (2025-08-14) + # golangci: v2.13.2 (2026-08-27) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 # Issue: https://github.com/google/go-licenses/issues/312 @cd $(MAKEFILE_DIR) \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@43d03392d7dc3746fa776dbddd66dfcccff70651 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a diff --git a/mod/tigron/internal/assertive/assertive.go b/mod/tigron/internal/assertive/assertive.go index bc9b2df4563..b7bfb3a4ca6 100644 --- a/mod/tigron/internal/assertive/assertive.go +++ b/mod/tigron/internal/assertive/assertive.go @@ -77,7 +77,8 @@ func Contains(testing tig.T, actual, contains string, msg ...string) { strings.Contains(actual, contains), actual, fmt.Sprintf("~= `%v`", contains), - msg...) + msg..., + ) } // DoesNotContain fails a test if the actual string contains the other string. @@ -89,7 +90,8 @@ func DoesNotContain(testing tig.T, actual, contains string, msg ...string) { !strings.Contains(actual, contains), actual, fmt.Sprintf("! ~= `%v`", contains), - msg...) + msg..., + ) } // HasSuffix fails a test if the string does not end with suffix. @@ -101,7 +103,8 @@ func HasSuffix(testing tig.T, actual, suffix string, msg ...string) { strings.HasSuffix(actual, suffix), actual, fmt.Sprintf("`%v` $", suffix), - msg...) + msg..., + ) } // HasPrefix fails a test if the string does not start with prefix. @@ -113,7 +116,8 @@ func HasPrefix(testing tig.T, actual, prefix string, msg ...string) { strings.HasPrefix(actual, prefix), actual, fmt.Sprintf("^ `%v`", prefix), - msg...) + msg..., + ) } // Match fails a test if the string does not match the regexp. diff --git a/mod/tigron/internal/logger/logger.go b/mod/tigron/internal/logger/logger.go index a9be51a296c..85cd74c099e 100644 --- a/mod/tigron/internal/logger/logger.go +++ b/mod/tigron/internal/logger/logger.go @@ -47,7 +47,9 @@ func (cl *ConcreteLogger) Log(args ...any) { cl.wrappedLog.Log( append( append([]any{"[" + time.Now().Format(time.RFC3339) + "]"}, cl.meta...), - args...)...) + args..., + )..., + ) } } From 3467ca3269e221fd9a10bdee116f86125b7dda54 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 12:11:16 +0900 Subject: [PATCH 809/868] update gotestsum (1.13.0) Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a4801fb20b3..68e011a01e5 100644 --- a/Makefile +++ b/Makefile @@ -267,6 +267,7 @@ install-dev-tools: # golangci: v2.13.2 (2026-08-27) # git-validation: main (2025-02-25) # ltag: main (2025-03-04) + # gotestsum: v1.13.0 (2025-09-11) # go-licenses: v2.0.0-alpha.1 (2024-06-27) # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 # Issue: https://github.com/google/go-licenses/issues/312 @@ -275,7 +276,7 @@ install-dev-tools: && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ - && go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d + && go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f # gomodjail: v2.0.1 (2026-09-09) # Not installed on Windows hosts: gomodjail does not build there, as its dynamic mode # is compiled in unconditionally (https://github.com/AkihiroSuda/gomodjail) From c074348f94a0f727259e8bc4042dc9be517ad839 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 14:25:58 +0900 Subject: [PATCH 810/868] Makefile: refer to git-validation by its release (v1.2.2) The pinned commit is the v1.2.2 tag, not an arbitrary main commit. Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Makefile | 2 +- mod/tigron/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 68e011a01e5..91d54fcae60 100644 --- a/Makefile +++ b/Makefile @@ -265,7 +265,7 @@ endif install-dev-tools: $(call title, $@) # golangci: v2.13.2 (2026-08-27) - # git-validation: main (2025-02-25) + # git-validation: v1.2.2 (2025-02-26) # ltag: main (2025-03-04) # gotestsum: v1.13.0 (2025-09-11) # go-licenses: v2.0.0-alpha.1 (2024-06-27) diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index 51a3feba08a..63d1a4b5ff2 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -165,7 +165,7 @@ up: install-dev-tools: $(call title, $@) # golangci: v2.13.2 (2026-08-27) - # git-validation: main (2025-02-25) + # git-validation: v1.2.2 (2025-02-26) # ltag: main (2025-03-04) # go-licenses: v2.0.0-alpha.1 (2024-06-27) # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 From 492793e8437bd5b1ee79e738f2fae58160b3f8d3 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 14:28:24 +0900 Subject: [PATCH 811/868] update go-licenses (2.0.1) Move back to upstream google/go-licenses: the fork existed only to carry a dependency bump for Go 1.25 compatibility, and upstream v2.0.1 has since picked that up. Verified to report the same results as the fork on both modules. Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Makefile | 6 ++---- mod/tigron/Makefile | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 91d54fcae60..1469805d53a 100644 --- a/Makefile +++ b/Makefile @@ -268,11 +268,9 @@ install-dev-tools: # git-validation: v1.2.2 (2025-02-26) # ltag: main (2025-03-04) # gotestsum: v1.13.0 (2025-09-11) - # go-licenses: v2.0.0-alpha.1 (2024-06-27) - # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 - # Issue: https://github.com/google/go-licenses/issues/312 + # go-licenses: v2.0.1 (2025-09-08) @cd $(MAKEFILE_DIR) \ - && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a \ + && go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index 63d1a4b5ff2..31b556f8a31 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -167,14 +167,12 @@ install-dev-tools: # golangci: v2.13.2 (2026-08-27) # git-validation: v1.2.2 (2025-02-26) # ltag: main (2025-03-04) - # go-licenses: v2.0.0-alpha.1 (2024-06-27) - # stubbing go-licenses with dependency upgrade due to non-compatibility with golang 1.25rc1 - # Issue: https://github.com/google/go-licenses/issues/312 + # go-licenses: v2.0.1 (2025-09-08) @cd $(MAKEFILE_DIR) \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ - && go install github.com/Shubhranshu153/go-licenses/v2@f8c503d1357dffb6c97ed3b94e912ab294dde24a + && go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) From c20d41f93f9b35ea79a06d28a9c9ecb9a4e48feb Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 10 Sep 2026 14:28:24 +0900 Subject: [PATCH 812/868] Makefile: refer to ltag by its release (v0.3.0) The pinned commit is the v0.3.0 tag, not an arbitrary main commit. Assisted-by: Claude Opus 5 Signed-off-by: Akihiro Suda --- Makefile | 2 +- mod/tigron/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1469805d53a..f717f1ad0e7 100644 --- a/Makefile +++ b/Makefile @@ -266,7 +266,7 @@ install-dev-tools: $(call title, $@) # golangci: v2.13.2 (2026-08-27) # git-validation: v1.2.2 (2025-02-26) - # ltag: main (2025-03-04) + # ltag: v0.3.0 (2025-03-04) # gotestsum: v1.13.0 (2025-09-11) # go-licenses: v2.0.1 (2025-09-08) @cd $(MAKEFILE_DIR) \ diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index 31b556f8a31..3efe9f18681 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -166,7 +166,7 @@ install-dev-tools: $(call title, $@) # golangci: v2.13.2 (2026-08-27) # git-validation: v1.2.2 (2025-02-26) - # ltag: main (2025-03-04) + # ltag: v0.3.0 (2025-03-04) # go-licenses: v2.0.1 (2025-09-08) @cd $(MAKEFILE_DIR) \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ From 575115cd9973e90aff42a0fecfb10ee53373bbcd Mon Sep 17 00:00:00 2001 From: Filip Plevjak Date: Thu, 10 Sep 2026 12:34:00 +0200 Subject: [PATCH 813/868] Makefile: fix lint-licenses failing on MPL-2.0 (filepath-securejoin) (#5199) Added --ignore github.com/cyphar/filepath-securejoin in Makefile due to its MPL-2.0 license breaking make lint-licenses. Fixes #5199 Signed-off-by: Filip Plevjak --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index b738a73adb4..4959e276ba8 100644 --- a/Makefile +++ b/Makefile @@ -204,12 +204,16 @@ lint-gomodjail-all: # https://github.com/google/go-licenses/issues/186 # This is impacting gotest.tools # FIXME: go-base36 is multi-license (MIT/Apache), using a custom boilerplate file that go-licenses fails to understand +# filepath-securejoin is MPL-2.0, which is not in the allowed list, but is explicitly allowed by CNCF: +# https://github.com/cncf/foundation/issues/1154 +# It is a transitive dependency (pulled in by go-selinux) that cannot currently be removed. lint-licenses: $(call title, $@: $(GOOS)) @cd $(MAKEFILE_DIR) \ && go-licenses check --include_tests --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib \ --ignore gotest.tools \ --ignore github.com/multiformats/go-base36 \ + --ignore github.com/cyphar/filepath-securejoin \ ./... $(call footer, $@) From 29046b809458183603a536affa41ef2e8869655f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 11 Sep 2026 02:50:56 +0900 Subject: [PATCH 814/868] mod/soigneur: reusable flaky test dashboard A test that fails once in a while, in one of the dozen environments an integration suite runs in, is only visible to whoever happens to look at that one job. Nothing accumulates, so nothing gets fixed. Soigneur is a dashboard for those, with no infrastructure behind it: the archived logs of the analyzed runs are the storage. A soigneur is the keeper who tends the animals, and this one keeps an eye on what Tigron runs: it has two halves, and one marker line per test is the contract between them (see lib.sh): - `flaky-annotate.sh` classifies the tests of a gotestsum json file at the end of a test job, and writes them to the job log as "flaky-test-dashboard: failing TestFoo" (failed, and never passed, even on retry) or "flaky-test-dashboard: flaky TestFoo" (failed, then passed when `--rerun-fails` retried it: flaky beyond a doubt). Both classes are also emitted as annotations, for the humans reading a pull request. - `flaky-report.sh` lists the workflow runs of a branch and, for each of them, downloads the archived logs of all of its jobs in a single request and reads the markers back; `flaky-report.jq` renders the aggregate as markdown; `flaky-issue.sh` publishes it to the one issue whose number it was given, rewriting its description and posting the digest as a comment. It never opens an issue, which is the point: there is exactly one dashboard, and it is the one that was named. Occurrences are deduplicated per (commit, job configuration, kind, top-level test), so that a single bad job execution counts once, and subtests do not inflate the ranking of their parent. Collecting is deliberately per run, and not per job: a run's logs come as one zip, so a week of a busy branch costs about 200 API requests and 50 MB, against the 1000 requests per hour and per repository that the GITHUB_TOKEN of a workflow gets. Reading the annotations of each job through the checks API instead would cost some 800 requests for the same week. The markers are what gets collected, rather than those same annotations, because the runner mangles a multi-line annotation into orphaned fragments in the log. GitHub's job summaries, which do aggregate all of this per run in the web UI, cannot be used at all: they are not exposed by any API, and are rendered from signed attachments only a browser session can mint. The logs of the runs that predate the markers only hold the readable block that flaky-annotate.sh prints at the end of a job, so that block is read as a fallback, and a dashboard has history from the start rather than after a full window. `action.yml` wraps the collecting half as a composite action, so that other projects can use it as-is: everything project-specific is an input, including the caveats rendered in the report's methodology. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- mod/soigneur/LICENSE | 202 +++++++++++++++++++ mod/soigneur/README.md | 188 ++++++++++++++++++ mod/soigneur/action.yml | 148 ++++++++++++++ mod/soigneur/flaky-annotate.sh | 134 +++++++++++++ mod/soigneur/flaky-issue.sh | 114 +++++++++++ mod/soigneur/flaky-report.jq | 255 ++++++++++++++++++++++++ mod/soigneur/flaky-report.sh | 344 +++++++++++++++++++++++++++++++++ mod/soigneur/lib.sh | 70 +++++++ 8 files changed, 1455 insertions(+) create mode 100644 mod/soigneur/LICENSE create mode 100644 mod/soigneur/README.md create mode 100644 mod/soigneur/action.yml create mode 100755 mod/soigneur/flaky-annotate.sh create mode 100755 mod/soigneur/flaky-issue.sh create mode 100644 mod/soigneur/flaky-report.jq create mode 100755 mod/soigneur/flaky-report.sh create mode 100644 mod/soigneur/lib.sh diff --git a/mod/soigneur/LICENSE b/mod/soigneur/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/mod/soigneur/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/mod/soigneur/README.md b/mod/soigneur/README.md new file mode 100644 index 00000000000..5e2860632f5 --- /dev/null +++ b/mod/soigneur/README.md @@ -0,0 +1,188 @@ +# Soigneur + +A flaky test is a test that fails, then passes, without anything having changed. The first +problem with those is *knowing* about them: a failure that nobody looks at twice is a failure +that stays. + +Soigneur keeps a **flaky test dashboard**: a single, long-lived GitHub issue holding what the CI +reported over the last week. It runs no test of its own - it watches the ones that do, and says +which of them need care. A *soigneur* is the keeper who tends the animals; the test framework +next door is [Tigron](../tigron). + +Two halves, with no infrastructure behind them: + +- **Emit**: at the end of a test job, [`flaky-annotate.sh`](flaky-annotate.sh) turns a + `gotestsum` json file into one marker line per test in the job log - the tests that failed, and + the tests that failed and then *passed on retry* - plus the matching annotations, for the + humans reading the pull request. +- **Collect**: once a week, [`flaky-report.sh`](flaky-report.sh) lists the workflow runs of a + branch and, for each of them, downloads the archived logs of all of its jobs in a single + request and reads the markers back; then [`flaky-issue.sh`](flaky-issue.sh) publishes the + aggregate to a single, long-lived issue. + +No database, no test result artifact, and no server: the logs of the analyzed runs *are* the +storage. The trade-off is the window, which is limited by the retention of the logs (90 days, by +default). + +## What the dashboard reports + +For the analyzed window: + +- which tests were reported as failing, how often, in how many different job configurations, and + when they were last seen; +- which of those failures were **recovered on retry**, in other words: proven flaky; +- which job configurations failed, and how often - a job that fails without any test failing is + usually an environment or a timeout problem, and that is worth knowing too. + +Occurrences are counted per (commit × job configuration × outcome × top-level test), so a single +bad job execution counts once, and subtests do not inflate the ranking of their parent. + +## Emitting + +The two halves talk to each other through one marker line per test, written to the job log: + +``` +flaky-test-dashboard: failing TestFoo +flaky-test-dashboard: flaky TestBar/subtest +``` + +| Class | Meaning | +| --- | --- | +| `failing` | Failed, and never passed - even on retry. Flaky, or simply broken. | +| `flaky` | Failed, then passed when retried. Flaky beyond a doubt. | + +The same two classes are also emitted as GitHub Actions annotations (`Failing tests` as an +error, `Flaky tests` as a warning), which is what shows up on a pull request. The collector does +not read those: annotations cost one API request per job, markers cost none, since the logs of a +whole run come in a single archive. + +`flaky-annotate.sh` derives both classes from a `gotestsum` json file, so the retry information +is only there if the tests were actually retried (`--rerun-fails`): + +```yaml +- name: "Run: tests" + run: | + gotestsum \ + --jsonfile=/tmp/tests.json \ + --rerun-fails=2 \ + --post-run-command ./mod/soigneur/flaky-annotate.sh \ + -- ./... +``` + +It can also be called as a plain step (`flaky-annotate.sh /tmp/tests.json`), and it writes the +two lists to `SOIGNEUR_FAILING_OUT` / `SOIGNEUR_SOIGNEUR_OUT` for callers that render their own job +summary. Projects that do not use `gotestsum` only have to print those marker lines themselves, +one per test, to be collected. The marker and the annotation titles are defined in +[`lib.sh`](lib.sh). + +## Collecting, with the action + +```yaml +name: flaky-test-dashboard + +on: + schedule: + - cron: "0 7 * * 1" # every Monday + workflow_dispatch: + +permissions: + contents: read + +jobs: + dashboard: + runs-on: ubuntu-latest + permissions: + contents: read # fetch the action from this repository + actions: read # list the workflow runs, and download their logs + issues: write # create, and update, the dashboard issue + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: containerd/nerdctl/mod/soigneur@main + with: + branch: main + days: 7 + issue-number: "1234" # the issue holding the dashboard + # Optional: the workflows that actually run tests. Without it, the logs of every + # workflow run of the window are downloaded, which only costs time. + workflows: "test.yml,nightly.yml" +``` + +Open the dashboard issue by hand once, and name its number: its description is rewritten with the +latest report, and the digest is posted as a comment, so that the subscribers get notified without +the issue growing a copy of every report. Soigneur never opens an issue itself, which is the +point - there is exactly one dashboard, and it is the one you named. To start a fresh one, open it +and change the number. + +### Inputs + +All of them are optional. See [`action.yml`](action.yml) for the defaults. + +| Input | Description | +| --- | --- | +| `token` | Token used to read the run logs and write the issue. Needs `actions: read` and `issues: write`. | +| `repository`, `branch`, `days`, `event` | What to report on. `event` defaults to `push`, the post-merge signal. | +| `workflows` | Only download the logs of these workflow files. | +| `max-runs`, `parallel` | Bound the number, and the concurrency, of the runs collected. | +| `max-tests`, `max-links` | How much detail the report carries. | +| `publish`, `comment` | Turn off the issue update, or just the digest comment (dry run). | +| `issue-number` | Which issue is *the* dashboard. Required. | +| `summary` | Whether to also write the report to the run summary. | +| `docs-url`, `footer-notes` | Project-specific pointers and caveats, rendered in the report. | + +### Outputs + +`report-file`, `json-file`, `digest`, and `issue-url`. The json is the aggregate the markdown was +rendered from, for projects that want to slice it differently: + +```bash +jq '.tests[] | select(.flaky > 0) | .test' "$JSON_FILE" +``` + +## Running it locally + +The collector only needs `gh` (authenticated), `jq`, `unzip`, and read access to the repository: + +```bash +# The last 7 days of main, as markdown, on stdout +SOIGNEUR_REPO=containerd/nerdctl ./flaky-report.sh + +# A different window or branch +SOIGNEUR_DAYS=30 SOIGNEUR_BRANCH=release/2.2 ./flaky-report.sh + +# Keep (and re-read) the API responses, which makes iterating on the report almost free +SOIGNEUR_WORKDIR=/tmp/flaky ./flaky-report.sh +``` + +Every knob is an environment variable, documented at the top of each script. + +## Cost + +Collecting costs two API requests per workflow run - one for the jobs, one for the log archive - +and the archive itself, which is about 1 MB for a run of twenty jobs. A busy week of a single +branch measures at roughly 100 runs, so 200 requests and 50 MB, against the 1000 requests per +hour and per repository that the `GITHUB_TOKEN` of a workflow gets. + +The per-job alternative, reading the annotations through the checks API, costs one request per +job execution instead - about 800 requests for the same week, uncomfortably close to that limit, +which is why the markers in the logs are what gets collected. The annotations remain, for the +humans. + +Note that the logs are the only thing that can be read back: GitHub's job summaries are not +exposed by any API, only rendered in the web UI from signed attachments. + +The downloaded logs are whole job logs, written to a temporary directory that is removed when the +report is done - unless `SOIGNEUR_WORKDIR` is set, in which case they stay there. GitHub masks the +registered secrets in the logs it archives, but a log still holds everything else the run +printed, so do not upload a workdir as an artifact. + +The test names, too, come out of those logs, which means they are only as trustworthy as what the +tests printed: a test that prints a line shaped like a marker gets it collected. The collector +therefore keeps only the names that can plausibly be a Go test name, and the renderer escapes +what ends up in the tables, so that a crafted name cannot turn into a link, or into markup, in +the issue. + +## License + +Apache License 2.0. See [LICENSE](LICENSE). diff --git a/mod/soigneur/action.yml b/mod/soigneur/action.yml new file mode 100644 index 00000000000..9fe61ceb5f2 --- /dev/null +++ b/mod/soigneur/action.yml @@ -0,0 +1,148 @@ +# Soigneur: a composite action that collects the test failures a repository's CI reported over a +# period of time, and publishes them to a single, long-lived GitHub issue: the flaky test +# dashboard. +# +# The data source is the archived log of every workflow run of the window, so the token needs +# `actions: read` (and `issues: write` to publish). See README.md for the marker lines it expects +# the test jobs to write (flaky-annotate.sh). +name: "Soigneur" +description: "Keep a flaky test dashboard: the failures the CI reported, in a single issue" +author: "The containerd Authors" + +branding: + icon: "activity" + color: "orange" + +inputs: + token: + description: "Token used to read the run logs and to write the issue (actions:read, issues:write)" + required: false + default: ${{ github.token }} + repository: + description: "Repository to report on" + required: false + default: ${{ github.repository }} + branch: + description: "Branch to report on" + required: false + default: "main" + days: + description: "Size of the window to report on, in days" + required: false + default: "7" + event: + description: "Only report on the runs of that event, empty for all" + required: false + default: "push" + workflows: + description: "Only report on the runs of these workflow files, comma-separated, empty for all" + required: false + default: "" + max-runs: + description: "Maximum number of workflow runs to analyze, which bounds the work" + required: false + default: "200" + max-tests: + description: "Maximum number of tests to detail" + required: false + default: "25" + max-links: + description: "Maximum number of links per test" + required: false + default: "5" + parallel: + description: "Number of concurrent API requests" + required: false + default: "8" + publish: + description: "Whether to write the report to the dashboard issue" + required: false + default: "true" + issue-number: + description: "The dashboard issue, as a number: open it by hand once, then name it here" + required: true + comment: + description: "Whether to post the digest as a comment, to notify the subscribers" + required: false + default: "true" + summary: + description: "Whether to write the report to the run summary" + required: false + default: "true" + docs-url: + description: "Where the project documents its flaky tests, linked from the report" + required: false + default: "" + footer-notes: + description: "Extra markdown for the report's methodology section, for project-specific caveats" + required: false + default: "" + +outputs: + report-file: + description: "Path of the markdown report" + value: ${{ steps.collect.outputs.report-file }} + json-file: + description: "Path of the aggregated data, as json" + value: ${{ steps.collect.outputs.json-file }} + digest: + description: "One-line summary of the report" + value: ${{ steps.collect.outputs.digest }} + issue-url: + description: "URL of the dashboard issue, empty when publishing is disabled" + value: ${{ steps.publish.outputs.issue-url }} + +runs: + using: composite + steps: + - name: "Collect" + id: collect + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + SOIGNEUR_REPO: ${{ inputs.repository }} + SOIGNEUR_BRANCH: ${{ inputs.branch }} + SOIGNEUR_DAYS: ${{ inputs.days }} + SOIGNEUR_EVENT: ${{ inputs.event }} + SOIGNEUR_WORKFLOWS: ${{ inputs.workflows }} + SOIGNEUR_MAX_RUNS: ${{ inputs.max-runs }} + SOIGNEUR_MAX_TESTS: ${{ inputs.max-tests }} + SOIGNEUR_MAX_LINKS: ${{ inputs.max-links }} + SOIGNEUR_PARALLEL: ${{ inputs.parallel }} + SOIGNEUR_DOCS_URL: ${{ inputs.docs-url }} + SOIGNEUR_FOOTER: ${{ inputs.footer-notes }} + SOIGNEUR_SUMMARY: ${{ inputs.summary }} + SOIGNEUR_RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + ACTION_PATH: ${{ github.action_path }} + run: | + report="$RUNNER_TEMP"/flaky-report.md + digest="$RUNNER_TEMP"/flaky-digest.txt + json="$RUNNER_TEMP"/flaky-report.json + + SOIGNEUR_DIGEST_OUT="$digest" SOIGNEUR_JSON_OUT="$json" \ + "$ACTION_PATH"/flaky-report.sh > "$report" + + { + echo "report-file=$report" + echo "json-file=$json" + echo "digest=$(head -n 1 "$digest")" + } >> "$GITHUB_OUTPUT" + + case "${SOIGNEUR_SUMMARY,,}" in + 1 | t | true | y | yes | on) cat "$report" >> "$GITHUB_STEP_SUMMARY" ;; + esac + + - name: "Publish" + id: publish + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + SOIGNEUR_REPO: ${{ inputs.repository }} + SOIGNEUR_PUBLISH: ${{ inputs.publish }} + SOIGNEUR_ISSUE_NUMBER: ${{ inputs.issue-number }} + SOIGNEUR_ISSUE_COMMENT: ${{ inputs.comment }} + ACTION_PATH: ${{ github.action_path }} + REPORT_FILE: ${{ steps.collect.outputs.report-file }} + run: | + url="$("$ACTION_PATH"/flaky-issue.sh "$REPORT_FILE" "$RUNNER_TEMP"/flaky-digest.txt)" + echo "issue-url=$url" >> "$GITHUB_OUTPUT" diff --git a/mod/soigneur/flaky-annotate.sh b/mod/soigneur/flaky-annotate.sh new file mode 100755 index 00000000000..3a451ac63bf --- /dev/null +++ b/mod/soigneur/flaky-annotate.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Classifies the tests of a gotestsum json file, and reports them to the flaky test dashboard. +# This is the *emitting* half of the dashboard: call it at the end of a test job +# (`gotestsum --post-run-command`, or a plain step). Two classes are reported: +# - "failing": the tests that failed and never passed, even on retry. +# - "flaky": the tests that failed, then passed on retry. Flaky beyond a doubt. Those only exist +# when the tests were retried, which gotestsum does with `--rerun-fails`. +# +# Each class is reported twice, for two different readers: +# - as one marker line per test in the job log (`flaky-test-dashboard: failing TestFoo`), which is +# what flaky-report.sh collects, from the archived logs of the workflow run; +# - as a GitHub Actions annotation, which is what a human sees on a pull request. +# Both are defined in lib.sh, and are a contract with the collector. +# +# Usage: +# gotestsum --jsonfile=/tmp/test.log --rerun-fails=2 --post-run-command "flaky-annotate.sh" ... +# flaky-annotate.sh [gotestsum-jsonfile] # defaults to $GOTESTSUM_JSONFILE +# +# Environment: +# SOIGNEUR_FAILING_OUT if set, the list of consistently failing tests is written to that file +# SOIGNEUR_SOIGNEUR_OUT if set, the list of tests that recovered on retry is written to that file +# (both are meant for callers that render their own step summary) +# SOIGNEUR_QUIET if true, only the annotations are emitted, not the readable blocks + +set -o errexit -o errtrace -o functrace -o nounset -o pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" +readonly root + +# shellcheck source-path=SCRIPTDIR +. "$root"/lib.sh + +: "${SOIGNEUR_FAILING_OUT:=}" +: "${SOIGNEUR_SOIGNEUR_OUT:=}" +: "${SOIGNEUR_QUIET:=false}" + +# Prints a list where a human will look for it: at the end of the job log, past the noise of the +# run itself. The block is also the format that flaky-report.sh falls back to for the logs that +# predate the markers, hence the closing delimiter, which is what tells it where the list ends. +soigneur::block(){ + local title="$1" + local list="$2" + local rule + + rule="$(printf '%*s' "$((${#title} + 8))" '')" + printf '\n=== %s ===\n%s\n%s\n' "$title" "$list" "${rule// /=}" +} + +# Writes one marker line per test, inside a collapsed group so that the job log stays readable. +soigneur::mark(){ + local kind="$1" + local list="$2" + + echo "::group::$soigneur_marker $kind" + while read -r test; do + [ "$test" == "" ] || printf "%s %s %s\n" "$soigneur_marker" "$kind" "$test" + done <<< "$list" + echo "::endgroup::" +} + +# Emits a GitHub Actions annotation. Multi-line messages need their newlines as %0A. +soigneur::annotate(){ + local level="$1" + local title="$2" + local msg="$3" + + echo "::$level title=$title::${msg//$'\n'/%0A}" +} + +# Prints the tests of one class, using the outcomes of the whole run: a test that failed and then +# passed was retried and recovered ("flaky"); one that only ever failed is "failing". +soigneur::classify(){ + local want="$1" + local outcomes="$2" + + printf "%s\n" "$outcomes" | awk -F'\t' -v want="$want" ' + $1 == "fail" { failed[$2] = 1 } + $1 == "pass" { passed[$2] = 1 } + END { + for (t in failed) { + if (((t in passed) ? "flaky" : "failing") == want) print t + } + } + ' | sort +} + +soigneur::main(){ + local jsonfile="${1:-${GOTESTSUM_JSONFILE:-}}" + local outcomes failing flaky + + [ "$jsonfile" != "" ] || { + echo "usage: $0 " >&2 + return 1 + } + [ -r "$jsonfile" ] || { + echo "error: cannot read $jsonfile" >&2 + return 1 + } + + outcomes="$(jq -rc 'select(.Test) | select(.Action == "fail" or .Action == "pass") | [.Action, .Test] | @tsv' < "$jsonfile")" + failing="$(soigneur::classify failing "$outcomes")" + flaky="$(soigneur::classify flaky "$outcomes")" + + [ "$SOIGNEUR_FAILING_OUT" == "" ] || printf "%s\n" "$failing" > "$SOIGNEUR_FAILING_OUT" + [ "$SOIGNEUR_SOIGNEUR_OUT" == "" ] || printf "%s\n" "$flaky" > "$SOIGNEUR_SOIGNEUR_OUT" + + if [ "$failing" != "" ]; then + soigneur::bool "$SOIGNEUR_QUIET" || soigneur::block "$soigneur_title_failing" "$failing" + soigneur::mark "failing" "$failing" + soigneur::annotate "error" "$soigneur_title_failing" "$failing" + fi + + if [ "$flaky" != "" ]; then + soigneur::bool "$SOIGNEUR_QUIET" || soigneur::block "$soigneur_title_flaky (passed on retry)" "$flaky" + soigneur::mark "flaky" "$flaky" + soigneur::annotate "warning" "$soigneur_title_flaky" "$flaky" + fi +} + +soigneur::main "$@" diff --git a/mod/soigneur/flaky-issue.sh b/mod/soigneur/flaky-issue.sh new file mode 100755 index 00000000000..ad9d4200ccd --- /dev/null +++ b/mod/soigneur/flaky-issue.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Publishes the report generated by flaky-report.sh to *the* flaky test dashboard issue. +# +# The dashboard lives in one issue, created by hand once and named by its number: the description +# is rewritten with the latest report, and a one-line digest is posted as a comment, so that the +# subscribers get notified without the issue growing a full copy of every report. Nothing here +# ever opens an issue, which is the point: there is exactly one, and it is the one you named. +# +# The URL of the issue is printed on stdout; everything else goes to stderr. +# +# Usage: +# ./flaky-issue.sh [digest.txt] +# +# Environment: +# SOIGNEUR_REPO repository to publish to (default: $GITHUB_REPOSITORY) +# SOIGNEUR_PUBLISH set to false to do nothing at all (default: true) +# SOIGNEUR_ISSUE_NUMBER the dashboard issue, as a number (required) +# SOIGNEUR_ISSUE_COMMENT whether to post the digest as a comment (default: true) + +set -o errexit -o errtrace -o functrace -o nounset -o pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" +readonly root + +# shellcheck source-path=SCRIPTDIR +. "$root"/lib.sh + +: "${SOIGNEUR_REPO:=${GITHUB_REPOSITORY:-}}" +: "${SOIGNEUR_PUBLISH:=true}" +: "${SOIGNEUR_ISSUE_NUMBER:=}" +: "${SOIGNEUR_ISSUE_COMMENT:=true}" + +# An issue description cannot exceed 65536 characters. +readonly max_body=64000 + +soigneur::main(){ + local report="$1" + local digest="${2:-}" + local body number state url + + [ -s "$report" ] || { + echo "error: empty report: $report" >&2 + return 1 + } + + soigneur::bool "$SOIGNEUR_PUBLISH" || { + echo "Publishing is disabled (SOIGNEUR_PUBLISH=$SOIGNEUR_PUBLISH)" >&2 + return + } + + [ "$SOIGNEUR_REPO" != "" ] || { + echo "error: no repository to publish to: set SOIGNEUR_REPO or GITHUB_REPOSITORY" >&2 + return 1 + } + + [ "$SOIGNEUR_ISSUE_NUMBER" != "" ] || { + echo "error: no dashboard issue: open one by hand, then set SOIGNEUR_ISSUE_NUMBER" >&2 + return 1 + } + soigneur::number "SOIGNEUR_ISSUE_NUMBER" "$SOIGNEUR_ISSUE_NUMBER" + number="$SOIGNEUR_ISSUE_NUMBER" + + body="$report" + if [ "$(wc -c < "$report")" -gt "$max_body" ]; then + body="$(mktemp)" + # shellcheck disable=SC2064 + trap "rm -f '$body'" EXIT + # Copy whole lines, while they fit: a byte-exact cut would leave a broken table row, and + # could split a multi-byte character in half, which the API rejects. + # In the C locale, awk counts bytes rather than characters, which is what the limit is in. + LC_ALL=C awk -v max="$max_body" '{ total += length($0) + 1; if (total > max) exit; print }' \ + "$report" > "$body" + # A report whose very first line exceeds the limit cannot happen with this renderer, but + # publishing an empty body would wipe the description, so that case keeps the raw cut. + [ -s "$body" ] || head -c "$max_body" "$report" > "$body" + printf "\n\n_Report truncated: see the workflow run for the full version._\n" >> "$body" + fi + + gh issue edit "$number" --repo "$SOIGNEUR_REPO" --body-file "$body" > /dev/null + url="https://github.com/$SOIGNEUR_REPO/issues/$number" + echo "Updated $url" >&2 + + state="$(gh issue view "$number" --repo "$SOIGNEUR_REPO" --json state --jq '.state')" + [ "$state" != "CLOSED" ] || echo "warning: the dashboard issue is closed: $url" >&2 + + if [ "$digest" != "" ] && [ -s "$digest" ] && soigneur::bool "$SOIGNEUR_ISSUE_COMMENT"; then + gh issue comment "$number" --repo "$SOIGNEUR_REPO" \ + --body "$(cat "$digest") The description above holds the full report." > /dev/null + echo "Commented on $url" >&2 + fi + + echo "$url" +} + +[ "$#" -ge 1 ] || { + echo "usage: $0 [digest.txt]" >&2 + exit 1 +} + +soigneur::main "$@" diff --git a/mod/soigneur/flaky-report.jq b/mod/soigneur/flaky-report.jq new file mode 100644 index 00000000000..17d2370d272 --- /dev/null +++ b/mod/soigneur/flaky-report.jq @@ -0,0 +1,255 @@ +# Renders the "flaky test dashboard" from the raw data collected by flaky-report.sh. +# +# Input (through --slurpfile and --arg): +# $executions: [{sha, id, name, conclusion, url, at}] one entry per job execution +# $findings: [{id, kind, tests}] one entry per reported class +# $notes: [string] caveats about the collection itself +# $footer: string extra markdown for the methodology +# $docsURL: string where the project documents flakiness +# Output: {markdown, digest, data} +# +# Note: the same test may be reported several times by a single job (the integration suite is +# invoked once per variant: non-flaky, flaky, ipv6), and both a parent test and its subtests may +# be reported. Occurrences are therefore counted per (commit, job configuration, kind, top-level +# test), so that a single bad job execution counts once. + +def normalize: gsub("\\s+"; " ") | sub("^ +"; "") | sub(" +$"; ""); + +# Table cells are pipe-separated, and what goes in them comes from a job log or a workflow file: +# a stray pipe would break the table, and a stray backtick would break out of the code span the +# cell is rendered in. The collector rejects both in a test name; this is the second line. +def mdcell: tostring | .[0:200] | gsub("\\|"; "\\|") | gsub("`"; "\u0027"); + +def toplevel: split("/")[0]; + +def day: split("T")[0]; + +def clamp($n): if $n < 0 then 0 elif $n > 10 then 10 else $n end; + +def pct($num; $denom): if $denom > 0 then (100 * $num / $denom | round) else 0 end; + +# A ten characters wide horizontal bar, so that the job failure rates can be compared at a glance. +# Anything that did happen at all gets at least one block: "rare" and "never" must not look alike. +def bar($ratio): + clamp(if $ratio > 0 then ([($ratio * 10) | round, 1] | max) else 0 end) as $filled + | (if $filled > 0 then "█" * $filled else "" end) + + (if $filled < 10 then "░" * (10 - $filled) else "" end); + +def plural($n; $word): "\($n) \($word)\(if $n == 1 then "" else "s" end)"; + +($maxTests | tonumber) as $maxTests +| ($maxLinks | tonumber) as $maxLinks +| ($executions | map(.name |= normalize)) as $execs +| ($execs | map({key: .id, value: .}) | from_entries) as $byID +| ($execs | map(.sha) | unique | length) as $commits +| ( + $findings + | map( + . as $finding + | ($byID[$finding.id] // empty) as $run + | $finding.tests[] + | { + test: ., + top: toplevel, + kind: $finding.kind, + job: $run.name, + url: $run.url, + at: $run.at, + sha: $run.sha + } + ) + ) as $occurrences +| ( + $occurrences + | group_by(.top) + | map( + . as $group + | ($group | group_by([.sha, .job, .kind]) | map(.[0]) | sort_by(.at) | reverse) as $unique + | { + test: $group[0].top, + cases: ($group | map(.test) | unique), + failing: ($unique | map(select(.kind == "failing")) | length), + flaky: ($unique | map(select(.kind == "flaky")) | length), + commits: ($group | map(.sha) | unique | length), + jobs: ($group | map(.job) | unique), + occurrences: $unique + } + | .total = (.failing + .flaky) + ) + | sort_by(-.total, .test) + ) as $tests +| ( + $execs + | group_by(.name) + | map({ + job: .[0].name, + executions: length, + failures: (map(select(.conclusion == "failure")) | length), + urls: [.[] | select(.conclusion == "failure") | .url] + }) + | map(select(.failures > 0)) + | sort_by(-(.failures / .executions), -.failures, .job) + ) as $jobs +| ($tests[:$maxTests]) as $top +| { + data: { + repo: $repo, + branch: $branch, + since: $since, + until: $now, + commits: $commits, + executions: ($execs | length), + tests: $tests, + jobs: $jobs + }, + + markdown: ( + [ + "", + "## Flaky test dashboard", + "", + "Failures reported by the CI on [`\($branch)`](https://github.com/\($repo)/commits/\($branch))" + + " between \($since | day) and \($now | day)" + + " — \(plural($commits; "commit")) with CI results, \(plural($execs | length; "job execution")).", + "", + ( + if ($notes | length) == 0 then + "" + else + (["> [!NOTE]"] + ($notes | map("> - " + .)) + [""]) | join("\n") + end + ), + + "### Tests", + "", + ( + if ($tests | length) == 0 then + "No test-level failure was reported in this window. 🎉" + else + [ + "| Test | Occurrences | Recovered on retry | Commits affected | Configurations | Last seen |", + "| --- | --: | --: | --- | --: | --- |" + ] + + ( + $top + | map( + "| `\(.test | mdcell)`" + + (if (.cases | length) > 1 then " \(plural(.cases | length; "case"))" else "" end) + + " | \(.total)" + + " | \(.flaky)" + + " | \(.commits)/\($commits)" + + " | \(.jobs | length)" + + " | [\(.occurrences[0].at | day)](\(.occurrences[0].url)) |" + ) + ) + + ( + if ($tests | length) > ($top | length) then + ["", "_\(plural(($tests | length) - ($top | length); "less frequently reported test")) omitted._"] + else + [] + end + ) + | join("\n") + end + ), + "", + ( + $top + | map( + "
\(.test) — \(plural(.total; "occurrence"))\n" + + "\n" + + ( + [ + "| When | Job configuration | Test | Outcome |", + "| --- | --- | --- | --- |" + ] + + ( + .occurrences[:$maxLinks] + | map( + "| [\(.at | day)](\(.url))" + + " | `\(.job | mdcell)`" + + " | `\(.test | mdcell)`" + + " | \(if .kind == "flaky" then "recovered on retry" else "failed" end) |" + ) + ) + | join("\n") + ) + + ( + if (.occurrences | length) > $maxLinks then + "\n\n_\(plural((.occurrences | length) - $maxLinks; "older occurrence")) omitted._" + else + "" + end + ) + + "\n\n
" + ) + | join("\n") + ), + "", + + "### Job configurations", + "", + "_Every job, test or not: a job that fails without any test failing is usually an" + + " environment, timeout, or infrastructure problem._", + "", + ( + if ($jobs | length) == 0 then + "Every job execution succeeded in this window. 🎉" + else + [ + "| Job configuration | Failure rate | Failed | Executions | Recent failures |", + "| --- | --- | --: | --: | --- |" + ] + + ( + $jobs + | map( + "| `\(.job | mdcell)`" + + " | `\(bar(.failures / .executions))` \(pct(.failures; .executions))%" + + " | \(.failures)" + + " | \(.executions)" + + " | \([.urls[:3] | to_entries[] | "[\(.key + 1)](\(.value))"] | join(" ")) |" + ) + ) + | join("\n") + end + ), + "", + + "
How this is collected", + "", + "This dashboard is generated by", + "[Soigneur](https://github.com/containerd/nerdctl/tree/main/mod/soigneur),", + "which aggregates the GitHub Actions annotations that the test jobs attach to themselves.", + "No database, test artifact, or server is involved, so the window that can be reported on is", + "limited by the retention of the check runs (90 days, by default).", + "", + "- **Occurrences**: how many (commit × job configuration) executions reported the test as failing.", + "- **Recovered on retry**: occurrences where the test did pass when it was retried", + " (`gotestsum --rerun-fails`). Those are flaky beyond a doubt. The others are either flaky,", + " or consistently broken.", + (if $footer == "" then "" else $footer end), + "", + ( + if $docsURL == "" then + "" + else + "See \($docsURL) to reproduce, fix, or quarantine a flaky test." + end + ), + "", + "
", + "", + (if $runURL == "" then "" else "Generated by [this run](\($runURL))." end) + ] + | join("\n") + ), + + digest: ( + if ($tests | length) == 0 then + "No test-level failure was reported on `\($branch)` over the last \(plural($commits; "commit"))." + else + "\(plural($tests | length; "test")) reported over the last \(plural($commits; "commit")) on `\($branch)`" + + ", topped by " + ([$tests[:3][] | "`\(.test)` (\(.total))"] | join(", ")) + "." + end + ) + } diff --git a/mod/soigneur/flaky-report.sh b/mod/soigneur/flaky-report.sh new file mode 100755 index 00000000000..6fa412e1174 --- /dev/null +++ b/mod/soigneur/flaky-report.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Collects the test failures that the CI reported over a period of time, and renders them as a +# markdown "flaky test dashboard" on stdout. +# +# The data source is the archived log of every workflow run of the window: one request per run +# returns a zip holding the log of each of its jobs, from which the marker lines that +# flaky-annotate.sh wrote (see lib.sh) are read back. There is no database, no test result +# artifact, and no server involved: the logs of the analyzed runs *are* the storage, which also +# means that the window is limited by the retention of the logs (90 days, by default). +# See README.md. +# +# Usage: +# gh auth login # or export GH_TOKEN +# ./flaky-report.sh # markdown to stdout +# +# Environment: +# SOIGNEUR_REPO repository to analyze (default: $GITHUB_REPOSITORY) +# SOIGNEUR_BRANCH branch to analyze (default: main) +# SOIGNEUR_DAYS size of the window, in days (default: 7) +# SOIGNEUR_EVENT only analyze the runs of that event, empty for all (default: push) +# SOIGNEUR_WORKFLOWS only analyze the runs of these workflow files, as a comma-separated list +# of file names, empty for all (default: empty) +# SOIGNEUR_MAX_RUNS maximum number of runs to analyze (default: 200) +# SOIGNEUR_MAX_TESTS maximum number of tests to detail (default: 25) +# SOIGNEUR_MAX_LINKS maximum number of links per test or job (default: 5) +# SOIGNEUR_PARALLEL number of runs to collect concurrently (default: 4) +# SOIGNEUR_API_TIMEOUT per-request timeout, in seconds (default: 300) +# SOIGNEUR_JSON_OUT if set, the raw aggregated data is written to that file +# SOIGNEUR_DIGEST_OUT if set, a one-line summary is written to that file +# SOIGNEUR_RUN_URL link back to the run that generated the report (default: none) +# SOIGNEUR_DOCS_URL where the project documents its flaky tests, linked from the report +# SOIGNEUR_FOOTER extra markdown for the "How this is collected" section, for the caveats +# that are specific to the project (which suites do report per-test data...) +# SOIGNEUR_WORKDIR if set, the downloaded logs and the intermediate results are kept in (and +# re-read from) that directory, which makes iterating on the report cheap + +set -o errexit -o errtrace -o functrace -o nounset -o pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" +readonly root + +# shellcheck source-path=SCRIPTDIR +. "$root"/lib.sh + +: "${SOIGNEUR_REPO:=${GITHUB_REPOSITORY:-}}" +: "${SOIGNEUR_BRANCH:=main}" +: "${SOIGNEUR_DAYS:=7}" +: "${SOIGNEUR_EVENT:=push}" +: "${SOIGNEUR_WORKFLOWS:=}" +: "${SOIGNEUR_MAX_RUNS:=200}" +: "${SOIGNEUR_MAX_TESTS:=25}" +: "${SOIGNEUR_MAX_LINKS:=5}" +: "${SOIGNEUR_PARALLEL:=4}" +: "${SOIGNEUR_API_TIMEOUT:=300}" +: "${SOIGNEUR_JSON_OUT:=}" +: "${SOIGNEUR_DIGEST_OUT:=}" +: "${SOIGNEUR_RUN_URL:=}" +: "${SOIGNEUR_DOCS_URL:=}" +: "${SOIGNEUR_FOOTER:=}" +: "${SOIGNEUR_WORKDIR:=}" +export SOIGNEUR_REPO SOIGNEUR_API_TIMEOUT + +[ "$SOIGNEUR_REPO" != "" ] || { + echo "error: no repository to analyze: set SOIGNEUR_REPO or GITHUB_REPOSITORY" >&2 + exit 1 +} + +soigneur::repo "SOIGNEUR_REPO" "$SOIGNEUR_REPO" +for knob in SOIGNEUR_DAYS SOIGNEUR_MAX_RUNS SOIGNEUR_MAX_TESTS SOIGNEUR_MAX_LINKS SOIGNEUR_PARALLEL SOIGNEUR_API_TIMEOUT; do + soigneur::number "$knob" "${!knob}" +done + +# A single hung request would otherwise hang the whole report: `gh` has no request timeout. +api=(gh api --header "Accept: application/vnd.github+json") +! command -v timeout > /dev/null || api=(timeout "$SOIGNEUR_API_TIMEOUT" "${api[@]}") +readonly api + +# Retries on the failures that are worth retrying. A 4xx is an answer, not a hiccup: a run whose +# logs have expired, or that was deleted, is expected and must not cost three attempts. +soigneur::api(){ + local attempt=1 + local err + + err="$(mktemp)" + while true; do + if "${api[@]}" "$@" 2> "$err" < /dev/null; then + rm -f "$err" + return 0 + fi + if grep -q "HTTP 4" "$err"; then + cat "$err" >&2 + rm -f "$err" + return 1 + fi + [ "$attempt" -lt 3 ] || { + cat "$err" >&2 + echo "error: giving up on: gh api $*" >&2 + rm -f "$err" + return 1 + } + echo "warning: retrying: gh api $*" >&2 + sleep "$((attempt * 5))" + attempt="$((attempt + 1))" + done +} + +# Lists the completed workflow runs of the window, as one json object per line. +soigneur::runs(){ + local since="$1" + local query="repos/$SOIGNEUR_REPO/actions/runs?status=completed&per_page=100" + query="$query&branch=$SOIGNEUR_BRANCH&created=>=$since" + [ "$SOIGNEUR_EVENT" == "" ] || query="$query&event=$SOIGNEUR_EVENT" + + soigneur::api --paginate "$query" \ + | jq -c --arg wanted "$SOIGNEUR_WORKFLOWS" ' + ($wanted | split(",") | map(sub("^ +"; "") | sub(" +$"; "")) | map(select(length > 0))) as $wanted + | .workflow_runs[] + | { + id: (.id | tostring), + attempts: (.run_attempt // 1), + workflow: (.path | sub("^\\.github/workflows/"; "")), + at: .created_at + } + | select(($wanted | length) == 0 or ($wanted | index(.workflow)))' +} + +# A job log holds whatever the tests printed, so a test that prints a line shaped like a marker +# gets that line collected. Since the collected names are rendered into a GitHub issue, only the +# ones that can plausibly be a Go test name are kept: no backtick to break out of the code span +# they are rendered in, no bracket to turn them into a link, and a bounded length. +soigneur::plausible(){ + awk -F'\t' ' + NF != 2 { next } + length($2) <= 200 && $2 ~ /^[A-Za-z0-9_.\/#:@+=,()~^-]+$/ { print; next } + { printf "warning: ignoring an implausible test name: %s\n", substr($2, 1, 60) > "/dev/stderr" } + ' +} + +# Reads the marker lines out of the job logs of one extracted archive, and prints one json object +# per (job, class) found. Takes a job name -> job id lookup, as tsv. +soigneur::logs::parse(){ + local dir="$1" + local lookup="$2" + local -A jobid=() + local file name key id markers + + while IFS=$'\t' read -r key id; do + [ "$key" == "" ] || jobid["$key"]="$id" + done <<< "$lookup" + + # Only the top-level "_.txt" entries are whole job logs: the per-step files + # live in a directory named after the job, and would count twice. + for file in "$dir"/*.txt; do + [ -e "$file" ] || continue + + # GitHub sanitizes the job name for the file name (the slashes and the colons are dropped, the + # newlines that a multi-line `name:` leaves behind are collapsed), so the two are matched on + # their letters and digits only. + name="$(basename "$file" .txt)" + key="$(printf '%s' "${name#*_}" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9')" + id="${jobid[$key]:-}" + [ "$id" != "" ] || { + echo "warning: no job matches the log of '$name'" >&2 + continue + } + + markers="$(tr -d '\r' < "$file" \ + | { grep -oE "$soigneur_marker (failing|flaky) [^[:space:]]+" || true; } \ + | awk '{ print $2 "\t" $3 }' \ + | sort -u)" + + # The logs that predate the markers only hold the block that flaky-annotate.sh prints for + # humans. Reading it as a fallback gives the dashboard the history it would otherwise have to + # wait a full window for. Every log line starts with a timestamp, hence the first field. + [ "$markers" == "" ] && markers="$(tr -d '\r' < "$file" \ + | awk ' + { sub(/^[^ ]+ /, "") } + /^=== Failing tests ===$/ { kind = "failing"; next } + /^=== Flaky tests/ { kind = "flaky"; next } + /^====/ { kind = ""; next } + kind != "" && $1 != "" { print kind "\t" $1 } + ' \ + | sort -u)" || true + + printf "%s" "$markers" | soigneur::plausible | jq -R -s -c --arg id "$id" ' + split("\n") + | map(select(length > 0) | split("\t")) + | group_by(.[0]) + | map({id: $id, kind: .[0][0], tests: map(.[1])})[]' + done +} + +# Collects one run: its job executions, and the markers of every attempt's logs. +soigneur::run::collect(){ + local dir="$1" + local id="$2" + local attempts="$3" + local jobs zip extracted lookup n + + [ ! -e "$dir/$id.done" ] || return 0 + + jobs="$(soigneur::api "repos/$SOIGNEUR_REPO/actions/runs/$id/jobs?per_page=100&filter=all")" || { + echo "$id" >> "$dir/$id.failed" + return 0 + } + + jq -c ' + .jobs[] + | { + sha: .head_sha, + id: (.id | tostring), + name: .name, + conclusion: .conclusion, + url: .html_url, + at: (.completed_at // .started_at) + }' <<< "$jobs" > "$dir/$id.executions" + + : > "$dir/$id.findings" + for ((n = 1; n <= attempts; n++)); do + zip="$dir/$id-$n.zip" + if [ ! -s "$zip" ]; then + soigneur::api "repos/$SOIGNEUR_REPO/actions/runs/$id/attempts/$n/logs" > "$zip" || { + echo "warning: no logs for run $id, attempt $n" >&2 + rm -f "$zip" + echo "$id/$n" >> "$dir/$id.nologs" + continue + } + fi + + extracted="$(mktemp -d)" + unzip -o -q "$zip" -d "$extracted" || { + echo "warning: cannot extract the logs of run $id, attempt $n" >&2 + rm -rf "$extracted" + echo "$id/$n" >> "$dir/$id.nologs" + continue + } + + lookup="$(jq -r --argjson n "$n" ' + .jobs[] + | select(.run_attempt == $n) + | [(.name | ascii_downcase | gsub("[^a-z0-9]"; "")), (.id | tostring)] + | @tsv' <<< "$jobs")" + + soigneur::logs::parse "$extracted" "$lookup" >> "$dir/$id.findings" + rm -rf "$extracted" + done + + touch "$dir/$id.done" +} + +soigneur::main(){ + local tmp since now runs failed nologs + local notes=() + + if [ "$SOIGNEUR_WORKDIR" != "" ]; then + tmp="$SOIGNEUR_WORKDIR" + mkdir -p "$tmp" + else + tmp="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '$tmp'" EXIT + fi + mkdir -p "$tmp"/runs + + now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + # BSD date does not know about `-d`. + since="$(date -u -d "$SOIGNEUR_DAYS days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ + || date -u -v-"$SOIGNEUR_DAYS"d +%Y-%m-%dT%H:%M:%SZ)" + + echo "Collecting $SOIGNEUR_REPO $SOIGNEUR_BRANCH from $since to $now" >&2 + + [ -s "$tmp"/runs.json ] || soigneur::runs "$since" \ + | awk -v max="$SOIGNEUR_MAX_RUNS" 'NR <= max' > "$tmp"/runs.json + runs="$(wc -l < "$tmp"/runs.json)" + echo "Workflow runs: $runs" >&2 + [ "$runs" -lt "$SOIGNEUR_MAX_RUNS" ] || notes+=( + "Only the $runs most recent workflow runs of the window were analyzed (\`SOIGNEUR_MAX_RUNS\`)." + ) + + # One request for the jobs, and one archive download, per run. + jq -r '[.id, .attempts] | @tsv' < "$tmp"/runs.json \ + | tr '\t' '\n' \ + | xargs -r -P "$SOIGNEUR_PARALLEL" -n 2 "$root"/flaky-report.sh --collect-run "$tmp"/runs + + find "$tmp"/runs -name '*.executions' -exec cat '{}' + > "$tmp"/executions.json + find "$tmp"/runs -name '*.findings' -exec cat '{}' + > "$tmp"/findings.json + echo "Job executions: $(wc -l < "$tmp"/executions.json)" >&2 + + failed="$(find "$tmp"/runs -name '*.failed' | wc -l)" + [ "$failed" == "0" ] || notes+=( + "$failed of the $runs workflow runs could not be read: this report is incomplete." + ) + nologs="$(find "$tmp"/runs -name '*.nologs' | wc -l)" + [ "$nologs" == "0" ] || notes+=( + "The logs of $nologs of the $runs workflow runs are gone: their test failures are missing here." + ) + + jq -n -f "$root"/flaky-report.jq \ + --slurpfile executions "$tmp"/executions.json \ + --slurpfile findings "$tmp"/findings.json \ + --arg repo "$SOIGNEUR_REPO" \ + --arg branch "$SOIGNEUR_BRANCH" \ + --arg since "$since" \ + --arg now "$now" \ + --arg maxTests "$SOIGNEUR_MAX_TESTS" \ + --arg maxLinks "$SOIGNEUR_MAX_LINKS" \ + --arg runURL "$SOIGNEUR_RUN_URL" \ + --arg docsURL "$SOIGNEUR_DOCS_URL" \ + --arg footer "$SOIGNEUR_FOOTER" \ + --argjson notes "$(printf '%s\n' "" "${notes[@]:-}" | jq -R -s 'split("\n") | map(select(length > 0))')" \ + > "$tmp"/report.json + + [ "$SOIGNEUR_JSON_OUT" == "" ] || jq '.data' < "$tmp"/report.json > "$SOIGNEUR_JSON_OUT" + [ "$SOIGNEUR_DIGEST_OUT" == "" ] || jq -r '.digest' < "$tmp"/report.json > "$SOIGNEUR_DIGEST_OUT" + jq -r '.markdown' < "$tmp"/report.json +} + +case "${1:-}" in + --collect-run) + # Invoked as a subprocess, one per run: see SOIGNEUR_PARALLEL. + soigneur::run::collect "$2" "$3" "$4" + ;; + "") + soigneur::main + ;; + *) + echo "error: unknown argument: $1" >&2 + exit 1 + ;; +esac diff --git a/mod/soigneur/lib.sh b/mod/soigneur/lib.sh new file mode 100644 index 00000000000..8d3b3f03010 --- /dev/null +++ b/mod/soigneur/lib.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Definitions shared by the two halves of the flaky test dashboard. + +# shellcheck disable=SC2034 +{ + # The marker that flaky-annotate.sh writes to the job log, and that flaky-report.sh reads back + # from the archived logs of a workflow run. This is the contract between the two halves: + # + # flaky-test-dashboard: failing TestFoo + # flaky-test-dashboard: flaky TestBar/subtest + # + # One line per test, so that nothing depends on the layout of the log around it. Changing this + # breaks the collection of every job that still runs the previous version, hence the constant. + readonly soigneur_marker="flaky-test-dashboard:" + + # The titles of the annotations that flaky-annotate.sh emits for the GitHub web UI. They carry + # the same information as the markers, for humans looking at a pull request. + readonly soigneur_title_failing="Failing tests" + readonly soigneur_title_flaky="Flaky tests" +} + +# Fails, with a message naming the variable, when a value is not a plain non-negative integer. +# The knobs reach the scripts as environment variables, and end up as arguments of `date`, `awk`, +# `jq`, and `gh`: a value that is not a number deserves to be named, and one that starts with a +# dash would be read as a flag by whatever it is passed to. +soigneur::number(){ + local name="$1" + local value="$2" + + [[ "$value" =~ ^[0-9]+$ ]] || { + echo "error: $name must be a non-negative integer, got '$value'" >&2 + return 1 + } +} + +# Fails when a value is not an "owner/name" pair. It is interpolated into the API paths, and +# passed to `gh` as an argument, so it has to be neither a path traversal nor a flag. +soigneur::repo(){ + local name="$1" + local value="$2" + + [[ "$value" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { + echo "error: $name must be OWNER/NAME, got '$value'" >&2 + return 1 + } +} + +# Truthiness, the same way Go's strconv.ParseBool sees it: an explicit "false" means false, and +# so does anything unset. Never gate on non-empty. +soigneur::bool(){ + case "${1,,}" in + 1 | t | true | y | yes | on) return 0 ;; + *) return 1 ;; + esac +} From 84c09649946b87b1dc52bb29682f1e1975774dd2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 11 Sep 2026 02:50:56 +0900 Subject: [PATCH 815/868] CI: publish a weekly flaky test dashboard Use the mod/soigneur action, on both ends: - gotestsum-reporter.sh hands its json file to flaky-annotate.sh, so that the tests that recovered on retry are reported too. That set was already computed, only to be excluded from the failing ones, and then thrown away - yet those are the tests that are flaky beyond a doubt, and the ones nobody could see, since a job that recovers is green. The step summary now lists them next to the failing ones. - the flaky-test-dashboard workflow collects the week, every Monday on a quiet hour, and publishes it to #5202, the issue it names. It only looks at the runs of the workflows that do run tests, and can also be dispatched manually, with a window, a branch, and a dry-run switch. docs/testing/flaky.md covers what the dashboard reports, what it does not (only the integration suites report per-test data), how to run the collector locally, and how to reproduce, fix, or quarantine a flaky test. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .github/workflows/flaky-test-dashboard.yml | 73 ++++++++++++ docs/testing/README.md | 3 + docs/testing/flaky.md | 127 +++++++++++++++++++++ hack/github/gotestsum-reporter.sh | 45 ++++---- 4 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/flaky-test-dashboard.yml create mode 100644 docs/testing/flaky.md diff --git a/.github/workflows/flaky-test-dashboard.yml b/.github/workflows/flaky-test-dashboard.yml new file mode 100644 index 00000000000..1e6e441520d --- /dev/null +++ b/.github/workflows/flaky-test-dashboard.yml @@ -0,0 +1,73 @@ +# This workflow collects the test failures that the CI reported over the last week, and publishes +# them to a single GitHub issue: the "flaky test dashboard", containerd/nerdctl#5202. +# +# Everything reusable lives in the mod/soigneur action: the data source is the archived +# logs of the test runs, in which hack/github/gotestsum-reporter.sh left one marker line per +# failing test, so there is no database, no test result artifact, and no server involved. +# See docs/testing/flaky.md. +name: flaky-test-dashboard + +on: + schedule: + # Every Monday at 07:00 UTC: a quiet hour, since collecting a week does download the logs of + # every test run of that week (about 50 MB, and one API request per run). + - cron: "0 7 * * 1" + workflow_dispatch: + inputs: + days: + description: "Size of the window to report on, in days" + required: false + default: "7" + type: string + branch: + description: "Branch to report on" + required: false + default: "main" + type: string + publish: + description: "Update the dashboard issue (otherwise, only write to the run summary)" + required: false + default: true + type: boolean + +permissions: + contents: read + +# The dashboard is a single issue: two runs rewriting it, and commenting on it, at the same time +# would be visible. Queue them instead. +concurrency: + group: flaky-test-dashboard + cancel-in-progress: false + +jobs: + dashboard: + name: "collect and publish" + # Never run on forks: this would open an issue in the fork. + if: ${{ github.repository == 'containerd/nerdctl' }} + runs-on: ubuntu-26.04 + timeout-minutes: 45 + permissions: + contents: read # fetch the action from this repository + actions: read # list the workflow runs, and download their logs + issues: write # create, and update, the dashboard issue + + steps: + # `$/` resolves the action from this repository at the ref the workflow runs at, so there + # is nothing to check out, and no working tree that a previous step could have altered. + - name: "Run" + uses: $/mod/soigneur + with: + # https://github.com/containerd/nerdctl/issues/5202 is *the* dashboard: its description + # is rewritten, and the digest posted as a comment. Nothing ever opens an issue. + issue-number: "5202" + branch: ${{ inputs.branch || 'main' }} + days: ${{ inputs.days || '7' }} + # The other workflows have no test to report on, and downloading their logs would only + # spend requests. Keep this in sync when a test workflow is added. + workflows: "workflow-test.yml,workflow-flaky.yml,workflow-tigron.yml" + # For a schedule, `inputs.publish` is empty, and publishing is what the schedule is for. + publish: ${{ inputs.publish || github.event_name == 'schedule' }} + docs-url: "https://github.com/containerd/nerdctl/blob/main/docs/testing/flaky.md" + footer-notes: | + - Only the integration suites report per-test data: the unit tests, Windows, and + FreeBSD do not run the reporter. diff --git a/docs/testing/README.md b/docs/testing/README.md index efea60cf4b5..23d099f9f35 100644 --- a/docs/testing/README.md +++ b/docs/testing/README.md @@ -5,6 +5,9 @@ and principles about writing tests. For more comprehensive information about nerdctl test tools, see [tools.md](tools.md). +For flaky tests - how the CI reports them, and what to do about them - see +[flaky.md](flaky.md). + ## Code, fix, lint, rinse, repeat ``` diff --git a/docs/testing/flaky.md b/docs/testing/flaky.md new file mode 100644 index 00000000000..2d3ead813de --- /dev/null +++ b/docs/testing/flaky.md @@ -0,0 +1,127 @@ +# Flaky tests + +A flaky test is a test that fails, then passes, without anything having changed. + +nerdctl runs its integration suite against a dozen different environments (rootful, rootless, +ipv6, arm64, older ubuntu, older containerd, almalinux, Docker, ...), on ephemeral runners, with +real daemons, real networking, and real images. Some tests do occasionally fail there without +anybody having broken anything, and the first problem with those is *knowing* about them: a +failure that nobody looks at twice is a failure that stays. + +This is what the **flaky test dashboard** is for. + +## The dashboard + +The dashboard is a single, long-lived GitHub issue, +[containerd/nerdctl#5202](https://github.com/containerd/nerdctl/issues/5202), whose description is +rewritten every Monday by the +[`flaky-test-dashboard`](../../.github/workflows/flaky-test-dashboard.yml) workflow. Every week it also +posts a one-line digest as a comment, so that the subscribers get notified without the issue +growing a copy of every report. + +It reports, for the last seven days of `main`: + +- which tests were reported as failing, how often, and in how many different job configurations; +- which of those failures were *recovered on retry*, in other words: proven flaky; +- which job configurations failed, and how often - a job that fails without any test failing is + usually an environment or a timeout problem, and that is worth knowing too. + +Note that: + +- Only the integration suites report per-test data. The unit tests, and the Windows and FreeBSD + jobs, do not run the reporter (see the `--post-run-command` in + [`hack/test-integration.sh`](../../hack/test-integration.sh)). +- A test that is reported as failing is not necessarily flaky: it may simply be broken. Only the + "recovered on retry" column proves flakiness by itself. +- The window cannot be extended arbitrarily: the data lives in the logs of the analyzed runs, + which GitHub retains for 90 days by default. + +## How it works + +There is no test result database, no artifact to download, and no server to operate. The CI +already annotates its own test jobs, and the dashboard is just a weekly aggregation of those +annotations: + +1. [`hack/test-integration.sh`](../../hack/test-integration.sh) runs the suite with + `gotestsum --jsonfile`, and, for the flaky suite, with `--rerun-fails`. +2. [`hack/github/gotestsum-reporter.sh`](../../hack/github/gotestsum-reporter.sh) reads that json + file when the run is over, and hands it to + [`flaky-annotate.sh`](../../mod/soigneur/flaky-annotate.sh), which writes one marker + line per test to the job log, in two classes: + - `flaky-test-dashboard: failing TestFoo`: failed, and never passed, even on retry; + - `flaky-test-dashboard: flaky TestFoo`: failed, then passed on retry. + + The same two classes are also emitted as annotations (`Failing tests`, `Flaky tests`), which + is what shows up on a pull request. +3. [`flaky-report.sh`](../../mod/soigneur/flaky-report.sh) lists the workflow runs of the + branch and, for each of them, downloads the archived logs of all of its jobs in a single + request, reads the markers back, and renders the aggregate as markdown. +4. [`flaky-issue.sh`](../../mod/soigneur/flaky-issue.sh) publishes the result to the + dashboard issue. + +Steps 2 to 4 are not nerdctl-specific, and live in [Soigneur](../../mod/soigneur), a reusable +action - a *soigneur* is the keeper who tends the animals, and this one keeps an eye on the test +suite. [`.github/workflows/flaky-test-dashboard.yml`](../../.github/workflows/flaky-test-dashboard.yml) +calls it with the nerdctl-specific bits (the branch, the window, the link to this document). Its +[README](../../mod/soigneur/README.md) covers the inputs, the outputs, and the marker +contract - the marker lines are an API between the two halves, so do not change them on one side +only. Note that only the runs of the `push` event are reported on, which is the post-merge +signal: a failure on a pull request is usually the pull request's own doing. + +## Running the collector locally + +The collector only needs `gh` (authenticated), `jq`, and read access to the repository: + +```bash +export SOIGNEUR_REPO=containerd/nerdctl + +# The last 7 days of main, as markdown, on stdout +./mod/soigneur/flaky-report.sh + +# A different window, or branch +SOIGNEUR_DAYS=30 SOIGNEUR_BRANCH=release/2.2 ./mod/soigneur/flaky-report.sh + +# Keep (and re-read) the API responses, which makes iterating on the report almost free +SOIGNEUR_WORKDIR=/tmp/flaky ./mod/soigneur/flaky-report.sh + +# Get the aggregate as json instead, to slice it differently +SOIGNEUR_JSON_OUT=/tmp/flaky.json ./mod/soigneur/flaky-report.sh > /dev/null +jq '.tests[] | select(.flaky > 0) | .test' /tmp/flaky.json +``` + +Collecting a week costs about 200 API requests and downloads some 50 MB of logs, which is why +the workflow runs weekly, on a quiet hour, and only for the workflows that do run tests. +`SOIGNEUR_DAYS`, `SOIGNEUR_MAX_RUNS`, and the other knobs are documented at the top of the script; +`SOIGNEUR_WORKDIR` keeps the downloaded logs around, which makes a second look free. + +The dashboard itself can be refreshed at any time by dispatching the workflow manually +(`Actions` > `flaky-test-dashboard` > `Run workflow`), which also accepts a window and a branch, and +can be told not to touch the issue at all (`publish: false`), in which case the report is only +written to the run summary. + +## Working on a flaky test + +To reproduce, run the test in a loop, in the environment the dashboard points at: + +```bash +go test ./cmd/nerdctl/container -run 'TestRunSomething' -count 10 -p 1 +``` + +If it does not fail, try it under load (`-parallel`, or simply another suite running at the same +time), as most flakiness in this project comes from timing and from resource contention. + +A test that is known to be flaky, and that cannot be fixed right away, should be marked as such +rather than left to fail at random: + +```go +testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/1234") +``` + +Flaky tests are then only run by the dedicated `-test.only-flaky=true` pass, which retries them +(`--rerun-fails`), and which the +[`[flaky, see #3988]`](../../.github/workflows/workflow-flaky.yml) workflow may skip entirely. +This keeps the signal of the main suites clean - at the price of no longer really testing what +those tests cover, so please do link an issue, and do come back to it. + +See also [tools.md](tools.md) for the test framework itself, and +[README.md](README.md) for how to run the suites. diff --git a/hack/github/gotestsum-reporter.sh b/hack/github/gotestsum-reporter.sh index edd357c0487..d245e9eac97 100755 --- a/hack/github/gotestsum-reporter.sh +++ b/hack/github/gotestsum-reporter.sh @@ -24,18 +24,25 @@ readonly root GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}" -# Identify consistently failing tests: those that failed but never passed, even on retry. -# Tests that failed then passed on retry (flaky) are excluded. -failing_tests="$(jq -rc 'select(.Test) | select(.Action == "fail" or .Action == "pass") | [.Action, .Test] | @tsv' < "$GOTESTSUM_JSONFILE" \ - | awk -F'\t' ' - $1 == "fail" { failed[$2] = 1 } - $1 == "pass" { passed[$2] = 1 } - END { - for (t in failed) { - if (!(t in passed)) print t - } - } - ' | sort)" +# The classification of the tests, and everything the flaky test dashboard needs, live in the +# reusable module: see mod/soigneur/README.md and docs/testing/flaky.md. +# - "failing": the test never passed, even on retry (either consistently broken, or not retried). +# - "flaky": the test did pass on retry, so it is flaky beyond a doubt. +# flaky-annotate.sh prints both lists to stdout, so that they stay visible at the end of the job +# log, writes the marker lines that the dashboard is collected from, and emits the matching +# annotations for the pull request. The lists come back through SOIGNEUR_FAILING_OUT and +# SOIGNEUR_SOIGNEUR_OUT, for the step summary below. +readonly soigneur="$root"/../../mod/soigneur + +lists="$(mktemp -d)" +# shellcheck disable=SC2064 +trap "rm -rf '$lists'" EXIT + +SOIGNEUR_FAILING_OUT="$lists"/failing SOIGNEUR_SOIGNEUR_OUT="$lists"/flaky \ + "$soigneur"/flaky-annotate.sh "$GOTESTSUM_JSONFILE" + +failing_tests="$(cat "$lists"/failing)" +flaky_tests="$(cat "$lists"/flaky)" { github::md::h3 "Total number of tests: $TESTS_TOTAL" @@ -50,17 +57,13 @@ failing_tests="$(jq -rc 'select(.Test) | select(.Action == "fail" or .Action == echo "${failing_tests:-}" echo '```' + github::md::h3 "Flaky tests (failed, then passed on retry)" + echo '```' + echo "${flaky_tests:-}" + echo '```' + github::md::h3 "Tests taking more than 15 seconds" echo '```' gotestsum tool slowest --threshold 15s --jsonfile "$GOTESTSUM_JSONFILE" echo '```' } >> "$GITHUB_STEP_SUMMARY" - -# Print failing tests to stdout so they are visible at the end of the job log. -if [ -n "${failing_tests:-}" ]; then - printf '\n=== Failing tests ===\n%s\n=====================\n' "$failing_tests" - # Also emit as a GitHub Actions error annotation (visible in PR checks and annotations panel). - # GitHub Actions uses %0A for newlines inside annotation messages. - encoded="${failing_tests//$'\n'/%0A}" - echo "::error title=Failing tests::${encoded}" -fi From fe48ce6cd9aa033756c20f9f9b9e43100de05a04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:32:46 +0000 Subject: [PATCH 816/868] build(deps): bump golang.org/x/net in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.58.0 to 0.59.0 - [Commits](https://github.com/golang/net/compare/v0.58.0...v0.59.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.59.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c073bcacb48..c18f4a2b827 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/yuchanns/srslog v1.1.0 //gomodjail:unconfined go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.57.0 - golang.org/x/net v0.58.0 //gomodjail:unconfined + golang.org/x/net v0.59.0 //gomodjail:unconfined golang.org/x/sync v0.23.0 //gomodjail:unconfined golang.org/x/sys v0.48.0 //gomodjail:unconfined golang.org/x/term v0.46.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 05ac016f32d..4c78ab3b1a5 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,8 @@ 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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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= From 5a26853389ab35312f3d9c0a441b75315ba03c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:32:52 +0000 Subject: [PATCH 817/868] build(deps): bump github.com/moby/sys/userns in the moby-sys group Bumps the moby-sys group with 1 update: [github.com/moby/sys/userns](https://github.com/moby/sys). Updates `github.com/moby/sys/userns` from 0.2.0 to 0.2.1 - [Release notes](https://github.com/moby/sys/releases) - [Commits](https://github.com/moby/sys/compare/user/v0.2.0...userns/v0.2.1) --- updated-dependencies: - dependency-name: github.com/moby/sys/userns dependency-version: 0.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: moby-sys ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c073bcacb48..00730fc2caa 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined github.com/moby/sys/user v0.4.1 //gomodjail:unconfined - github.com/moby/sys/userns v0.2.0 //gomodjail:unconfined + github.com/moby/sys/userns v0.2.1 //gomodjail:unconfined github.com/moby/term v0.5.2 //gomodjail:unconfined github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined github.com/opencontainers/go-digest v1.0.0 diff --git a/go.sum b/go.sum index 05ac016f32d..b6667bdaf52 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0= github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y= -github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM= -github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/sys/userns v0.2.1 h1:4OvdM7BcPkASbuouHsbW3aeMJSFlYDldBRnXVZhaRk8= +github.com/moby/sys/userns v0.2.1/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= From 0af2b8d8e8e6ba6af9dca4917c9b3a9ecffa53e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:33:18 +0000 Subject: [PATCH 818/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.15 to 0.15.16. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.15...v0.15.16) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.15.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c073bcacb48..9ab87b0ced4 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined github.com/containerd/log v0.2.0 //gomodjail:unconfined github.com/containerd/nerdctl/mod/tigron v0.0.0 //gomodjail:unconfined - github.com/containerd/nydus-snapshotter v0.15.15 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.15.16 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 05ac016f32d..1102194b35b 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQuj github.com/containerd/imgcrypt/v2 v2.0.3/go.mod h1:Sjfd3uOiBWtb1dUo1yS5Z/ZxdfMU7MpBr2pzs9HoRDs= github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= -github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk= -github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY= +github.com/containerd/nydus-snapshotter v0.15.16 h1:FkAmNADhOQFeWZtrHcDz04ZoOKsIeKZtKbz7PHxIFtc= +github.com/containerd/nydus-snapshotter v0.15.16/go.mod h1:x+/i5WkV8w4jSXbdv57YSMWvkzT2JFwpeIDJxvcYe38= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= From 4e4753c501540ce2086520f67869e9229a2bff1b Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Sun, 13 Sep 2026 20:05:10 +0400 Subject: [PATCH 819/868] fix(network): combine repeated name filters with OR Repeated name filters were treated as an intersection, so separate names returned no results. Match any requested name while preserving label intersections, and cover the behavior with unit and integration tests. Signed-off-by: Immanuel Tikhonov --- .../network/network_list_linux_test.go | 25 ++++++++ pkg/cmd/network/list.go | 9 ++- pkg/cmd/network/list_test.go | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/network/list_test.go diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index fbc374d6f4d..695ab0ccdcc 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -112,6 +112,31 @@ func TestNetworkLsFilter(t *testing.T) { } }, }, + { + Description: "filter multiple names", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "ls", "--quiet", + "--filter", "name="+data.Labels().Get("net1"), + "--filter", "name="+data.Labels().Get("net2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Equal(t, len(lines), 2) + netIDs := map[string]struct{}{ + data.Labels().Get("netID1")[:12]: {}, + data.Labels().Get("netID2")[:12]: {}, + } + for _, id := range lines { + _, ok := netIDs[id] + assert.Assert(t, ok) + delete(netIDs, id) + } + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go index c01471532c0..090af4b5ddf 100644 --- a/pkg/cmd/network/list.go +++ b/pkg/cmd/network/list.go @@ -214,11 +214,14 @@ func networkMatchesFilter(net *netutil.NetworkConfig, labelFilterFuncs []func(*m return false } } + if len(nameFilterFuncs) == 0 { + return true + } for _, nameFilterFunc := range nameFilterFuncs { - if !nameFilterFunc(net.Name) { - return false + if nameFilterFunc(net.Name) { + return true } } - return true + return false } diff --git a/pkg/cmd/network/list_test.go b/pkg/cmd/network/list_test.go new file mode 100644 index 00000000000..87c61e51618 --- /dev/null +++ b/pkg/cmd/network/list_test.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package network + +import ( + "testing" + + "github.com/containernetworking/cni/libcni" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/netutil" +) + +func TestNetworkMatchesFilter(t *testing.T) { + t.Parallel() + labels := map[string]string{"env": "prod", "tier": "web"} + net := &netutil.NetworkConfig{ + NetworkConfigList: &libcni.NetworkConfigList{Name: "frontend"}, + NerdctlLabels: &labels, + } + + testCases := []struct { + name string + filters []string + expected bool + }{ + {"no filters", nil, true}, + {"matching name", []string{"name=frontend"}, true}, + {"one of multiple names", []string{"name=backend", "name=frontend"}, true}, + {"all labels", []string{"label=env=prod", "label=tier=web"}, true}, + {"one of multiple labels", []string{"label=env=dev", "label=tier=web"}, false}, + {"matching name and label", []string{"name=frontend", "label=env=prod"}, true}, + {"matching name only", []string{"name=frontend", "label=env=dev"}, false}, + {"matching label only", []string{"name=backend", "label=env=prod"}, false}, + {"no match", []string{"name=backend", "label=env=dev"}, false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + labelFilters, nameFilters, err := getNetworkFilterFuncs(tc.filters) + assert.NilError(t, err) + assert.Equal(t, networkMatchesFilter(net, labelFilters, nameFilters), tc.expected) + }) + } +} From ca8cc827b6b5ccf30e16ac6d4219bf15f635385c Mon Sep 17 00:00:00 2001 From: Mayur Das Date: Mon, 22 Jun 2026 22:42:54 +0530 Subject: [PATCH 820/868] feat(mount): support --mount type=image with image-subpath Mount an image's filesystem into a container read-only, matching Docker: --mount type=image,source=,destination=. The source image is ensured and unpacked, a read-only snapshot view of its rootfs is created and mounted at the destination, and the view is removed when the container is deleted. The image-subpath option exposes a single directory of the image rootfs at the destination instead of the whole rootfs. An OCI overlay mount cannot select a subdirectory, so a subpath mount materializes the read-only view on a host directory under the data root, resolves the subpath, and bind-mounts the resolved directory read-only into the container. The host materialization path is recorded on a container label and unmounted and removed on container deletion, alongside the snapshot view. The subpath is normalized and bounded to the rootfs at parse time (rejecting absolute paths and traversal that escapes), then opened in the materialized rootfs with os.OpenInRoot, which requires it to exist and rejects absolute or escaping symlinks rather than following them against the host. Any symlink left in the path is relative and stays inside the rootfs when mount(2) follows it, so the unresolved path is used as the bind source. Docker rejects those same subpaths. The whole-rootfs path hands the snapshotter mount straight to the runtime, which owns its lifecycle. Labels in the reserved nerdctl/ namespace are stripped from an image's config labels. --label already rejects that prefix, but image config labels bypassed it, which would let an image forge internal container state - including the image-mount host paths that nerdctl rm unmounts and deletes. Mounting the same image at multiple destinations is supported; the corresponding tests are skipped on Docker, which rejects mounting the same image more than once. Signed-off-by: Mayur Das --- .../container_run_mount_image_linux_test.go | 192 +++++++++++++++++- docs/command-reference.md | 2 +- pkg/cmd/container/create.go | 48 ++++- pkg/cmd/container/remove.go | 17 +- pkg/cmd/container/run_mount.go | 137 ++++++++++--- pkg/labels/labels.go | 5 + pkg/mountutil/mountutil.go | 7 + pkg/mountutil/mountutil_linux.go | 53 ++++- pkg/mountutil/mountutil_linux_test.go | 93 ++++++++- 9 files changed, 505 insertions(+), 49 deletions(-) diff --git a/cmd/nerdctl/container/container_run_mount_image_linux_test.go b/cmd/nerdctl/container/container_run_mount_image_linux_test.go index 4e7435f6130..90f163e0e26 100644 --- a/cmd/nerdctl/container/container_run_mount_image_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_image_linux_test.go @@ -17,6 +17,7 @@ package container import ( + "errors" "fmt" "testing" @@ -95,10 +96,147 @@ func TestRunMountTypeImageReadOnly(t *testing.T) { testCase.Run(t) } +// TestRunMountTypeImageSubpath verifies that image-subpath exposes only the +// selected directory of the image rootfs at the destination: the image's +// /etc/os-release is reachable as /os-release. +func TestRunMountTypeImageSubpath(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=etc", testutil.CommonImage), + testutil.CommonImage, "cat", "/mnt/img/os-release") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("Alpine"), + } + } + + testCase.Run(t) +} + +// TestRunMountTypeImageSubpathMultiple verifies that two image-subpath mounts of +// the same image at different destinations each expose their own subdirectory, +// exercising the multi-mount label round-trip and cleanup. +func TestRunMountTypeImageSubpathMultiple(t *testing.T) { + testCase := nerdtest.Setup() + // nerdctl-only: Docker keys an image mount by its source image and rejects + // mounting the same image twice ("mount already exists with name"). + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/etc,image-subpath=etc", testutil.CommonImage), + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/bin,image-subpath=bin", testutil.CommonImage), + testutil.CommonImage, "ls", "/mnt/etc", "/mnt/bin") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + } + } + + testCase.Run(t) +} + +// TestRunMountTypeImageSubpathAbsoluteSymlink verifies a subpath through an +// absolute symlink (-> /etc, which exists on any host) is rejected, not followed +// against the host. Docker rejects it too but with its own message. +func TestRunMountTypeImageSubpathAbsoluteSymlink(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +RUN ln -s /etc /abs +`, testutil.CommonImage) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("img"), data.Temp().Path()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=abs", data.Identifier("img")), + testutil.CommonImage, "true") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + errMsg := "path escapes from parent" + if nerdtest.IsDocker() { + errMsg = "escapes the base directory" + } + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New(errMsg)}, + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("img")) + } + + testCase.Run(t) +} + +// TestRunMountTypeImageSubpathRelativeSymlink verifies a subpath through a +// relative symlink that stays inside the rootfs resolves to the image's own +// target, as it does with Docker. +func TestRunMountTypeImageSubpathRelativeSymlink(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +RUN mkdir -p /data/real /links && echo hello > /data/real/f && ln -s ../data/real /links/rel +`, testutil.CommonImage) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("img"), data.Temp().Path()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=links/rel", data.Identifier("img")), + testutil.CommonImage, "cat", "/mnt/img/f") + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello\n")) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("img")) + } + + testCase.Run(t) +} + +// TestRunMountTypeImageSubpathReadOnly verifies an image-subpath mount is +// read-only so writing fails, matching Docker. +func TestRunMountTypeImageSubpathReadOnly(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=etc", testutil.CommonImage), + testutil.CommonImage, "touch", "/mnt/img/should-fail") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("Read-only file system")}, + } + } + + testCase.Run(t) +} + // TestRunMountTypeImageErrors verifies that an image mount missing its source, -// or using the not-yet-supported image-subpath option, is rejected. Docker -// implements image-subpath, so that case diverges and the test is not run -// against Docker. +// or using the not-yet-supported subpath option, or an image-subpath that +// escapes the rootfs, is rejected. These are nerdctl-specific behaviours here, +// so the test is not run against Docker. func TestRunMountTypeImageErrors(t *testing.T) { testCase := nerdtest.Setup() testCase.Require = require.Not(nerdtest.Docker) @@ -118,16 +256,58 @@ func TestRunMountTypeImageErrors(t *testing.T) { }, }, { - Description: "image-subpath not supported", + Description: "subpath not supported", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,subpath=etc", testutil.CommonImage), + testutil.CommonImage, "true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("subpath")}, + } + }, + }, + { + Description: "image-subpath parent traversal rejected", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=../etc", testutil.CommonImage), + testutil.CommonImage, "true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("escapes")}, + } + }, + }, + { + Description: "image-subpath absolute rejected", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=/etc", testutil.CommonImage), + testutil.CommonImage, "true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{fmt.Errorf("relative")}, + } + }, + }, + { + Description: "empty image-subpath rejected", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", - "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=etc", testutil.CommonImage), + "--mount", fmt.Sprintf("type=image,source=%s,destination=/mnt/img,image-subpath=", testutil.CommonImage), testutil.CommonImage, "true") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeGenericFail, - Errors: []error{fmt.Errorf("image-subpath")}, + Errors: []error{fmt.Errorf("value is empty")}, } }, }, diff --git a/docs/command-reference.md b/docs/command-reference.md index c0455e60508..75cacd30d10 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -332,7 +332,7 @@ Volume flags: - Options specific to `image`: - :whale: `src`, `source`: image reference (mandatory). - :whale: Currently, the image filesystem is mounted read-only. - - unimplemented options: `image-subpath` + - :whale: `image-subpath`: relative path inside the image rootfs to mount instead of the whole rootfs. The value is normalized (`a/../b` means `b`) and must resolve inside the rootfs: an empty value, an absolute path, a path escaping the rootfs, and a path through an absolute symlink (such as Alpine's `/bin/sh`) are rejected. A value that normalizes to the rootfs itself, such as `.`, mounts the whole rootfs. - :whale: `--volumes-from`: Mount volumes from the specified container(s), e.g. "--volumes-from my-container". Rootfs flags: diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index e2f89159d95..b2cfd880331 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -95,20 +95,24 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.platform = options.Platform internalLabels.namespace = options.GOptions.Namespace - // If creation fails after image-mount views are created, remove them so the - // snapshots do not leak (the cleanup label is only persisted on success). + // If creation fails after image-mount state is created, tear it down so the + // snapshots and host mounts do not leak (the cleanup labels are only persisted + // on success). defer func() { if retErr == nil { return } - var keys []string + var keys, hostpaths []string for _, mp := range internalLabels.mountPoints { if mp.ImageMountSnapshot != "" { keys = append(keys, mp.ImageMountSnapshot) } + if mp.ImageMountHostpath != "" { + hostpaths = append(hostpaths, mp.ImageMountHostpath) + } } - if len(keys) > 0 { - removeImageMountViews(ctx, client.SnapshotService(options.GOptions.Snapshotter), keys) + if len(keys) > 0 || len(hostpaths) > 0 { + removeImageMounts(ctx, client.SnapshotService(options.GOptions.Snapshotter), hostpaths, keys) } }() @@ -234,7 +238,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa // 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)) + cOpts = append(cOpts, containerd.WithImageConfigLabels(ensuredImage.Image), withoutReservedLabels()) } opts = append(opts, rootfsOpts...) cOpts = append(cOpts, rootfsCOpts...) @@ -740,6 +744,22 @@ func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) { }, nil } +// withoutReservedLabels drops labels in the internal "nerdctl/" namespace. It runs +// right after WithImageConfigLabels, the one path that can set them without going +// through --label (which rejects the prefix): otherwise an image could forge +// internal state, e.g. the image-mount host paths that `nerdctl rm` deletes. +func withoutReservedLabels() containerd.NewContainerOpts { + return func(_ context.Context, _ *containerd.Client, c *containers.Container) error { + for k := range c.Labels { + if strings.HasPrefix(k, labels.Prefix) { + log.L.Warnf("Ignoring reserved label %q set by the image config", k) + delete(c.Labels, k) + } + } + return nil + } +} + func withContainerLabels(label, labelFile []string) ([]containerd.NewContainerOpts, error) { var opts []containerd.NewContainerOpts @@ -894,13 +914,16 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.AnonymousVolumes] = string(anonVolumeJSON) } - // Record the snapshot keys of any type=image mount views so they can be - // removed when the container is deleted. - var imageMountSnapshots []string + // Record the snapshot keys and host materialization paths of any type=image + // mounts so they can be removed when the container is deleted. + var imageMountSnapshots, imageMountHostpaths []string for _, mp := range internalLabels.mountPoints { if mp.ImageMountSnapshot != "" { imageMountSnapshots = append(imageMountSnapshots, mp.ImageMountSnapshot) } + if mp.ImageMountHostpath != "" { + imageMountHostpaths = append(imageMountHostpaths, mp.ImageMountHostpath) + } } if len(imageMountSnapshots) > 0 { b, err := json.Marshal(imageMountSnapshots) @@ -909,6 +932,13 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO } m[labels.ImageMountSnapshots] = string(b) } + if len(imageMountHostpaths) > 0 { + b, err := json.Marshal(imageMountHostpaths) + if err != nil { + return nil, err + } + m[labels.ImageMountHostpaths] = string(b) + } if internalLabels.pidFile != "" { m[labels.PIDFile] = internalLabels.pidFile diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index ceffe0374b7..d00332bcc02 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -286,15 +286,22 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions } } - // Remove the read-only views backing type=image mounts - soft failure. + // Tear down type=image mount state (host materializations and read-only + // views) backing this container - soft failure. + var imageMountKeys, imageMountHostpaths []string if snapshotsJSON, ok := containerLabels[labels.ImageMountSnapshots]; ok { - var keys []string - if err = json.Unmarshal([]byte(snapshotsJSON), &keys); err != nil { + if err = json.Unmarshal([]byte(snapshotsJSON), &imageMountKeys); err != nil { log.G(ctx).WithError(err).Warnf("failed to unmarshal image-mount snapshots for container %q", id) - } else { - removeImageMountViews(ctx, client.SnapshotService(imageMountSnapshotter), keys) } } + if hostpathsJSON, ok := containerLabels[labels.ImageMountHostpaths]; ok { + if err = json.Unmarshal([]byte(hostpathsJSON), &imageMountHostpaths); err != nil { + log.G(ctx).WithError(err).Warnf("failed to unmarshal image-mount host paths for container %q", id) + } + } + if len(imageMountKeys) > 0 || len(imageMountHostpaths) > 0 { + removeImageMounts(ctx, client.SnapshotService(imageMountSnapshotter), imageMountHostpaths, imageMountKeys) + } }() // Get the task. diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index d6bdb97e603..4727ab6da2a 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -127,19 +127,20 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr const gcRootLabel = "containerd.io/gc.root" // setupImageMount ensures and unpacks ref, then creates a read-only GC-rooted -// snapshot view of its rootfs. Image mounts are always read-only, matching -// Docker. It returns the OCI mount for destination and the view's snapshot key. -func setupImageMount(ctx context.Context, client *containerd.Client, options types.ContainerCreateOptions, ref, destination string) (specs.Mount, string, error) { +// snapshot view of its rootfs. It returns the OCI mount for destination, the +// view's snapshot key, and the host path a subpath was materialized on (empty +// for a whole-rootfs mount). The view is removed if setup fails after creating it. +func setupImageMount(ctx context.Context, client *containerd.Client, options types.ContainerCreateOptions, ref, destination, subpath string) (_ specs.Mount, _ string, _ string, retErr error) { ensured, err := imgutil.EnsureImage(ctx, client, ref, options.ImagePullOpt) if err != nil { - return specs.Mount{}, "", fmt.Errorf("failed to ensure image %q for image mount: %w", ref, err) + return specs.Mount{}, "", "", fmt.Errorf("failed to ensure image %q for image mount: %w", ref, err) } if err := ensured.Image.Unpack(ctx, options.GOptions.Snapshotter); err != nil { - return specs.Mount{}, "", fmt.Errorf("failed to unpack image %q for image mount: %w", ref, err) + return specs.Mount{}, "", "", fmt.Errorf("failed to unpack image %q for image mount: %w", ref, err) } diffIDs, err := ensured.Image.RootFS(ctx) if err != nil { - return specs.Mount{}, "", fmt.Errorf("failed to get rootfs of image %q for image mount: %w", ref, err) + return specs.Mount{}, "", "", fmt.Errorf("failed to get rootfs of image %q for image mount: %w", ref, err) } chainID := identity.ChainID(diffIDs).String() @@ -149,16 +150,31 @@ func setupImageMount(ctx context.Context, client *containerd.Client, options typ gcRootLabel: time.Now().UTC().Format(time.RFC3339), })) if err != nil { - return specs.Mount{}, "", fmt.Errorf("failed to create read-only view of image %q: %w", ref, err) + return specs.Mount{}, "", "", fmt.Errorf("failed to create read-only view of image %q: %w", ref, err) } - // overlayfs and native snapshotters each yield a single mount for a view. - if len(mounts) != 1 { - if rmErr := s.Remove(ctx, snapshotKey); rmErr != nil && !errdefs.IsNotFound(rmErr) { - log.G(ctx).WithError(rmErr).Warnf("failed to remove image-mount snapshot %q", snapshotKey) + defer func() { + if retErr == nil { + return + } + if err := s.Remove(ctx, snapshotKey); err != nil && !errdefs.IsNotFound(err) { + log.G(ctx).WithError(err).Warnf("failed to remove image-mount snapshot %q", snapshotKey) + } + }() + + if subpath != "" { + m, hostMountpoint, err := setupImageSubpathMount(ctx, options, ref, destination, subpath, snapshotKey, mounts) + if err != nil { + return specs.Mount{}, "", "", err } - return specs.Mount{}, "", fmt.Errorf("image mount expects exactly one mount from the snapshotter, got %d", len(mounts)) + return m, snapshotKey, hostMountpoint, nil } + // Whole rootfs: hand the snapshotter's mount straight to the OCI runtime, + // which mounts and unmounts it with the container. overlayfs and native + // snapshotters each yield exactly one mount for a view. + if len(mounts) != 1 { + return specs.Mount{}, "", "", fmt.Errorf("image mount expects exactly one mount from the snapshotter, got %d", len(mounts)) + } m := mounts[0] opts := m.Options // A view without an upper dir is already read-only; make it explicit for @@ -171,13 +187,81 @@ func setupImageMount(ctx context.Context, client *containerd.Client, options typ Source: m.Source, Destination: destination, Options: opts, - }, snapshotKey, nil + }, snapshotKey, "", nil +} + +// setupImageSubpathMount materializes the view on a host dir under the data root +// and returns a read-only bind mount of subpath plus that dir. The dir is +// unmounted and removed if setup fails; otherwise it lives until container +// deletion so the mount survives restarts. +func setupImageSubpathMount(ctx context.Context, options types.ContainerCreateOptions, ref, destination, subpath, snapshotKey string, mounts []mount.Mount) (_ specs.Mount, _ string, retErr error) { + // Keyed by snapshot key so the dir is unique per view. + hostMountpoint := filepath.Join(options.GOptions.DataRoot, "image-mounts", snapshotKey) + // mount.All can apply some mounts before failing, so unmount before RemoveAll + // recurses. Both are no-ops on a missing or never-mounted dir. + defer func() { + if retErr == nil { + return + } + if err := mount.UnmountAll(hostMountpoint, 0); err != nil { + log.G(ctx).WithError(err).Warnf("failed to unmount image-mount host path %q", hostMountpoint) + } + if err := os.RemoveAll(hostMountpoint); err != nil { + log.G(ctx).WithError(err).Warnf("failed to remove image-mount host path %q", hostMountpoint) + } + }() + + if err := os.MkdirAll(hostMountpoint, 0o700); err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to create image-mount host dir: %w", err) + } + if err := mount.All(mounts, hostMountpoint); err != nil { + return specs.Mount{}, "", fmt.Errorf("failed to materialize image %q for subpath mount: %w", ref, err) + } + source, err := resolveImageSubpath(hostMountpoint, subpath) + if err != nil { + return specs.Mount{}, "", fmt.Errorf("image-subpath %q in image %q: %w", subpath, ref, err) + } + // Non-recursive bind: nothing is mounted under the host dir, and "ro" would + // not cover a submount anyway. + return specs.Mount{ + Type: "bind", + Source: source, + Destination: destination, + Options: []string{"bind", "ro"}, + }, hostMountpoint, nil +} + +// resolveImageSubpath returns the bind source for subpath under the materialized +// rootfs. The scoped lookup requires it to exist and rejects absolute or escaping +// symlinks, so any symlink left in the path is relative and stays inside the +// rootfs when mount(2) follows it. +func resolveImageSubpath(rootfs, subpath string) (string, error) { + f, err := os.OpenInRoot(rootfs, subpath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return "", errors.New("does not exist in the image") + } + return "", err + } + defer f.Close() + return f.Name(), nil } -// removeImageMountViews removes the snapshotter views created for type=image -// mounts. NotFound is ignored; other failures are logged but not fatal. -func removeImageMountViews(ctx context.Context, s snapshots.Snapshotter, keys []string) { - for _, k := range keys { +// removeImageMounts tears down type=image mount state for a container: it +// unmounts and removes any host materialization directories (image-subpath), +// then removes the read-only snapshot views. NotFound is ignored; other +// failures are logged but not fatal. +func removeImageMounts(ctx context.Context, s snapshots.Snapshotter, hostpaths, snapshotKeys []string) { + // Unmount host materializations before removing the views they hold open. + for _, p := range hostpaths { + if err := mount.UnmountAll(p, 0); err != nil { + log.G(ctx).WithError(err).Warnf("failed to unmount image-mount host path %q", p) + } + if err := os.RemoveAll(p); err != nil { + log.G(ctx).WithError(err).Warnf("failed to remove image-mount host path %q", p) + } + } + for _, k := range snapshotKeys { if err := s.Remove(ctx, k); err != nil && !errdefs.IsNotFound(err) { log.G(ctx).WithError(err).Warnf("failed to remove image-mount snapshot %q", k) } @@ -190,14 +274,15 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm volStore volumestore.VolumeStore, options types.ContainerCreateOptions) (opts []oci.SpecOpts, anonVolumes []string, mountPoints []*mountutil.Processed, retErr error) { //nolint:prealloc var ( - userMounts []specs.Mount - imageMountViews []string + userMounts []specs.Mount + imageMountViews []string + imageMountHostpaths []string ) - // Remove any image-mount views created here if this function fails, so a - // partial setup does not leak snapshots. + // Tear down any image-mount state created here if this function fails, so a + // partial setup does not leak snapshots or host mounts. defer func() { - if retErr != nil && len(imageMountViews) > 0 { - removeImageMountViews(ctx, client.SnapshotService(options.GOptions.Snapshotter), imageMountViews) + if retErr != nil && (len(imageMountViews) > 0 || len(imageMountHostpaths) > 0) { + removeImageMounts(ctx, client.SnapshotService(options.GOptions.Snapshotter), imageMountHostpaths, imageMountViews) } }() mounted := make(map[string]struct{}) @@ -299,13 +384,17 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm // type=image: build the read-only view now and record its snapshot // key for cleanup on container removal. if x.Type == mountutil.Image { - m, snapshotKey, err := setupImageMount(ctx, client, options, x.Mount.Source, x.Mount.Destination) + m, snapshotKey, hostMountpoint, err := setupImageMount(ctx, client, options, x.Mount.Source, x.Mount.Destination, x.ImageSubpath) if err != nil { return nil, nil, nil, err } imageMountViews = append(imageMountViews, snapshotKey) + if hostMountpoint != "" { + imageMountHostpaths = append(imageMountHostpaths, hostMountpoint) + } ociMounts[i] = m x.ImageMountSnapshot = snapshotKey + x.ImageMountHostpath = hostMountpoint mounted[filepath.Clean(x.Mount.Destination)] = struct{}{} continue } diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 9f689ff7cff..c665e3dece5 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -84,6 +84,11 @@ const ( // the read-only views backing `--mount type=image`, removed on container deletion. ImageMountSnapshots = Prefix + "image-mount-snapshots" + // ImageMountHostpaths is a JSON-marshalled []string of host directories where + // `--mount type=image,image-subpath=...` rootfs views are materialized; each + // must be unmounted and removed on container deletion. + ImageMountHostpaths = Prefix + "image-mount-hostpaths" + // Platform is the normalized platform string like "linux/ppc64le". Platform = Prefix + "platform" diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index 350d4443376..986edd4dea8 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -55,6 +55,13 @@ type Processed struct { // ImageMountSnapshot is the snapshotter key of the read-only view for a // type=image mount; empty for other mount types. ImageMountSnapshot string + // ImageSubpath is the relative path inside a type=image rootfs to expose at + // the destination, instead of the whole rootfs. Empty means the whole rootfs. + ImageSubpath string + // ImageMountHostpath is the host directory where a type=image rootfs is + // materialized so an image-subpath can be bind-mounted from it. It must be + // unmounted and removed on container deletion. Empty when no subpath is used. + ImageMountHostpath string } type volumeSpec struct { diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 46ed18cb892..78aa67a75ea 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -21,6 +21,7 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" "strconv" "strings" @@ -360,6 +361,29 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { return res, nil } +// validateImageSubpath normalizes an image-subpath, rejecting absolute paths and +// ones escaping the rootfs. A value resolving to the rootfs itself returns empty, +// the whole-rootfs case Docker accepts. Image paths are always forward-slash. +func validateImageSubpath(p string) (string, error) { + if p == "" { + return "", nil + } + if path.IsAbs(p) { + return "", fmt.Errorf("image-subpath must be relative to the image rootfs, got %q", p) + } + clean := path.Clean(p) + // Clean collapses ".." segments; anything still leading with ".." escapes root. + if clean == ".." || strings.HasPrefix(clean, "../") { + return "", fmt.Errorf("image-subpath %q escapes the image rootfs", p) + } + // "." is the whole rootfs (e.g. from "a/.."); treat it as no subpath so the + // caller mounts the full image view, matching Docker. + if clean == "." { + return "", nil + } + return clean, nil +} + func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime string) (*Processed, error) { fields := strings.Split(s, ",") var ( @@ -371,6 +395,8 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str bindRecursive string // "enabled", "disabled", "writable", or "readonly" volumeNoCopy bool rwOption string + imageSubpath string + imageSubpathSet bool tmpfsSize int64 tmpfsMode os.FileMode err error @@ -445,9 +471,11 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str rwOption = key } case "image-subpath": - // image-subpath is Docker's option to mount a subdirectory of the - // image; it is not implemented yet. - return nil, fmt.Errorf("mount option %q is not yet supported", key) + // Selects a directory inside a type=image rootfs; validated below once + // the mount type is known. Presence is tracked separately from the + // value so that an explicit empty value is not read as "unset". + imageSubpath = value + imageSubpathSet = true case "bind-propagation": // here don't validate the propagation value // parseVolumeOptions will do that. @@ -493,6 +521,18 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str return nil, fmt.Errorf("the option 'volume-nocopy' is only supported for volume mounts") } + // Check presence, not value: an explicit empty image-subpath is an error on + // every type, and on other types the option itself is rejected before falling + // through to the legacy bind/volume/tmpfs handlers. Both match Docker. + if imageSubpathSet { + if imageSubpath == "" { + return nil, fmt.Errorf("invalid value for image-subpath: value is empty") + } + if mountType != Image { + return nil, fmt.Errorf("image-subpath is only supported for type=image") + } + } + // type=image's source is an image reference resolved later with a containerd // client; validate the intent here. Like Docker, an image mount is always // read-only: a readonly/ro option is accepted for compatibility but the @@ -504,6 +544,12 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str if dst == "" { return nil, fmt.Errorf("type=image requires a destination") } + // Bound the subpath at parse time; symlinks are checked once the rootfs + // is materialized. + cleanSubpath, err := validateImageSubpath(imageSubpath) + if err != nil { + return nil, err + } return &Processed{ Type: Image, // Mode "ro" so inspect/label metadata reports the mount read-only. @@ -513,6 +559,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str Source: src, Destination: cleanMount(dst), }, + ImageSubpath: cleanSubpath, }, nil } diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 016b6b619b4..a93287ff9d9 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -645,8 +645,98 @@ func TestProcessFlagMountImage(t *testing.T) { }, }, { + // bare subpath is not a type=image option; image-subpath is. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,subpath=etc", + err: "subpath", + }, + { + // image-subpath selects a directory inside the image rootfs. rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=etc", - err: "image-subpath", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + ImageSubpath: "etc", + }, + }, + { + // image-subpath is normalized: leading ./ and trailing / are stripped. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=./etc/", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + ImageSubpath: "etc", + }, + }, + { + // parent traversal must be rejected before the mount is built. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=../etc", + err: "escapes", + }, + { + // traversal that normalizes back above root must be rejected. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=a/b/../../../etc", + err: "escapes", + }, + { + // a path normalizing to "." is the whole rootfs; like Docker, this is + // the no-subpath case rather than an error. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=.", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + }, + }, + { + // "a/.." also normalizes to the rootfs, so it is the whole-rootfs mount. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=a/..", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + }, + }, + { + // nested subpath is normalized and preserved. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=usr/lib", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + ImageSubpath: "usr/lib", + }, + }, + { + // absolute image-subpath is rejected; it must be relative to the rootfs. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=/etc", + err: "relative", + }, + { + // image-subpath only applies to type=image. + rawSpec: "type=bind,source=/tmp,destination=/mnt,image-subpath=etc", + err: "only supported for type=image", + }, + { + // an explicitly empty image-subpath is an error, not "unset": Docker + // rejects it on every mount type. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=", + err: "value is empty", + }, + { + rawSpec: "type=bind,source=/tmp,destination=/mnt,image-subpath=", + err: "value is empty", + }, + { + // a subpath whose ".." segments cancel out is normalized, like Docker. + rawSpec: "type=image,source=alpine:latest,destination=/mnt/img,image-subpath=a/../etc", + wants: &Processed{ + Type: Image, + Mode: "ro", + Mount: specs.Mount{Type: Image, Source: "alpine:latest", Destination: "/mnt/img"}, + ImageSubpath: "etc", + }, }, } for _, tt := range tests { @@ -662,6 +752,7 @@ func TestProcessFlagMountImage(t *testing.T) { assert.Equal(t, got.Mount.Type, tt.wants.Mount.Type) assert.Equal(t, got.Mount.Source, tt.wants.Mount.Source) assert.Equal(t, got.Mount.Destination, tt.wants.Mount.Destination) + assert.Equal(t, got.ImageSubpath, tt.wants.ImageSubpath) }) } } From f101221e1f381ae62927002122cd71386afcf152 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 14 Sep 2026 18:37:54 +0900 Subject: [PATCH 821/868] ci: report gosocialcheck findings as GitHub Actions annotations gosocialcheck flags the dependencies that do not appear to be adopted by a trusted project (CNCF Graduated). Group the containerd, Docker/Moby, OCI and golang.org/x requirements into `gosocialcheck:trusted` blocks in go.mod, add a `lint-gosocialcheck` target, and run it in CI with `--gha`, so that the remaining findings show up as annotations on the run and on the PR. `--gha` always exits 0, so this is advisory rather than a gate. The target is therefore left out of the blocking `make lint` aggregate, and CI opts into the flag via GOSOCIALCHECK_FLAGS, so that a local run still reports a real exit status. The job lives in its own job-lint-deps.yml, following the job-lint-* pattern, so that the other linters that vet go.mod can join it later. GITHUB_TOKEN is passed to the run step, as gosocialcheck queries the GitHub API and is otherwise rate-limited. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .github/workflows/job-lint-deps.yml | 66 +++++++++++++++++++++++ .github/workflows/workflow-lint.yml | 10 ++++ Makefile | 18 ++++++- go.mod | 81 ++++++++++++++++------------- 4 files changed, 137 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/job-lint-deps.yml diff --git a/.github/workflows/job-lint-deps.yml b/.github/workflows/job-lint-deps.yml new file mode 100644 index 00000000000..2ebcbb2d403 --- /dev/null +++ b/.github/workflows/job-lint-deps.yml @@ -0,0 +1,66 @@ +# This job runs the linters that vet the dependency tree declared in go.mod. +# +# - gosocialcheck reports the dependencies which do not appear to be adopted by a trusted +# project (CNCF Graduated). Modules that are trusted anyway are annotated +# `gosocialcheck:trusted` in go.mod. +# https://github.com/AkihiroSuda/gosocialcheck +# This is advisory, not a gate: `--gha` reports the findings as annotations on the run and +# on the PR, and always exits 0. +# To run locally, use `make lint-gosocialcheck` (without `--gha`, so it does exit non-zero). +name: job-lint-deps + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + go-version: + required: true + type: string + runner: + required: true + type: string + +env: + GOTOOLCHAIN: local + +jobs: + lint-deps: + name: "dependencies" + timeout-minutes: ${{ inputs.timeout }} + runs-on: ${{ inputs.runner }} + defaults: + run: + shell: bash + + steps: + - name: "Init: checkout" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # gosocialcheck's `--gha` fetches the base branch on demand to rank the findings whose + # go.sum line changed in the PR first, so a shallow checkout is enough. + fetch-depth: 1 + persist-credentials: false + + - name: "Init: install go" + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ inputs.go-version }} + check-latest: true + # The calling workflow also runs on push, so the setup-go cache is disabled + # for zizmor's cache-poisoning audit + cache: false + + - name: "Init: install dev-tools" + run: | + echo "::group:: make install-dev-tools" + make install-dev-tools + echo "::endgroup::" + + - name: "Run: gosocialcheck" + env: + # gosocialcheck queries the GitHub API, which is heavily rate-limited when anonymous + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + NO_COLOR=true make lint-gosocialcheck GOSOCIALCHECK_FLAGS=--gha diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index d9c9cbf5799..bb55717dff6 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -92,6 +92,16 @@ jobs: run: | NO_COLOR=true make lint-gomodjail-all + # Report the dependencies that do not appear to be adopted by a trusted project. + # Advisory only: this job always succeeds, the findings show up as annotations. + lint-deps: + name: "deps" + uses: ./.github/workflows/job-lint-deps.yml + with: + timeout: 10 + go-version: "1.26" + runner: ubuntu-26.04 + # Lint for shell and yaml files lint-other: name: "other" diff --git a/Makefile b/Makefile index 7aa72b0e128..fcc2e87aa1b 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,7 @@ REVISION ?= $(shell git -C $(MAKEFILE_DIR) rev-parse HEAD 2>/dev/null || echo no LINT_COMMIT_RANGE ?= main..HEAD GO_BUILD_LDFLAGS ?= -s -w GO_BUILD_FLAGS ?= +GOSOCIALCHECK_FLAGS ?= BUILDTAGS ?= GO_TAGS=$(if $(BUILDTAGS),-tags "$(strip $(BUILDTAGS))",) @@ -200,6 +201,17 @@ lint-gomodjail-all: && GOOS=linux GOARCH=arm64 make lint-gomodjail $(call footer, $@) +# gosocialcheck reports dependencies that do not appear to be adopted by a trusted project +# (CNCF Graduated). Modules that are trusted anyway are annotated `gosocialcheck:trusted` in go.mod. +# https://github.com/AkihiroSuda/gosocialcheck +# Not part of `make lint`: the verdict is advisory, and CI runs it with GOSOCIALCHECK_FLAGS=--gha, +# which reports findings as workflow annotations and always exits 0. +lint-gosocialcheck: + $(call title, $@) + @cd $(MAKEFILE_DIR) \ + && gosocialcheck run $(GOSOCIALCHECK_FLAGS) ./... + $(call footer, $@) + # FIXME: go-licenses cannot find LICENSE from root of repo when submodule is imported: # https://github.com/google/go-licenses/issues/186 # This is impacting gotest.tools @@ -273,12 +285,14 @@ install-dev-tools: # ltag: v0.3.0 (2025-03-04) # gotestsum: v1.13.0 (2025-09-11) # go-licenses: v2.0.1 (2025-09-08) + # gosocialcheck: v0.2.0 (2026-09-11) @cd $(MAKEFILE_DIR) \ && go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 \ && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ - && go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f + && go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f \ + && go install github.com/AkihiroSuda/gosocialcheck/cmd/gosocialcheck@2c7caa6b92b1661a3a767cd8b768a49fc640016a # gomodjail: v2.0.1 (2026-09-09) # Not installed on Windows hosts: gomodjail does not build there, as its dynamic mode # is compiled in unconditionally (https://github.com/AkihiroSuda/gomodjail) @@ -367,7 +381,7 @@ artifacts: clean install \ uninstall \ clean \ - lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-gomodjail lint-gomodjail-all lint-licenses lint-licenses-all \ + lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-gomodjail lint-gomodjail-all lint-gosocialcheck lint-licenses lint-licenses-all \ fix-go fix-go-all fix-mod fix-gomodjail \ install-dev-tools \ test-unit test-unit-race test-unit-bench \ diff --git a/go.mod b/go.mod index 7eb4aee36c9..f7b37a3c315 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,9 @@ module github.com/containerd/nerdctl/v2 go 1.26.3 +// containerd, Docker/Moby, OCI, and golang.org/x packages are trusted +//gosocialcheck:trusted require ( - github.com/Masterminds/semver/v3 v3.5.0 - github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 - github.com/Microsoft/hcsshim v0.15.0-rc.4 - github.com/compose-spec/compose-go/v2 v2.15.0 //gomodjail:unconfined github.com/containerd/accelerated-container-image v1.4.4 //gomodjail:unconfined github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined @@ -26,15 +24,39 @@ require ( 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.3.0 + github.com/docker/cli v29.8.0+incompatible //gomodjail:unconfined + github.com/docker/go-connections v0.8.1 //gomodjail:unconfined + github.com/docker/go-units v0.5.0 + github.com/moby/moby/client v0.6.0 //gomodjail:unconfined + github.com/moby/moby/v2 v2.0.0-beta.23 //gomodjail:unconfined + github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined + github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined + github.com/moby/sys/user v0.4.1 //gomodjail:unconfined + github.com/moby/sys/userns v0.2.1 //gomodjail:unconfined + github.com/moby/term v0.5.2 //gomodjail:unconfined + 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.1 //gomodjail:unconfined + golang.org/x/crypto v0.57.0 + golang.org/x/net v0.59.0 //gomodjail:unconfined + golang.org/x/sync v0.23.0 //gomodjail:unconfined + golang.org/x/sys v0.48.0 //gomodjail:unconfined + golang.org/x/term v0.46.0 //gomodjail:unconfined + golang.org/x/text v0.42.0 +) + +require ( + github.com/Masterminds/semver/v3 v3.5.0 + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/hcsshim v0.15.0-rc.4 + github.com/compose-spec/compose-go/v2 v2.15.0 //gomodjail:unconfined github.com/containernetworking/cni v1.3.1 //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 //gomodjail:unconfined github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.8.0+incompatible //gomodjail:unconfined - github.com/docker/go-connections v0.8.1 //gomodjail:unconfined - github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined github.com/fluent/fluent-logger-golang v1.10.1 //gomodjail:unconfined @@ -43,18 +65,7 @@ require ( github.com/ipfs/go-cid v0.6.2 //gomodjail:unconfined github.com/klauspost/compress v1.20.0 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined - github.com/moby/moby/client v0.6.0 //gomodjail:unconfined - github.com/moby/moby/v2 v2.0.0-beta.23 //gomodjail:unconfined - github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined - github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined - github.com/moby/sys/user v0.4.1 //gomodjail:unconfined - github.com/moby/sys/userns v0.2.1 //gomodjail:unconfined - github.com/moby/term v0.5.2 //gomodjail:unconfined github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined - 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.1 //gomodjail:unconfined github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 //gomodjail:unconfined @@ -64,20 +75,12 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 //gomodjail:unconfined go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.57.0 - golang.org/x/net v0.59.0 //gomodjail:unconfined - golang.org/x/sync v0.23.0 //gomodjail:unconfined - golang.org/x/sys v0.48.0 //gomodjail:unconfined - golang.org/x/term v0.46.0 //gomodjail:unconfined - golang.org/x/text v0.42.0 gotest.tools/v3 v3.5.2 //gomodjail:unconfined tags.cncf.io/container-device-interface v1.1.1 //gomodjail:unconfined ) +//gosocialcheck:trusted require ( - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - //gomodjail:unconfined - github.com/cilium/ebpf v0.22.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect //gomodjail:unconfined github.com/containerd/go-runc v1.2.1 // indirect @@ -85,13 +88,26 @@ require ( //gomodjail:unconfined github.com/containerd/ttrpc v1.2.9 // indirect //gomodjail:unconfined + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/moby/api v1.56.0 // indirect + //gomodjail:unconfined + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/symlink v0.3.0 // indirect + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + //gomodjail:unconfined + github.com/cilium/ebpf v0.22.0 // indirect + //gomodjail:unconfined github.com/containers/ocicrypt v1.3.2 // indirect //gomodjail:unconfined github.com/creack/pty v1.1.24 // indirect //gomodjail:unconfined github.com/djherbis/times v1.6.0 // indirect - //gomodjail:unconfined - github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.4 // indirect @@ -110,11 +126,6 @@ require ( github.com/minio/sha256-simd v1.0.1 // indirect //gomodjail:unconfined github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/locker v1.0.1 // indirect - //gomodjail:unconfined - github.com/moby/sys/mountinfo v0.7.2 // 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 github.com/multiformats/go-base36 v0.2.0 // indirect @@ -148,7 +159,6 @@ require ( go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect - golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect //gomodjail:unconfined google.golang.org/grpc v1.83.2 // indirect @@ -168,7 +178,6 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect //gomodjail:unconfined github.com/google/uuid v1.6.0 // indirect - github.com/moby/moby/api v1.56.0 // indirect go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect //gomodjail:unconfined From 8acf8779eafefc247b4df0986a302e7464803083 Mon Sep 17 00:00:00 2001 From: Arman Balian Date: Wed, 16 Sep 2026 15:55:28 +0400 Subject: [PATCH 822/868] fix: allow removing containers without network annotations Signed-off-by: Arman Balian --- .../container/container_remove_linux_test.go | 90 +++++++++++++++++++ pkg/cmd/container/remove.go | 19 ++-- 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/cmd/nerdctl/container/container_remove_linux_test.go b/cmd/nerdctl/container/container_remove_linux_test.go index 3e15da38ad7..70e7673227c 100644 --- a/cmd/nerdctl/container/container_remove_linux_test.go +++ b/cmd/nerdctl/container/container_remove_linux_test.go @@ -18,6 +18,7 @@ package container import ( "fmt" + "os/exec" "strconv" "strings" "testing" @@ -28,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -129,3 +131,91 @@ func TestContainerRmIptables(t *testing.T) { testCase.Run(t) } + +func TestRemoveContainerWithoutNetworkAnnotation(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.OnlyKubernetes + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier() + namespace := identifier + containerID := "" + + kubectlPath, _ := exec.LookPath("kubectl") + + createNamespace := helpers.Custom(kubectlPath) + createNamespace.WithArgs("create", "namespace", namespace) + createNamespace.Run(&test.Expected{}) + + kube := func(args ...string) test.TestableCommand { + cmd := helpers.Custom(kubectlPath) + cmd.WithArgs("--namespace=" + namespace) + cmd.WithArgs(args...) + return cmd + } + + // Kubernetes creates containers through CRI in the k8s.io containerd + // namespace. These containers do not have nerdctl-specific network + // annotations, which reproduces the scenario from #5207. + kube( + "run", + "--restart=Never", + "--image", + testutil.CommonImage, + identifier, + "--", + "sleep", + nerdtest.Infinity, + ).Run(&test.Expected{}) + + cmd := kube( + "wait", + "pod", + identifier, + "--for=condition=ready", + "--timeout=1m", + ) + cmd.WithTimeout(70 * time.Second) + cmd.Run(&test.Expected{}) + + // Retrieve the actual containerd container ID created by Kubernetes. + kube( + "get", + "pods", + identifier, + "-o", + "jsonpath={ .status.containerStatuses[0].containerID }", + ).Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + containerID = strings.TrimPrefix(stdout, "containerd://") + }, + }) + + data.Labels().Set("containerID", containerID) + + // Stop the CRI-created container without deleting it from containerd. + // nerdctl rm must still succeed even without nerdctl network metadata. + helpers.Ensure("stop", containerID) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + kubectlPath, err := exec.LookPath("kubectl") + if err != nil { + return + } + + cmd := helpers.Custom(kubectlPath) + cmd.WithArgs("delete", "namespace", data.Identifier(), "--ignore-not-found=true") + cmd.WithTimeout(30 * time.Second) + cmd.Run(nil) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("rm", data.Labels().Get("containerID")) + } + + testCase.Expected = test.Expects(0, nil, nil) + + testCase.Run(t) +} diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index d00332bcc02..f946c91122c 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -203,21 +203,14 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions retErr = err return } - netOpts, err := containerutil.NetworkOptionsFromSpec(spec) - if err != nil { - retErr = err - return - } - - portSlice, err := portutil.LoadPortMappings(dataStore, globalOptions.Namespace, id, containerLabels) - if err != nil { - retErr = err - return - } - netOpts.PortMappings = portSlice - if err == nil { + portSlice, err := portutil.LoadPortMappings(dataStore, globalOptions.Namespace, id, containerLabels) + if err != nil { + retErr = err + return + } + netOpts.PortMappings = portSlice networkManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netOpts, client) if err != nil { retErr = fmt.Errorf("failed to instantiate network options manager: %w", err) From 436b22ad52758e40cad106dd83516f6650a6f8fc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 16 Sep 2026 10:16:18 +0900 Subject: [PATCH 823/868] update kubo (0.43.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3238c2c3e6e..40a15bd59e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG UBUNTU_VERSION=26.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 ARG NYDUS_VERSION=v2.4.5 ARG SOCI_SNAPSHOTTER_VERSION=0.15.0 -ARG KUBO_VERSION=v0.43.0 +ARG KUBO_VERSION=v0.43.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From e412c3f73ce53d84b7346742b929fde502696e5e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 11:19:35 +0900 Subject: [PATCH 824/868] update Docker (29.8.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 3318d234247..31a23cd4257 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -153,6 +153,6 @@ jobs: no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.3 - docker-version: 5:29.8.0-1~ubuntu.26.04~resolute + docker-version: 5:29.8.1-1~ubuntu.26.04~resolute windows-containerd-version: 2.4.0-beta.0 windows-containerd-sha: ae0df8baf1556a292da69b185d4912ddb955ecc2e47d1e0b754233b2df93acc9 From ae01f827644aecf62d11d6b98d163ffc14ad58bb Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 11:27:15 +0900 Subject: [PATCH 825/868] update containerd (2.4.0) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 31a23cd4257..0c63ba0f25c 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -154,5 +154,5 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.3 docker-version: 5:29.8.1-1~ubuntu.26.04~resolute - windows-containerd-version: 2.4.0-beta.0 - windows-containerd-sha: ae0df8baf1556a292da69b185d4912ddb955ecc2e47d1e0b754233b2df93acc9 + windows-containerd-version: 2.4.0 + windows-containerd-sha: cadffa872e2385e5eccfaf0425d4f7be8571e6d2118dbb090efa0f681d5083d9 diff --git a/Dockerfile b/Dockerfile index 40a15bd59e5..4f22bb0f5f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.4.0-beta.0@58e6cee9dee67ee87ee2f727d3570009050f9954 +ARG CONTAINERD_VERSION=v2.4.0@a7fe631d96c08fb14cf8eff0afdc280e99c30a94 ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From c1843e336b75067e332fb6035cbc834d41f408c9 Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Thu, 17 Sep 2026 11:23:29 +0400 Subject: [PATCH 826/868] fix(compose): allow detached runs with default interactive setting Signed-off-by: Immanuel Tikhonov --- cmd/nerdctl/compose/compose_run.go | 3 +++ cmd/nerdctl/compose/compose_run_linux_test.go | 20 +++++++++++++++++++ docs/command-reference.md | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_run.go b/cmd/nerdctl/compose/compose_run.go index bbfd98d8008..b396a16a7f3 100644 --- a/cmd/nerdctl/compose/compose_run.go +++ b/cmd/nerdctl/compose/compose_run.go @@ -119,6 +119,9 @@ func runAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + if detach && !cmd.Flags().Changed("interactive") { + interactive = false + } // FIXME : https://github.com/containerd/nerdctl/blob/v0.22.2/cmd/nerdctl/run.go#L100 tty := interactive rm, err := cmd.Flags().GetBool("rm") diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index c761a121401..d357b8ca3b3 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -131,6 +131,26 @@ services: testCase.Run(t) } +func TestComposeRunDetached(t *testing.T) { + dockerComposeYAML := fmt.Sprintf(` +services: + alpine: + image: %s + network_mode: none +`, testutil.CommonImage) + + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "run", "-d", "--name", data.Identifier(), "alpine", "sleep", "1h") + } + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + testCase.Cleanup = composeRunCleanup() + testCase.Run(t) +} + func TestComposeRunWithServicePorts(t *testing.T) { testCase := nerdtest.Setup() // A background compose run holds the global compose lock until cleanup. diff --git a/docs/command-reference.md b/docs/command-reference.md index 6d61bf93df3..bf6ceb2a0ed 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1985,7 +1985,7 @@ Flags: - :whale: `-d, —detach`: Detached mode: Run containers in the background. - :whale: `--entrypoint`: Overwrite the default ENTRYPOINT of the image. - :whale: `-e, —env`: Set environment variables. -- :whale: `-i, —interactive`: Keep STDIN open even if not attached (default true). +- :whale: `-i, —interactive`: Keep STDIN open even if not attached (default true; false with `--detach`). - :whale: `-l, —label`: Set metadata on container. - :whale: `--name`: Assign a name to the container. - :whale: `--no-build`: Don't build an image, even if it's missing. From 2e01d5c641767b97b941ef186795563eadd6bc33 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 17:47:38 +0900 Subject: [PATCH 827/868] CI: fix zizmor failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/containerd/nerdctl/actions/runs/35201153094/job/105135950811 ``` help[self-repository]: use GitHub's dedicated self-repository syntax --> .github/workflows/workflow-lint.yml:99:11 | 98 | name: "deps" | ------------ this job 99 | uses: ./.github/workflows/job-lint-deps.yml | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use '$/...' instead of './...' | = note: audit confidence → High = note: this finding has an auto-fix = help: audit documentation → https://docs.zizmor.sh/audits/#self-repository ``` Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 920a2331f40..b48beeadd0f 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -96,7 +96,7 @@ jobs: # Advisory only: this job always succeeds, the findings show up as annotations. lint-deps: name: "deps" - uses: ./.github/workflows/job-lint-deps.yml + uses: $/.github/workflows/job-lint-deps.yml with: timeout: 10 go-version: "1.26" From 71d237ae6d9e5e9f8e61eb18bc5b748a766e664c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 17:42:42 +0900 Subject: [PATCH 828/868] go.mod: fix groupification Signed-off-by: Akihiro Suda --- go.mod | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index f7b37a3c315..be5ec9f978b 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 //gomodjail:unconfined go.yaml.in/yaml/v3 v3.0.5 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba gotest.tools/v3 v3.5.2 //gomodjail:unconfined tags.cncf.io/container-device-interface v1.1.1 //gomodjail:unconfined ) @@ -99,9 +100,13 @@ require ( ) require ( + cyphar.com/go-pathrs v0.2.5 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect //gomodjail:unconfined github.com/cilium/ebpf v0.22.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect //gomodjail:unconfined github.com/containers/ocicrypt v1.3.2 // indirect //gomodjail:unconfined @@ -115,6 +120,8 @@ require ( //gomodjail:unconfined github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + //gomodjail:unconfined + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect //gomodjail:unconfined github.com/klauspost/cpuid/v2 v2.2.8 // indirect @@ -158,7 +165,9 @@ require ( //gomodjail:unconfined go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/sdk v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect //gomodjail:unconfined google.golang.org/grpc v1.83.2 // indirect @@ -166,22 +175,9 @@ require ( google.golang.org/protobuf v1.36.12 // indirect //gomodjail:unconfined lukechampine.com/blake3 v1.3.0 // indirect - tags.cncf.io/container-device-interface/specs-go v1.1.1 // indirect -) - -require go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - -require ( - cyphar.com/go-pathrs v0.2.5 // indirect - github.com/ProtonMail/go-crypto v1.4.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect - //gomodjail:unconfined - github.com/google/uuid v1.6.0 // indirect - go.opentelemetry.io/otel/sdk v1.46.0 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect //gomodjail:unconfined sigs.k8s.io/knftables v0.0.18 // indirect + tags.cncf.io/container-device-interface/specs-go v1.1.1 // indirect ) replace github.com/containerd/nerdctl/mod/tigron v0.0.0 => ./mod/tigron From 0cf480552e8881f43f169142041216f678c6d79f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 17:43:57 +0900 Subject: [PATCH 829/868] go.mod: github.com/containerd/containerd/v2 v2.4.0 Signed-off-by: Akihiro Suda --- go.mod | 9 +++++---- go.sum | 14 ++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index be5ec9f978b..953cf838455 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.26.3 +go 1.26.6 // containerd, Docker/Moby, OCI, and golang.org/x packages are trusted //gosocialcheck:trusted @@ -9,8 +9,8 @@ require ( github.com/containerd/accelerated-container-image v1.4.4 //gomodjail:unconfined 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.12.0-rc.0 //gomodjail:unconfined - github.com/containerd/containerd/v2 v2.4.0-beta.0 //gomodjail:unconfined + github.com/containerd/containerd/api v1.12.0 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.4.0 //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 @@ -85,6 +85,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect //gomodjail:unconfined github.com/containerd/go-runc v1.2.1 // indirect + github.com/containerd/log/otel v0.1.0 // indirect github.com/containerd/plugin v1.1.0 // indirect //gomodjail:unconfined github.com/containerd/ttrpc v1.2.9 // indirect @@ -114,7 +115,7 @@ require ( //gomodjail:unconfined github.com/djherbis/times v1.6.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect //gomodjail:unconfined diff --git a/go.sum b/go.sum index da06a81eb07..a774b039f1b 100644 --- a/go.sum +++ b/go.sum @@ -28,10 +28,10 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd/api v1.12.0-rc.0 h1:Uu/0brBDz8nW/Vo+g8ah/iJkhZML+1z+X44g8q70MVw= -github.com/containerd/containerd/api v1.12.0-rc.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc= -github.com/containerd/containerd/v2 v2.4.0-beta.0 h1:uFwtEE0kwGngxOT0WVW3/mozESCVg+8hKUExSis7SLY= -github.com/containerd/containerd/v2 v2.4.0-beta.0/go.mod h1:GOAqkxqN53nSzBcz9Y5jAM9JELRMo67r34O/HDCY4Ro= +github.com/containerd/containerd/api v1.12.0 h1:kuQm82SbDrCuO4n7hf2L8zsBtZLuympyq5X/VotfX2A= +github.com/containerd/containerd/api v1.12.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc= +github.com/containerd/containerd/v2 v2.4.0 h1:mZLYWkPAgF4tfTjinClrRfvUxmxHxg1PQL7cjrrpTtk= +github.com/containerd/containerd/v2 v2.4.0/go.mod h1:gHZz+v5y8mGt9grF3ynuaa3r6bXLmghBZWTAftHAUtg= 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= @@ -48,6 +48,8 @@ github.com/containerd/imgcrypt/v2 v2.0.3 h1:yM6//IOTtca9TpxPP8prJqLDuidbF/VhUQuj github.com/containerd/imgcrypt/v2 v2.0.3/go.mod h1:Sjfd3uOiBWtb1dUo1yS5Z/ZxdfMU7MpBr2pzs9HoRDs= github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= +github.com/containerd/log/otel v0.1.0 h1:Az5rFFo0+c4v2yC8ROR63sWsZpKl/vhtUnUhqLSpE6U= +github.com/containerd/log/otel v0.1.0/go.mod h1:65C5iYF2xIQByCyxzoO2KKKK1+/tGxD4XqhdlrTtvzE= github.com/containerd/nydus-snapshotter v0.15.16 h1:FkAmNADhOQFeWZtrHcDz04ZoOKsIeKZtKbz7PHxIFtc= github.com/containerd/nydus-snapshotter v0.15.16/go.mod h1:x+/i5WkV8w4jSXbdv57YSMWvkzT2JFwpeIDJxvcYe38= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= @@ -105,8 +107,8 @@ github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOe github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= -github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= +github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From 6f2c5a140bd9f354e6f07d3ccef4e85f742088d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:53:12 +0000 Subject: [PATCH 830/868] build(deps): bump zizmorcore/zizmor-action from 0.6.3 to 0.6.4 Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.3 to 0.6.4. - [Release notes](https://github.com/zizmorcore/zizmor-action/releases) - [Commits](https://github.com/zizmorcore/zizmor-action/compare/70fb788f84895a7701f5643d103d587e460b5c99...cc914d7f3750a2d13d75c7f184a1060aa0e9d482) --- updated-dependencies: - dependency-name: zizmorcore/zizmor-action dependency-version: 0.6.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/workflow-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index b48beeadd0f..41c15d9bcec 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -139,7 +139,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: "Run: zizmor" - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 with: inputs: .github/workflows # Do not send workflow diagnostics to GitHub Advanced Security code scanning in this project right now. From d58bdc2822f15f96c2d5a062a684e2ff38c5d694 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 18:01:35 +0900 Subject: [PATCH 831/868] mod/soigneur: fix the workflow filter `index(f)` evaluates f against its own input, which here is $wanted, the array of workflow file names - not the run being filtered. So `.workflow` indexed an array, and the collector died before it collected anything: jq: error (at :0): Cannot index array with string "workflow" Feed the name to IN instead, which takes the run as its input. This only ever failed when `workflows` was set, because `or` short circuits and the empty list takes the other branch. Setting it is what the nerdctl workflow does, and leaving it unset is what every local run of the collector did, which is how this shipped. Fixes https://github.com/containerd/nerdctl/issues/5216 Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- mod/soigneur/flaky-report.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mod/soigneur/flaky-report.sh b/mod/soigneur/flaky-report.sh index 6fa412e1174..8aceb925153 100755 --- a/mod/soigneur/flaky-report.sh +++ b/mod/soigneur/flaky-report.sh @@ -135,7 +135,9 @@ soigneur::runs(){ workflow: (.path | sub("^\\.github/workflows/"; "")), at: .created_at } - | select(($wanted | length) == 0 or ($wanted | index(.workflow)))' + # `index` evaluates its argument against its own input, which is $wanted, the array: + # the name to look for has to reach it another way. + | select(($wanted | length) == 0 or (.workflow | IN($wanted[])))' } # A job log holds whatever the tests printed, so a test that prints a line shaped like a marker @@ -329,6 +331,9 @@ soigneur::main(){ jq -r '.markdown' < "$tmp"/report.json } +# Sourcing this script defines its functions and runs nothing: that is how test.sh reaches them. +[ "${BASH_SOURCE[0]}" == "${0}" ] || return 0 + case "${1:-}" in --collect-run) # Invoked as a subprocess, one per run: see SOIGNEUR_PARALLEL. From bc1981440ab0672bf3d92ca9aee2d28421ce168e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 18:01:44 +0900 Subject: [PATCH 832/868] mod/soigneur: test the collector, on every branch it has A dashboard that runs once a week takes a week to tell you it is broken, and the configuration that broke in #5216 - a workflow filter - was the one no local run ever used. So the module gets a test for the branches that the report itself does not walk: the filter with and without names, the markers and the block that predates them, the names a hostile log can carry, the aggregation of a parent test with its subtests, and the validators. Everything is offline: the API is stubbed, and the fixtures are written by the test. The scripts only dispatch when executed, so that sourcing one defines its functions and runs nothing. The workflow follows mod/tigron's: every push to main, and the pull requests that touch the module, or the reporter on the other side of the marker contract. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-soigneur.yml | 37 +++++ mod/soigneur/flaky-annotate.sh | 3 + mod/soigneur/flaky-issue.sh | 3 + mod/soigneur/test.sh | 197 ++++++++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 .github/workflows/workflow-soigneur.yml create mode 100755 mod/soigneur/test.sh diff --git a/.github/workflows/workflow-soigneur.yml b/.github/workflows/workflow-soigneur.yml new file mode 100644 index 00000000000..d084faffb58 --- /dev/null +++ b/.github/workflows/workflow-soigneur.yml @@ -0,0 +1,37 @@ +# Tests Soigneur, the flaky test dashboard living in mod/soigneur. Everything it does is offline: +# the GitHub API is stubbed, and the fixtures are written by the test itself. +name: soigneur + +on: + push: + branches: + - main + - 'release/**' + pull_request: + paths: + - 'mod/soigneur/**' + # The marker lines are a contract between the two halves, and this is the other half. + - 'hack/github/gotestsum-reporter.sh' + +permissions: + contents: read + +jobs: + test: + name: "test" + timeout-minutes: 10 + runs-on: ubuntu-26.04 + defaults: + run: + shell: bash + + steps: + - name: "Init: checkout" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + + - name: "Run" + run: | + ./mod/soigneur/test.sh diff --git a/mod/soigneur/flaky-annotate.sh b/mod/soigneur/flaky-annotate.sh index 3a451ac63bf..2f30835a540 100755 --- a/mod/soigneur/flaky-annotate.sh +++ b/mod/soigneur/flaky-annotate.sh @@ -131,4 +131,7 @@ soigneur::main(){ fi } +# Sourcing this script defines its functions and runs nothing: that is how test.sh reaches them. +[ "${BASH_SOURCE[0]}" == "${0}" ] || return 0 + soigneur::main "$@" diff --git a/mod/soigneur/flaky-issue.sh b/mod/soigneur/flaky-issue.sh index ad9d4200ccd..c1fd0169f4a 100755 --- a/mod/soigneur/flaky-issue.sh +++ b/mod/soigneur/flaky-issue.sh @@ -106,6 +106,9 @@ soigneur::main(){ echo "$url" } +# Sourcing this script defines its functions and runs nothing: that is how test.sh reaches them. +[ "${BASH_SOURCE[0]}" == "${0}" ] || return 0 + [ "$#" -ge 1 ] || { echo "usage: $0 [digest.txt]" >&2 exit 1 diff --git a/mod/soigneur/test.sh b/mod/soigneur/test.sh new file mode 100755 index 00000000000..165fbe5c57a --- /dev/null +++ b/mod/soigneur/test.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Exercises the parts of Soigneur that running a report locally does not: the workflow filter, +# the log parser, the renderer, and the validators. Everything here is offline - the API is +# stubbed, and the fixtures are written by the test itself. +# +# Usage: +# ./test.sh + +set -o errexit -o errtrace -o functrace -o nounset -o pipefail +# `root` belongs to the scripts this sources, which declare it readonly. +here="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" +readonly here + +export SOIGNEUR_REPO="owner/name" + +# Sourcing the collector brings in its functions, and lib.sh with them. +# shellcheck source-path=SCRIPTDIR +. "$here"/flaky-report.sh + +tmp="$(mktemp -d)" +# shellcheck disable=SC2064 +trap "rm -rf '$tmp'" EXIT + +failed=0 +total=0 + +check(){ + local name="$1" + local want="$2" + local got="$3" + + total="$((total + 1))" + if [ "$want" == "$got" ]; then + echo "ok $name" + else + failed="$((failed + 1))" + echo "FAIL $name" + echo " want: $want" + echo " got: $got" + fi +} + +# --- the workflow filter ------------------------------------------------------------------- +# Regression test for #5216: `index` evaluates its argument against its own input, so filtering +# the runs used to die with "Cannot index array with string" as soon as `workflows` was set - +# which is to say, in the only configuration that ships. +cat > "$tmp"/runs.json <<'EOF' +{"workflow_runs":[ + {"id":1,"run_attempt":1,"path":".github/workflows/test.yml","created_at":"2026-09-17T00:00:00Z"}, + {"id":2,"run_attempt":2,"path":".github/workflows/lint.yml","created_at":"2026-09-17T00:00:00Z"}, + {"id":3,"run_attempt":1,"path":".github/workflows/flaky.yml","created_at":"2026-09-17T00:00:00Z"}]} +EOF + +soigneur::api(){ cat "$tmp"/runs.json; } + +filtered(){ + SOIGNEUR_WORKFLOWS="$1" soigneur::runs "2026-09-10T00:00:00Z" 2>&1 | jq -sc '[.[].workflow]' +} + +check "no filter keeps every run" \ + '["test.yml","lint.yml","flaky.yml"]' "$(filtered "")" +check "a filter keeps the named workflows" \ + '["test.yml","flaky.yml"]' "$(filtered "test.yml,flaky.yml")" +check "a filter tolerates spaces around the names" \ + '["test.yml","flaky.yml"]' "$(filtered " test.yml , flaky.yml ")" +check "a filter matching nothing keeps nothing" \ + '[]' "$(filtered "nope.yml")" +check "attempts and ids are carried over" \ + '[["1",1],["2",2],["3",1]]' \ + "$(SOIGNEUR_WORKFLOWS="" soigneur::runs "x" | jq -sc '[.[] | [.id, .attempts]]')" + +# --- the log parser ------------------------------------------------------------------------ +mkdir -p "$tmp"/logs +lookup="$(printf 'inhostrootfullinux\t77\n')" + +{ + echo "2026-09-17T00:00:00.0000000Z ##[group]$soigneur_marker failing" + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker failing TestAlpha" + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker flaky TestBeta/sub_one" + echo "2026-09-17T00:00:00.0000000Z ##[endgroup]" +} > "$tmp"/logs/'1_in-host _ rootful linux.txt' + +check "markers are read back, per class" \ + '{"id":"77","kind":"failing","tests":["TestAlpha"]} {"id":"77","kind":"flaky","tests":["TestBeta/sub_one"]}' \ + "$(soigneur::logs::parse "$tmp"/logs "$lookup" 2>/dev/null | tr '\n' ' ' | sed 's/ $//')" + +# The logs that predate the markers only hold the block printed at the end of a job. +{ + echo "2026-09-17T00:00:00.0000000Z === Failing tests ===" + echo "2026-09-17T00:00:00.0000000Z TestGamma" + echo "2026-09-17T00:00:00.0000000Z =====================" + echo "2026-09-17T00:00:00.0000000Z Post job cleanup." +} > "$tmp"/logs/'1_in-host _ rootful linux.txt' + +check "the pre-marker block is still read, and stops at its rule" \ + '{"id":"77","kind":"failing","tests":["TestGamma"]}' \ + "$(soigneur::logs::parse "$tmp"/logs "$lookup" 2>/dev/null)" + +# A job log holds whatever the tests printed, and what is collected ends up in an issue. +{ + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker failing TestLegit" + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker failing x\`](https://evil.example)[\`y" + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker flaky Test|Pipe" + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker failing " + echo "2026-09-17T00:00:00.0000000Z $soigneur_marker failing TestSub/issue_#3568_-_ok=yes" +} > "$tmp"/logs/'1_in-host _ rootful linux.txt' + +check "implausible test names are dropped, realistic ones are kept" \ + '{"id":"77","kind":"failing","tests":["TestLegit","TestSub/issue_#3568_-_ok=yes"]}' \ + "$(soigneur::logs::parse "$tmp"/logs "$lookup" 2>/dev/null)" + +check "a log that matches no job is reported, not attributed at random" \ + 'warning: no job matches the log of 1_in-host _ rootful linux' \ + "$(soigneur::logs::parse "$tmp"/logs "$(printf 'other\t99\n')" 2>&1 >/dev/null | tr -d "'")" + +# --- the renderer -------------------------------------------------------------------------- +render(){ + jq -n -f "$here"/flaky-report.jq \ + --slurpfile executions "$1" \ + --slurpfile findings "$2" \ + --arg repo "owner/name" --arg branch "main" \ + --arg since "2026-09-10T00:00:00Z" --arg now "2026-09-17T00:00:00Z" \ + --arg maxTests 25 --arg maxLinks 5 --arg runURL "" --arg docsURL "" --arg footer "" \ + --argjson notes '[]' +} + +cat > "$tmp"/executions.json <<'EOF' +{"sha":"abc","id":"77","name":"in-host / rootful\n linux","conclusion":"failure","url":"https://example/1","at":"2026-09-17T00:00:00Z"} +{"sha":"def","id":"78","name":"in-host / rootful\n linux","conclusion":"success","url":"https://example/2","at":"2026-09-16T00:00:00Z"} +EOF +cat > "$tmp"/findings.json <<'EOF' +{"id":"77","kind":"failing","tests":["TestAlpha","TestAlpha/sub"]} +{"id":"77","kind":"flaky","tests":["TestBeta"]} +EOF + +report="$(render "$tmp"/executions.json "$tmp"/findings.json)" + +check "a parent test and its subtest count once, as one row" \ + '1' "$(printf '%s' "$report" | jq -r '[.data.tests[] | select(.test == "TestAlpha")] | length')" +check "the recovered test is counted as flaky" \ + '1' "$(printf '%s' "$report" | jq -r '.data.tests[] | select(.test == "TestBeta") | .flaky')" +check "the job name loses the newlines the API puts in it" \ + 'in-host / rootful linux' "$(printf '%s' "$report" | jq -r '.data.jobs[0].job')" +check "the failure rate is per execution" \ + '1/2' "$(printf '%s' "$report" | jq -r '.data.jobs[0] | "\(.failures)/\(.executions)"')" + +: > "$tmp"/empty.json +check "an empty window renders, and says so" \ + 'true' \ + "$(render "$tmp"/empty.json "$tmp"/empty.json | jq -r '.markdown | contains("No test-level failure was reported")')" + +# --- the validators ------------------------------------------------------------------------ +check "false means false" "off" "$(soigneur::bool "false" && echo on || echo off)" +check "unset means false" "off" "$(soigneur::bool "" && echo on || echo off)" +check "true means true" "on" "$(soigneur::bool "TRUE" && echo on || echo off)" +check "a flag is not a number" "rejected" \ + "$(soigneur::number "N" "--body-file=/etc/passwd" 2>/dev/null && echo accepted || echo rejected)" +check "a traversal is not a repository" "rejected" \ + "$(soigneur::repo "R" "../../evil" 2>/dev/null && echo accepted || echo rejected)" + +# --- the emitter --------------------------------------------------------------------------- +cat > "$tmp"/gotestsum.json <<'EOF' +{"Action":"fail","Test":"TestAlpha","Package":"p"} +{"Action":"fail","Test":"TestBeta","Package":"p"} +{"Action":"pass","Test":"TestBeta","Package":"p"} +EOF +emitted="$(SOIGNEUR_QUIET=true "$here"/flaky-annotate.sh "$tmp"/gotestsum.json)" + +check "a test that never passed is failing" "true" \ + "$(printf '%s' "$emitted" | grep -qF "$soigneur_marker failing TestAlpha" && echo true || echo false)" +check "a test that passed on retry is flaky" "true" \ + "$(printf '%s' "$emitted" | grep -qF "$soigneur_marker flaky TestBeta" && echo true || echo false)" +check "and both are annotated for the pull request" "2" \ + "$(printf '%s' "$emitted" | grep -c '^::\(error\|warning\) title=')" + +echo +if [ "$failed" == "0" ]; then + echo "$total checks, all good." +else + echo "$total checks, $failed failed." + exit 1 +fi From 80d3ab8a4dac11f4c87f0ea09d07492807c458a8 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 20:09:01 +0900 Subject: [PATCH 833/868] save: wait for the archive to be fully written before returning `nerdctl save` occasionally wrote a truncated - sometimes empty - archive, which then blew up at the other end: failed to ingest "blobs/sha256/ca3cd4...": short read: expected 2811947 bytes but got 2785280: unexpected EOF untar manifest "manifest.json": unexpected EOF tar: Unexpected EOF in archive unrecognized image format The archive comes back from the transfer service over a stream, and `ImageExportStream.MarshalAny` copies that stream into our writer from a goroutine of its own. `client.Transfer` returns when the daemon is done, not when that goroutine has drained what is still in flight, so `saveAction` would close the output file - or the process would simply exit - with the tail of the archive still unwritten. A quiet machine hides this; a busy CI runner does not. That goroutine closes the writer once the copy is over, which is the only signal available, so hand the export stream a writer that reports being closed and wait for it. Measured against containerd v2.4.0-beta.0, with a writer slow enough to lose the race every time: `Transfer` returns after 2621440 of 2723840 bytes, and the remaining 102400 arrive afterwards. Fixes the `TestSave`, `TestLoadStdinFromPipe` and `TestBuildContextWithOCILayout` entries of the flaky test dashboard (issue 5202): all three save an image and read the archive back. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- pkg/cmd/image/save.go | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index 9c38bf79e4f..dc88941f2a5 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "os" + "sync" "github.com/distribution/reference" "github.com/opencontainers/go-digest" @@ -101,7 +102,7 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio storeOpts = append(storeOpts, transferimage.WithExtraReference(imageRef)) } - w := nopWriteCloser{options.Stdout} + w := &signalWriteCloser{Writer: options.Stdout, closed: make(chan struct{})} progressOutput := io.Writer(os.Stderr) if options.Quiet { @@ -110,17 +111,40 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio pf, done := transferutil.ProgressHandler(ctx, progressOutput) defer done() - return client.Transfer(ctx, + if err = client.Transfer(ctx, transferimage.NewStore("", storeOpts...), tarchive.NewImageExportStream(w, "", exportOpts...), transfer.WithProgress(pf), - ) + ); err != nil { + return err + } + + // The transfer service hands the archive back over a stream that a goroutine of its own + // copies into `w`, and Transfer returns as soon as the daemon is done - which is before that + // goroutine has necessarily drained what is still in flight. The goroutine closes the writer + // when it is over, so that is what we wait for: returning any earlier hands the caller a + // truncated archive. + select { + case <-w.closed: + return nil + case <-ctx.Done(): + return ctx.Err() + } } -type nopWriteCloser struct { +// signalWriteCloser is an io.WriteCloser that reports, through the `closed` channel, that it has +// been closed - and tolerates being closed more than once, as io.Closer does not promise not to. +type signalWriteCloser struct { io.Writer + + once sync.Once + closed chan struct{} } -func (nopWriteCloser) Close() error { +func (w *signalWriteCloser) Close() error { + w.once.Do(func() { + close(w.closed) + }) + return nil } From 5a3eb031813685195768fceb2530a76c542a0f75 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 20:13:42 +0900 Subject: [PATCH 834/868] test: do not let the kubo healthcheck fight our own ipfs commands TestIPFSCompNoBuild kept failing to get a kubo registry up: kubo.go:78: assertion failed: error is not nil: error after 5 attempts: Get "http://10.1.0.151:5001/api/v0": context deadline exceeded with, in the container logs: Error: lock /data/ipfs/repo.lock: someone else has the lock The kubo image declares a HEALTHCHECK, `ipfs dag stat /ipfs/Qm...`, and `healthcheck.StartTimer` runs the first probe right away, when the container starts - which is exactly when the container command is running `ipfs init && ipfs config && ipfs daemon`. The probe and those commands both open the IPFS repo, only one of them gets /data/ipfs/repo.lock, and whichever loses exits non-zero. When that is `ipfs config`, the && chain never reaches `ipfs daemon` and the API is never served, so the test polls a port that will never answer. Reproduced, outside of any test, by running the probe command concurrently with `ipfs config` in that image: the same error, every couple of iterations. The probe is of no use to us - the helper already waits for the API to answer before handing the registry over - so turn it off. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- pkg/testutil/nerdtest/registry/kubo.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go index 1eda4c052d0..fd93e632029 100644 --- a/pkg/testutil/nerdtest/registry/kubo.go +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -50,6 +50,13 @@ func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, current "-d", "-p", fmt.Sprintf("%s:%d:%d", listenIP, port, port), "--name", containerName, + // The kubo image declares a HEALTHCHECK (`ipfs dag stat ...`), and nerdctl runs the very + // first probe as soon as the container starts - right while the command below is still + // running `ipfs init` and `ipfs config`. Both ends open the IPFS repo, only one of them + // can hold /data/ipfs/repo.lock, and the loser dies with "someone else has the lock", + // which breaks the && chain and leaves us without a daemon. Readiness is established by + // polling the API below anyway, so the probe buys us nothing here. + "--no-healthcheck", "--entrypoint=/bin/sh", platform.KuboImage, "-c", "--", From 18d63d6704590f60a446132d5a2fd5396a39369b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 20:25:34 +0900 Subject: [PATCH 835/868] test: wait for the log binary to have written before reading it TestRunWithLogBinary failed with: container_run_test.go:627: assertion failed: expression is false: strings.Contains(log, "foo") The test starts `run -d ... sh -euxc "echo foo; echo bar"` with a `binary://` log driver, and reads the file that the log binary writes as soon as `run -d` hands the container id back. At that point the container has not necessarily said anything yet, and the log binary - a process of its own, draining the container's stdout pipe on its own schedule - has possibly not even been scheduled. The file exists, because the binary creates it on startup, so the read succeeds and returns nothing. Wait for the container to be over, then for the binary to have written down what it said. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_run_test.go | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index b26db7dc2d0..4f002cfeb9e 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -620,12 +620,29 @@ COPY --from=builder /go/src/logger/logger / 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")) + // The logging binary is a process of its own, draining the container stdout pipe on + // its own schedule: when `run -d` hands us the container id back, the container has + // not necessarily said anything yet, and the binary has possibly not even been + // scheduled. Wait for the container to be over, then for the binary to have written + // down what it said. + nerdtest.EnsureContainerExited(helpers, data.Identifier(), 0) + + logPath := filepath.Join(os.TempDir(), fmt.Sprintf("%s_stdout.log", containerID)) + + var log string + for i := 0; i < 20; i++ { + logBytes, err := os.ReadFile(logPath) + if err == nil { + log = string(logBytes) + if strings.Contains(log, "foo") && strings.Contains(log, "bar") { + break + } + } + time.Sleep(time.Second) + } + + assert.Assert(t, strings.Contains(log, "foo"), "%q does not contain %q", log, "foo") + assert.Assert(t, strings.Contains(log, "bar"), "%q does not contain %q", log, "bar") }, } } From 6dae5e72ed45b0b521446f3797b361d24e053f1d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 20:26:13 +0900 Subject: [PATCH 836/868] test: wait for the auto-removed container to actually be gone TestAttachForAutoRemovedContainer failed with: container_attach_linux_test.go:207: assertion failed: strings.Contains(helpers.Capture("ps", "-a"), data.Identifier()) is true and, right after, in the cleanup: Error response from daemon: removal of container testattachforautoremovedcontainer-23f097dc is already in progress The container is started with `--rm`, and the test asserts that it is gone from `ps -a` the moment the `attach` it was running under returns. Removal is not done at that point - it is triggered by the container process being over and proceeds on its own - so `ps -a` may still list it, as the daemon itself said. Give it the same treatment as the other container states we wait on, with a new EnsureContainerRemoved next to EnsureContainerStarted and EnsureContainerExited. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .../container/container_attach_linux_test.go | 2 +- pkg/testutil/nerdtest/utilities.go | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index ee265480c2e..fc31119c788 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -204,7 +204,7 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { Output: expect.All( expect.Contains("markmark"), func(stdout string, t tig.T) { - assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier())) + nerdtest.EnsureContainerRemoved(helpers, data.Identifier()) }, ), } diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index 37775cf1e68..f906a0bebdb 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -159,6 +159,26 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { } } +// EnsureContainerRemoved waits for a container to be gone from `ps -a`. +// This is meant for containers started with `--rm`: removal there happens after the container +// process is over, and is not finished by the time the command that was attached to it returns. +func EnsureContainerRemoved(helpers test.Helpers, con string) { + helpers.T().Helper() + removed := false + for i := 0; i < maxRetry && !removed; i++ { + removed = !strings.Contains(helpers.Capture("ps", "-a"), con) + if !removed { + time.Sleep(sleep) + } + } + + if !removed { + helpers.T().Log(helpers.Capture("ps", "-a")) + helpers.T().Log(fmt.Sprintf("container %s still not removed after %d retries", con, maxRetry)) + helpers.T().FailNow() + } +} + func EnsureContainerExited(helpers test.Helpers, con string, exitCode int) { helpers.T().Helper() exited := false From b468941610848a88394a9a9eef52df29610becd7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 17 Sep 2026 20:27:18 +0900 Subject: [PATCH 837/868] test: wait for the compose services to be up before listing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestComposeStart failed with one of the two services missing from the listing entirely: 🖊️ Inspecting output (match) 👀 testing: `NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS ` ❌ FAILED! `Up|running` while the other one was reported as `Up Less than a second`, which says how tight this was. `compose start` returns once it has asked for the containers to be started, and `compose ps` only lists the ones that are actually running, so the service that is slowest to come up is simply not there yet. Wait for both containers to be running before asking for the listing. That needs the project name to be known rather than derived from the temp directory, so pin it with -p, as the compose down tests already do. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .../compose/compose_start_linux_test.go | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/compose/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go index 2bd5dc11af8..4d551b0ecde 100644 --- a/cmd/nerdctl/compose/compose_start_linux_test.go +++ b/cmd/nerdctl/compose/compose_start_linux_test.go @@ -25,6 +25,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -43,19 +44,20 @@ services: testCase := nerdtest.Setup() testCase.Cleanup = func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + helpers.Anyhow("compose", "-p", data.Identifier("project"), "-f", data.Temp().Path("compose.yaml"), "down") } testCase.Setup = func(data test.Data, helpers test.Helpers) { + projectName := data.Identifier("project") data.Temp().Save(dockerComposeYAML, "compose.yaml") - helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") - helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "start") - helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "stop", "--timeout", "1", "svc0") - helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "kill", "svc1") + helpers.Ensure("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "start") + helpers.Ensure("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "stop", "--timeout", "1", "svc0") + helpers.Ensure("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "kill", "svc1") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") + return helpers.Command("compose", "-p", data.Identifier("project"), "-f", data.Temp().Path("compose.yaml"), "start") } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { @@ -63,8 +65,15 @@ services: ExitCode: 0, Errors: nil, Output: func(stdout string, t tig.T) { - svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") - svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") + projectName := data.Identifier("project") + // `compose start` returns once it has asked for the containers to be started, and + // `compose ps` only lists the ones that are actually running: without waiting, the + // service that is the slowest to come up is simply missing from the listing. + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "svc0", "1")) + nerdtest.EnsureContainerStarted(helpers, serviceparser.DefaultContainerName(projectName, "svc1", "1")) + + svc0 := helpers.Capture("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") + svc1 := helpers.Capture("compose", "-p", projectName, "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") comp := expect.Match(regexp.MustCompile("Up|running")) comp(svc0, t) comp(svc1, t) From 263b16dbab5a6986a65eaf01aa2899da47a1551a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:32:35 +0000 Subject: [PATCH 838/868] build(deps): bump docker/build-push-action from 7.3.0 to 7.4.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.3.0 to 7.4.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 4 ++-- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 62e602649a0..165de97455c 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -63,7 +63,7 @@ jobs: # Build the image for the host platform and load it into Docker, to smoke test # it before the multi-platform image is built and published - name: Build Docker image for the smoke test - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . load: true @@ -77,7 +77,7 @@ jobs: # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 6cea990986b..53136cf2be4 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -250,7 +250,7 @@ jobs: # the canary containerd version (if any) takes precedence over the input. - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} name: "Init (linux): build test artifacts" - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . # push is false by default; stated explicitly for zizmor's cache-poisoning audit diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index bc939d0541e..c8aeb39d7bc 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -72,7 +72,7 @@ jobs: # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides the # GitHub runtime token and cache url to BuildKit by itself. - name: "Init: build test artifacts (on the host, with Docker)" - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . # push is false by default; stated explicitly for zizmor's cache-poisoning audit From 53bca62fea85dd868005ff9a68d413ef9d5159d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:32:41 +0000 Subject: [PATCH 839/868] build(deps): bump docker/setup-qemu-action from 4.3.0 to 4.4.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/1f40c72289eff860ee54a304f1438e3cff362e0a...99012661954931238ded8c8b007157a8430204e1) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 62e602649a0..b8acc6ab0a1 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -37,7 +37,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 721f304988a..3e2860f35ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: "Install go" uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: From 3db4e1cb62fe2d3e57cf89a05ec5743a64a1f87d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:33:02 +0000 Subject: [PATCH 840/868] build(deps): bump docker/setup-buildx-action from 4.3.0 to 4.4.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...594f3bf4285d9ea8dc53c9a0c9c4092420091003) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 62e602649a0..9ac07707b26 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -40,7 +40,7 @@ jobs: uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 6cea990986b..eb1c5cc9a87 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -242,7 +242,7 @@ jobs: - if: ${{ inputs.target != '' && env.SHOULD_RUN == 'yes' }} name: "Init (linux): Set up Docker Buildx" - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides # the GitHub runtime token and cache url to BuildKit by itself. diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index bc939d0541e..26741774ccd 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -67,7 +67,7 @@ jobs: template://${GUEST} - name: "Init: Set up Docker Buildx" - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 # Unlike a raw `docker buildx build --cache-from type=gha`, the action provides the # GitHub runtime token and cache url to BuildKit by itself. From a7e1d208ed0b87d646d8bf11db5cfd9af5113f85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:33:18 +0000 Subject: [PATCH 841/868] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.8.0+incompatible to 29.8.1+incompatible - [Commits](https://github.com/docker/cli/compare/v29.8.0...v29.8.1) Updates `github.com/moby/moby/v2` from 2.0.0-beta.23 to 2.0.0-beta.24 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.23...v2.0.0-beta.24) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.8.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 953cf838455..12fe3f679ff 100644 --- a/go.mod +++ b/go.mod @@ -24,11 +24,11 @@ require ( 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.3.0 - github.com/docker/cli v29.8.0+incompatible //gomodjail:unconfined + github.com/docker/cli v29.8.1+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 //gomodjail:unconfined github.com/docker/go-units v0.5.0 github.com/moby/moby/client v0.6.0 //gomodjail:unconfined - github.com/moby/moby/v2 v2.0.0-beta.23 //gomodjail:unconfined + github.com/moby/moby/v2 v2.0.0-beta.24 //gomodjail:unconfined github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined github.com/moby/sys/user v0.4.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index a774b039f1b..6a149ab96c2 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,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.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk= -github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c= +github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= @@ -173,8 +173,8 @@ github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= -github.com/moby/moby/v2 v2.0.0-beta.23 h1:vDK8/40mF1U1neLp6XOttWuSDeTMFgIODXFeOsgo2Uw= -github.com/moby/moby/v2 v2.0.0-beta.23/go.mod h1:CtC7n8ozJwwpHGGjNg6md+GBMuYhh7nVwuOmhmYEegY= +github.com/moby/moby/v2 v2.0.0-beta.24 h1:sWv7EckkDJhpraR9yUjkw1XeVf39+MWd58E22PYloNA= +github.com/moby/moby/v2 v2.0.0-beta.24/go.mod h1:3zRfNirit+BkquckweT36STGqB+A3iUXp1zARg13JHM= 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= From 8d7f34038a9d1f5593a52016631db7a880dc8f98 Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Sat, 19 Sep 2026 13:40:43 +0400 Subject: [PATCH 842/868] fix(top): handle stopped and paused containers Return an error for stopped tasks while allowing paused tasks to proceed to PID inspection. Signed-off-by: Immanuel Tikhonov --- cmd/nerdctl/container/container_top_test.go | 44 +++++++++++++++++++++ pkg/cmd/container/top_unix.go | 4 +- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_top_test.go b/cmd/nerdctl/container/container_top_test.go index e63d71f4150..4a859e43665 100644 --- a/cmd/nerdctl/container/container_top_test.go +++ b/cmd/nerdctl/container/container_top_test.go @@ -17,9 +17,11 @@ package container import ( + "errors" "runtime" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -69,6 +71,48 @@ func TestTop(t *testing.T) { testCase.Run(t) } +func TestTopStoppedContainer(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All(require.Linux, nerdtest.CgroupsAccessible) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--network", "none", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("stop", "--time", "1", 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 { + return helpers.Command("top", data.Identifier()) + } + + testCase.Expected = test.Expects(1, []error{errors.New("is not running")}, nil) + testCase.Run(t) +} + +func TestTopPausedContainer(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All(require.Linux, nerdtest.CgroupsAccessible) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--network", "none", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("pause", 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 { + return helpers.Command("top", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains("sleep")) + testCase.Run(t) +} + func TestTopHyperVContainer(t *testing.T) { testCase := nerdtest.Setup() diff --git a/pkg/cmd/container/top_unix.go b/pkg/cmd/container/top_unix.go index 92141a4c77a..4b524241dc9 100644 --- a/pkg/cmd/container/top_unix.go +++ b/pkg/cmd/container/top_unix.go @@ -74,8 +74,8 @@ func containerTop(ctx context.Context, stdio io.Writer, client *containerd.Clien return err } - if status.Status != containerd.Running { - return nil + if status.Status != containerd.Running && status.Status != containerd.Paused { + return fmt.Errorf("container %s is not running (status: %s)", id, status.Status) } //TO DO handle restarting case: wait for container to restart and then launch top command From 34a85c2d0937d6f429c2b859afc5412a30c1dcdc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 20 Sep 2026 00:39:56 +0900 Subject: [PATCH 843/868] go.mod: github.com/rootless-containers/rootlesskit/v3 v3.2.0 Signed-off-by: Akihiro Suda --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 12fe3f679ff..895af9474d1 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined github.com/pelletier/go-toml/v2 v2.4.3 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.2.0 //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 diff --git a/go.sum b/go.sum index 6a149ab96c2..3a2c6b11faf 100644 --- a/go.sum +++ b/go.sum @@ -231,8 +231,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t 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.2.0-beta.0 h1:ioTLLDV1EMgjrrs0PYlgZ5Mxri8IbiVwzC6hUAyJwY8= -github.com/rootless-containers/rootlesskit/v3 v3.2.0-beta.0/go.mod h1:2+6juXfEVqXFICaGaaWkSnyDdhbkWv7WAvUg8hnsKwU= +github.com/rootless-containers/rootlesskit/v3 v3.2.0 h1:yNzNHcceg+8ST+ckreY/U5BYti2UVXXwQYNldtXUtRM= +github.com/rootless-containers/rootlesskit/v3 v3.2.0/go.mod h1:2+6juXfEVqXFICaGaaWkSnyDdhbkWv7WAvUg8hnsKwU= 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= From 4504226e7c42c317b3cc05f0d08b28a7c176fda5 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 20 Sep 2026 00:41:00 +0900 Subject: [PATCH 844/868] update RootlessKit (3.2.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0 | 6 ++++++ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 | 6 ------ 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0 delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 diff --git a/Dockerfile b/Dockerfile index 4f22bb0f5f1..ca889d88f68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.3@3cd28929043ba2847633c32b806a6ccda8cd030f # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v3.2.0-beta.0@BINARY +ARG ROOTLESSKIT_VERSION=v3.2.0@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0 new file mode 100644 index 00000000000..b12210cbc89 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0 @@ -0,0 +1,6 @@ +d68815112d658affe45e1ea00a9016758ba7e92230c1e69f7ee572d053e6f9c0 rootlesskit-aarch64.tar.gz +3b8c5db6f9c2407a30b658b32a9997f23fe5919e0ea9c0902960089acb8f5cb3 rootlesskit-armv7l.tar.gz +341d9544bd093e69dfb3e26a07cf5ab6bb3800e7d7dc85daaa4f15aee840a843 rootlesskit-ppc64le.tar.gz +2d0e395987fc7c7a07838badfcd750a56ea684248cf7ead2ca9a1a4fad1780d4 rootlesskit-riscv64.tar.gz +6baf3b44494ce9305518fdd7fa3d041a62bbc7febec6dddc181102147531aa49 rootlesskit-s390x.tar.gz +ff612d1d35854a52569acc4a4e9152f41504e69dcc13ff4c94cc9040dc28b3d9 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 deleted file mode 100644 index ba2201500d1..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.2.0-beta.0 +++ /dev/null @@ -1,6 +0,0 @@ -eb4c76a73a64e703e2cf634c7c71e4c16dd18952d50d4d4d957154b889dcd7d9 rootlesskit-aarch64.tar.gz -69bd2275ca8b1373def427d40904e3337b0c453444cca1319f1ae94e80375e7a rootlesskit-armv7l.tar.gz -dc83ebd59492c43b167f1a414ae75941124a259493d56a9088c5e362f372d5e4 rootlesskit-ppc64le.tar.gz -b31ef1c9b6b5d7ca3ea013f0cacfa92caf2e54207967e506c8ab4fa712b87915 rootlesskit-riscv64.tar.gz -241e8f2338a4c34a57bbae7b1622ff586922970668091f37a081b5516b6df354 rootlesskit-s390x.tar.gz -2c05852a782fba0c9ff268065c156b8a4a827dc018b5d968ebeb2cc2ee947900 rootlesskit-x86_64.tar.gz From a71997f7361474fe25fd06fa94a509191f48a949 Mon Sep 17 00:00:00 2001 From: Samran Asif Date: Sun, 20 Sep 2026 17:13:08 +0500 Subject: [PATCH 845/868] fix(netutil): enable portMappings and dns capabilities in default nat CNI plugin (#5157) The default generated Windows nat CNI configuration omitted the capabilities block, causing CNI plugins to ignore runtime port mappings (-p) and DNS settings. This adds capabilities with portMappings: true and dns: true to newNatPlugin, matching the schema expected by the Windows CNI plugin. Signed-off-by: Samran Asif --- pkg/netutil/cni_plugin_windows.go | 11 ++++++++--- pkg/netutil/netutil_windows_test.go | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pkg/netutil/cni_plugin_windows.go b/pkg/netutil/cni_plugin_windows.go index e8320b10c7e..b57d5bf9186 100644 --- a/pkg/netutil/cni_plugin_windows.go +++ b/pkg/netutil/cni_plugin_windows.go @@ -17,9 +17,10 @@ package netutil type natConfig struct { - PluginType string `json:"type"` - Master string `json:"master,omitempty"` - IPAM map[string]interface{} `json:"ipam"` + PluginType string `json:"type"` + Master string `json:"master,omitempty"` + IPAM map[string]interface{} `json:"ipam"` + Capabilities map[string]bool `json:"capabilities,omitempty"` } func (*natConfig) GetPluginType() string { @@ -30,6 +31,10 @@ func newNatPlugin(master string) *natConfig { return &natConfig{ PluginType: "nat", Master: master, + Capabilities: map[string]bool{ + "portMappings": true, + "dns": true, + }, } } diff --git a/pkg/netutil/netutil_windows_test.go b/pkg/netutil/netutil_windows_test.go index eb26eef9449..9464f3be8d7 100644 --- a/pkg/netutil/netutil_windows_test.go +++ b/pkg/netutil/netutil_windows_test.go @@ -16,10 +16,26 @@ package netutil -import "testing" +import ( + "testing" + + "gotest.tools/v3/assert" +) // Tests whether nerdctl properly creates the default network when required. // On Windows, the default driver used will be "nat". (netutil.DefaultNetworkName) func TestDefaultNetworkCreation(t *testing.T) { testDefaultNetworkCreation(t) } + +func TestGenerateCNIPluginsNatCapabilities(t *testing.T) { + e := &CNIEnv{} + plugins, err := e.generateCNIPlugins("nat", "nat", nil, nil, false, false) + assert.NilError(t, err) + assert.Assert(t, len(plugins) == 1) + nat, ok := plugins[0].(*natConfig) + assert.Assert(t, ok) + assert.Assert(t, nat.Capabilities != nil) + assert.Assert(t, nat.Capabilities["portMappings"]) + assert.Assert(t, nat.Capabilities["dns"]) +} From ed68b8e6b0c3c8fd3d5b4ec3604fcef9e55c5ef9 Mon Sep 17 00:00:00 2001 From: Max Niia Date: Mon, 21 Sep 2026 08:31:09 +0200 Subject: [PATCH 846/868] Add `cgroup_parent` to unimplemented YAML fields in compose.md The equivalent `run` command was implemented in pr #1782. But the compose.md document was never updated. ```bash nerdctl compose up INFO[0000] Creating network compose_stuff_default WARN[0000] Ignoring: service probe: [CgroupParent] INFO[0000] Ensuring image busybox:latest INFO[0000] Creating container compose_stuff-probe-1 INFO[0000] Running [/usr/local/bin/nerdctl run --cidfile=/tmp/compose-1462036713/cid -l=com.docker.compose.project=compose_stuff -l=com.docker.compose.service=probe -l=com.docker.compose.config-hash=e6b1a6afae4e1c44f0de80fa8b4d1bfc71c0d073ce944fb93c6fb84e834c2ffe -d --name=compose_stuff-probe-1 --pull=never --net=compose_stuff_default --hostname=probe --restart=no busybox:latest sleep 60] INFO[0000] Attaching to logs ``` Signed-off-by: Max Niia --- docs/compose.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/compose.md b/docs/compose.md index c48639a612e..77398118e86 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -22,6 +22,7 @@ which was derived from [Docker Compose file version 3 specification](https://doc ### Unimplemented YAML fields - Fields that correspond to unimplemented `docker run` flags, e.g., `services..links` (corresponds to `docker run --link`) - Fields that correspond to unimplemented `docker build` flags, e.g., `services..build.extra_hosts` (corresponds to `docker build --add-host`) +- `services..cgroup_parent` - `services..credential_spec` - `services..deploy.update_config` - `services..deploy.rollback_config` From 3c6b143b44cbfb584c42a167dd171b21c96da561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:28:54 +0000 Subject: [PATCH 847/868] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.4.0...v2.4.1) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.4.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 895af9474d1..0e98d34ae45 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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.12.0 //gomodjail:unconfined - github.com/containerd/containerd/v2 v2.4.0 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.4.1 //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 diff --git a/go.sum b/go.sum index 3a2c6b11faf..898941cc93f 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,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.12.0 h1:kuQm82SbDrCuO4n7hf2L8zsBtZLuympyq5X/VotfX2A= github.com/containerd/containerd/api v1.12.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc= -github.com/containerd/containerd/v2 v2.4.0 h1:mZLYWkPAgF4tfTjinClrRfvUxmxHxg1PQL7cjrrpTtk= -github.com/containerd/containerd/v2 v2.4.0/go.mod h1:gHZz+v5y8mGt9grF3ynuaa3r6bXLmghBZWTAftHAUtg= +github.com/containerd/containerd/v2 v2.4.1 h1:DUx/ZJN7cEu0WuzHClDB+68H/bqMEH5pWoEjf0ae4hc= +github.com/containerd/containerd/v2 v2.4.1/go.mod h1:vgLdtvl3prFk1d3ZVqQcsDD5Q9IKM+vAIdU54U85w+I= 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= From ae24b6e01cf05e3e57c2ffad3ba38b6180db7aa7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:32:43 +0000 Subject: [PATCH 848/868] build(deps): bump github.com/containerd/accelerated-container-image Bumps [github.com/containerd/accelerated-container-image](https://github.com/containerd/accelerated-container-image) from 1.4.4 to 1.4.5. - [Release notes](https://github.com/containerd/accelerated-container-image/releases) - [Commits](https://github.com/containerd/accelerated-container-image/compare/v1.4.4...v1.4.5) --- updated-dependencies: - dependency-name: github.com/containerd/accelerated-container-image dependency-version: 1.4.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 895af9474d1..1cc23e22ce0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.26.6 // containerd, Docker/Moby, OCI, and golang.org/x packages are trusted //gosocialcheck:trusted require ( - github.com/containerd/accelerated-container-image v1.4.4 //gomodjail:unconfined + github.com/containerd/accelerated-container-image v1.4.5 //gomodjail:unconfined 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.12.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 3a2c6b11faf..317060f062e 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00= github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= -github.com/containerd/accelerated-container-image v1.4.4 h1:88mL7plI0lvrzCiU1obhB4CFE8YFWFiqzNipydqiYCM= -github.com/containerd/accelerated-container-image v1.4.4/go.mod h1:h8+s7FnzpT1fnPqH31JK9LybCqiRd1IgLgA82vtX+Tc= +github.com/containerd/accelerated-container-image v1.4.5 h1:m3dw34J2qVq36TGOg0GCHUewwx2L3U2B8dgfSaA7Mmc= +github.com/containerd/accelerated-container-image v1.4.5/go.mod h1:vCgrmhBDiF4dLRPLSHrcwGTrrW/ooioV+xQhTzwUUxk= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= From 603f2495e7314aef1f005800510295bdcb01bd7c Mon Sep 17 00:00:00 2001 From: Max Niia Date: Mon, 28 Sep 2026 11:11:50 +0200 Subject: [PATCH 849/868] Remove implemented fields from unimplemented list in compose.md - services..deploy.resources.reservations - services..stop_grace_period - services..stop_signal Signed-off-by: Max Niia --- docs/compose.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/compose.md b/docs/compose.md index 77398118e86..ede5110f9cb 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -26,12 +26,9 @@ which was derived from [Docker Compose file version 3 specification](https://doc - `services..credential_spec` - `services..deploy.update_config` - `services..deploy.rollback_config` -- `services..deploy.resources.reservations` - `services..deploy.placement` - `services..deploy.endpoint_mode` - `services..healthcheck.start_interval` -- `services..stop_grace_period` -- `services..stop_signal` - `configs..external` - `secrets..external` From 4c34096a7ce1529ff066ff6cbbbe16bdf35e0ea8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:32:39 +0000 Subject: [PATCH 850/868] build(deps): bump github.com/klauspost/compress from 1.20.0 to 1.20.1 Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.20.0 to 1.20.1. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](https://github.com/klauspost/compress/compare/v1.20.0...v1.20.1) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.20.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 895af9474d1..4a45a5327af 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/fsnotify/fsnotify v1.10.1 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 //gomodjail:unconfined - github.com/klauspost/compress v1.20.0 + github.com/klauspost/compress v1.20.1 github.com/mattn/go-isatty v0.0.24 //gomodjail:unconfined github.com/muesli/cancelreader v0.2.2 //gomodjail:unconfined github.com/pelletier/go-toml/v2 v2.4.3 diff --git a/go.sum b/go.sum index 3a2c6b11faf..c8e7fd3348b 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ 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/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= -github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= From 5e48714605afb533d0ba9dfbec1f96d6429779cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:32:58 +0000 Subject: [PATCH 851/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.15.16 to 0.16.0. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.15.16...v0.16.0) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.16.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 895af9474d1..5b6cc0a539c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined github.com/containerd/log v0.2.0 //gomodjail:unconfined github.com/containerd/nerdctl/mod/tigron v0.0.0 //gomodjail:unconfined - github.com/containerd/nydus-snapshotter v0.15.16 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.16.0 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 3a2c6b11faf..c2eec9054de 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/containerd/log/otel v0.1.0 h1:Az5rFFo0+c4v2yC8ROR63sWsZpKl/vhtUnUhqLSpE6U= github.com/containerd/log/otel v0.1.0/go.mod h1:65C5iYF2xIQByCyxzoO2KKKK1+/tGxD4XqhdlrTtvzE= -github.com/containerd/nydus-snapshotter v0.15.16 h1:FkAmNADhOQFeWZtrHcDz04ZoOKsIeKZtKbz7PHxIFtc= -github.com/containerd/nydus-snapshotter v0.15.16/go.mod h1:x+/i5WkV8w4jSXbdv57YSMWvkzT2JFwpeIDJxvcYe38= +github.com/containerd/nydus-snapshotter v0.16.0 h1:ruNhliMvo7D5nOHIWmt6nHZbJZephVMX5MclRDYHZ/8= +github.com/containerd/nydus-snapshotter v0.16.0/go.mod h1:bOppnH1X941XidPS8FadVSijJ/pIOK8L+Mc0fGFw74A= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= From 8207944999ea358956ab8ac29e0c5f4ffa8a4ded Mon Sep 17 00:00:00 2001 From: Adam Clettborn Date: Tue, 29 Sep 2026 13:35:46 +0200 Subject: [PATCH 852/868] fix(compose): pass CDI device names through unchanged Compose expanded every devices entry to source:target:permissions, so a CDI qualified name reached nerdctl run as a malformed host path and the container failed to create. Signed-off-by: Adam Clettborn --- pkg/composer/serviceparser/serviceparser.go | 7 +++++++ pkg/composer/serviceparser/serviceparser_test.go | 2 ++ 2 files changed, 9 insertions(+) diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 58b9393dbd4..d8a3cf50b71 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -29,6 +29,7 @@ import ( "time" "github.com/compose-spec/compose-go/v2/types" + cdiparser "tags.cncf.io/container-device-interface/pkg/parser" "github.com/containerd/log" @@ -563,6 +564,12 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e } for _, v := range svc.Devices { + // A CDI device is passed by its qualified name alone; `nerdctl run` + // only recognizes the name when nothing is appended to it. + if cdiparser.IsQualifiedName(v.Source) { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s", v.Source)) + continue + } c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s:%s:%s", v.Source, v.Target, v.Permissions)) } diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index ec23cdf6142..5feee8b9133 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -357,6 +357,7 @@ services: - /dev/a - /dev/b:/dev/b - /dev/c:/dev/c:rw + - vendor.com/class=name ` comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -375,6 +376,7 @@ services: assert.Assert(t, in(c.RunArgs, "--device=/dev/a:/dev/a:rwm")) assert.Assert(t, in(c.RunArgs, "--device=/dev/b:/dev/b:rwm")) assert.Assert(t, in(c.RunArgs, "--device=/dev/c:/dev/c:rw")) + assert.Assert(t, in(c.RunArgs, "--device=vendor.com/class=name")) } } From 379ca06b4a76b2f88cedee0387a6a741a9f9e832 Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Mon, 21 Sep 2026 10:05:50 +0400 Subject: [PATCH 853/868] fix: reject invalid network ls filters Signed-off-by: Immanuel Tikhonov --- pkg/cmd/network/list.go | 79 ++++++++++++++++++++++-------------- pkg/cmd/network/list_test.go | 31 ++++++++++++-- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go index 090af4b5ddf..1064b15310b 100644 --- a/pkg/cmd/network/list.go +++ b/pkg/cmd/network/list.go @@ -100,14 +100,14 @@ func List(ctx context.Context, options types.NetworkListOptions) error { return err } - labelFilterFuncs, nameFilterFuncs, err := getNetworkFilterFuncs(filters) + labelFilterFuncs, nameFilterFuncs, driverFilters, err := getNetworkFilterFuncs(filters) if err != nil { return err } if len(filters) > 0 { filtered := make([]*netutil.NetworkConfig, 0) for _, net := range netConfigs { - if networkMatchesFilter(net, labelFilterFuncs, nameFilterFuncs) { + if networkMatchesFilter(net, labelFilterFuncs, nameFilterFuncs, driverFilters) { filtered = append(filtered, net) } } @@ -167,45 +167,62 @@ func List(ctx context.Context, options types.NetworkListOptions) error { return nil } -func getNetworkFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, error) { +func getNetworkFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, []string, error) { labelFilterFuncs := make([]func(*map[string]string) bool, 0) nameFilterFuncs := make([]func(string) bool, 0) + var driverFilters []string for _, filter := range filters { - if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") { - filter, value, ok := strings.Cut(filter, "=") - if !ok { - continue + key, value, ok := strings.Cut(filter, "=") + if !ok { + return nil, nil, nil, fmt.Errorf("invalid argument %q for \"-f, --filter\": bad format of filter (expected name=value)", filter) + } + switch key { + case "name": + re, err := regexp.Compile(value) + if err != nil { + return nil, nil, nil, err } - switch filter { - case "name": - re, err := regexp.Compile(value) - if err != nil { - return nil, nil, err + nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { + return re.MatchString(name) + }) + case "label": + k, v, hasValue := strings.Cut(value, "=") + labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { + if labels == nil { + return false } - nameFilterFuncs = append(nameFilterFuncs, func(name string) bool { - return re.MatchString(name) - }) - case "label": - k, v, hasValue := strings.Cut(value, "=") - labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool { - if labels == nil { - return false - } - val, ok := (*labels)[k] - if !ok || (hasValue && val != v) { - return false - } - return true - }) - } - continue + val, ok := (*labels)[k] + if !ok || (hasValue && val != v) { + return false + } + return true + }) + case "driver": + driverFilters = append(driverFilters, value) + default: + return nil, nil, nil, fmt.Errorf("invalid filter '%s'", key) } } - return labelFilterFuncs, nameFilterFuncs, nil + return labelFilterFuncs, nameFilterFuncs, driverFilters, nil } -func networkMatchesFilter(net *netutil.NetworkConfig, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool) bool { +func networkMatchesFilter(net *netutil.NetworkConfig, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool, driverFilters []string) bool { + if len(driverFilters) > 0 { + if len(net.Plugins) == 0 { + return false + } + matched := false + for _, driver := range driverFilters { + if driver == net.Plugins[0].Network.Type { + matched = true + break + } + } + if !matched { + return false + } + } // Match against the user-visible labels only, so a --filter label= query can // neither select on nor be confused by nerdctl-internal keys. visible := visibleNetworkLabels(net.NerdctlLabels) diff --git a/pkg/cmd/network/list_test.go b/pkg/cmd/network/list_test.go index 87c61e51618..d1e4ba26a51 100644 --- a/pkg/cmd/network/list_test.go +++ b/pkg/cmd/network/list_test.go @@ -28,8 +28,10 @@ import ( func TestNetworkMatchesFilter(t *testing.T) { t.Parallel() labels := map[string]string{"env": "prod", "tier": "web"} + config, err := libcni.ConfListFromBytes([]byte(`{"cniVersion":"1.0.0","name":"frontend","plugins":[{"type":"bridge"}]}`)) + assert.NilError(t, err) net := &netutil.NetworkConfig{ - NetworkConfigList: &libcni.NetworkConfigList{Name: "frontend"}, + NetworkConfigList: config, NerdctlLabels: &labels, } @@ -47,12 +49,35 @@ func TestNetworkMatchesFilter(t *testing.T) { {"matching name only", []string{"name=frontend", "label=env=dev"}, false}, {"matching label only", []string{"name=backend", "label=env=prod"}, false}, {"no match", []string{"name=backend", "label=env=dev"}, false}, + {"matching driver", []string{"driver=bridge"}, true}, + {"nonmatching driver", []string{"driver=macvlan"}, false}, + {"one of multiple drivers", []string{"driver=macvlan", "driver=bridge"}, true}, + {"matching driver and label", []string{"driver=bridge", "label=env=prod"}, true}, + {"matching driver only", []string{"driver=bridge", "label=env=dev"}, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - labelFilters, nameFilters, err := getNetworkFilterFuncs(tc.filters) + labelFilters, nameFilters, driverFilters, err := getNetworkFilterFuncs(tc.filters) assert.NilError(t, err) - assert.Equal(t, networkMatchesFilter(net, labelFilters, nameFilters), tc.expected) + assert.Equal(t, networkMatchesFilter(net, labelFilters, nameFilters, driverFilters), tc.expected) + }) + } +} + +func TestNetworkFilterRejectsInvalidInput(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + filter string + want string + }{ + {"name", "bad format of filter"}, + {"label", "bad format of filter"}, + {"names=frontend", "invalid filter 'names'"}, + {"labels=env=prod", "invalid filter 'labels'"}, + } { + t.Run(tc.filter, func(t *testing.T) { + _, _, _, err := getNetworkFilterFuncs([]string{tc.filter}) + assert.ErrorContains(t, err, tc.want) }) } } From a29119bc8fc0392c0183ed28eb6bb644d3f52d07 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 30 Sep 2026 21:32:01 +0900 Subject: [PATCH 854/868] cp: pass the directory to tar via -C instead of the working directory GNU tar 1.30-13.el8_10 (AlmaLinux 8, released 2026-09-18) aborts with "Cannot getcwd: No such file or directory" when its working directory is under /proc//root of a container, as the path is unreachable from the host mount namespace (the kernel returns "(unreachable)/...", and glibc turns it into ENOENT). The regression comes from the combination of two upstream tar commits as backported to RHEL/AlmaLinux 8's tar 1.30: - 56fb4a96 ("chdir_id refactoring"), backported in 1.30-12 as part of the CVE-2025-45582 fix, introduced grow_wd(). Upstream initializes wd[0].abspath lazily (NULL), but the 1.30 backport keeps calling xgetcwd() eagerly and fails fatally on error. - 1b91f5f6 ("Draft patch for openat2 changes vs --one-top-level"), backported in 1.30-13, adds an unconditional `chdir_do (chdir_arg (".", ...), false)` to name_init(), so grow_wd() (and thus getcwd) is now reached on every invocation, not only when -C is specified. AlmaLinux 8 went from 1.30-11 directly to 1.30-13. Upstream tar is not affected (getcwd is lazy there), and neither commit is in an upstream release as of v1.35. Using `-C ` keeps the tar process's working directory on the host while tar opens the directory by itself. As the tar process no longer chdirs into the extraction directory, an inaccessible destination is now checked with access(2) beforehand, so that it is still reported as ErrTargetIsReadOnly. Fixes #5237 (the `nerdctl cp` part) Assisted-by: Claude Opus 5.5 (1M context) Signed-off-by: Akihiro Suda --- pkg/containerutil/cp_linux.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/containerutil/cp_linux.go b/pkg/containerutil/cp_linux.go index 6c2646e3c51..2332e1583dd 100644 --- a/pkg/containerutil/cp_linux.go +++ b/pkg/containerutil/cp_linux.go @@ -28,6 +28,8 @@ import ( "strconv" "strings" + "golang.org/x/sys/unix" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/core/mount" @@ -262,7 +264,11 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain if options.FollowSymLink { tarC = append(tarC, "-h") } - tarC = append(tarC, "-c", "-f", "-", tarCArg) + // Use -C rather than setting the working directory of the tar process, as GNU tar + // 1.30-13.el8_10 (AlmaLinux 8) fails with "Cannot getcwd" when its working directory is + // under /proc//root of another mount namespace. + // https://github.com/containerd/nerdctl/issues/5237 + tarC = append(tarC, "-C", tarCDir, "-c", "-f", "-", tarCArg) } tarXDir := destinationSpec.resolvedPath @@ -277,7 +283,13 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain if options.Container2Host && isGNUTar { tarX = append(tarX, "--no-same-owner") } - tarX = append(tarX, "-f", "-") + tarX = append(tarX, "-C", tarXDir, "-f", "-") + + // tar opens the -C directory by itself and fails with an unhelpful error when the directory + // is not accessible, so detect this beforehand. + if accessErr := unix.Access(tarXDir, unix.X_OK); errors.Is(accessErr, unix.EACCES) { + return ErrTargetIsReadOnly + } } if rootlessutil.IsRootless() { @@ -293,12 +305,10 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain // WARNING: some of our testing on stderr might not be portable across different versions of tar // In these cases (readonly target), we will just get the straight tar output instead tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...) - tarCCmd.Dir = tarCDir tarCCmd.Stdin = nil tarCCmd.Stderr = os.Stderr tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...) - tarXCmd.Dir = tarXDir if sourceSpec.fromStdin { // Reading from tar should pipe stdin into dst tarXCmd.Stdin = bufio.NewReader(os.Stdin) @@ -319,12 +329,12 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain var tarErr bytes.Buffer tarXCmd.Stderr = &tarErr - log.G(ctx).Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir) + log.G(ctx).Debugf("executing %v", tarCCmd.Args) if err := tarCCmd.Start(); err != nil { return errors.Join(fmt.Errorf("failed to execute %v", tarCCmd.Args), err) } - log.G(ctx).Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir) + log.G(ctx).Debugf("executing %v", tarXCmd.Args) if err := tarXCmd.Start(); err != nil { if strings.Contains(err.Error(), "permission denied") { return ErrTargetIsReadOnly From 702389a53d2d8b8e8c49bf72bc871d7e5ab74287 Mon Sep 17 00:00:00 2001 From: Adam Clettborn Date: Wed, 30 Sep 2026 21:35:32 +0200 Subject: [PATCH 855/868] fix(compose): support cgroup_parent and cgroup service fields Pass cgroup_parent and cgroup through to nerdctl run as --cgroup-parent and --cgroupns, which compose previously ignored. Signed-off-by: Adam Clettborn --- docs/compose.md | 1 - pkg/composer/serviceparser/serviceparser.go | 10 +++++++ .../serviceparser/serviceparser_test.go | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/compose.md b/docs/compose.md index ede5110f9cb..334133e6a61 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -22,7 +22,6 @@ which was derived from [Docker Compose file version 3 specification](https://doc ### Unimplemented YAML fields - Fields that correspond to unimplemented `docker run` flags, e.g., `services..links` (corresponds to `docker run --link`) - Fields that correspond to unimplemented `docker build` flags, e.g., `services..build.extra_hosts` (corresponds to `docker build --add-host`) -- `services..cgroup_parent` - `services..credential_spec` - `services..deploy.update_config` - `services..deploy.rollback_config` diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 58b9393dbd4..ea45aa5ae4d 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -61,6 +61,8 @@ func warnUnknownFields(svc types.ServiceConfig) { "BlkioConfig", "CapAdd", "CapDrop", + "Cgroup", + "CgroupParent", "CPUS", "CPUSet", "CPUShares", @@ -562,6 +564,14 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cpu-shares=%d", svc.CPUShares)) } + if svc.Cgroup != "" { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cgroupns=%s", svc.Cgroup)) + } + + if svc.CgroupParent != "" { + c.RunArgs = append(c.RunArgs, fmt.Sprintf("--cgroup-parent=%s", svc.CgroupParent)) + } + for _, v := range svc.Devices { c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s:%s:%s", v.Source, v.Target, v.Permissions)) } diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index ec23cdf6142..67711b7f55d 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -378,6 +378,33 @@ services: } } +func TestParseCgroup(t *testing.T) { + const dockerComposeYAML = ` +services: + foo: + image: nginx:alpine + cgroup: host + cgroup_parent: foo.slice +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + fooSvc, err := project.GetService("foo") + assert.NilError(t, err) + + foo, err := Parse(project, fooSvc) + assert.NilError(t, err) + + t.Logf("foo: %+v", foo) + for _, c := range foo.Containers { + assert.Assert(t, in(c.RunArgs, "--cgroupns=host")) + assert.Assert(t, in(c.RunArgs, "--cgroup-parent=foo.slice")) + } +} + func TestParseRelative(t *testing.T) { t.Parallel() From ef7c3064450f2063d91ddbd1dd620f0c6df4457f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:31:26 +0000 Subject: [PATCH 856/868] build(deps): bump github.com/docker/cli Bumps the docker group with 1 update in the / directory: [github.com/docker/cli](https://github.com/docker/cli). Updates `github.com/docker/cli` from 29.8.1+incompatible to 29.8.2+incompatible - [Commits](https://github.com/docker/cli/compare/v29.8.1...v29.8.2) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.8.2+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3a9e9322bfa..9c907c30882 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( 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.3.0 - github.com/docker/cli v29.8.1+incompatible //gomodjail:unconfined + github.com/docker/cli v29.8.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 //gomodjail:unconfined github.com/docker/go-units v0.5.0 github.com/moby/moby/client v0.6.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 273266efd36..8513b1b324e 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,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.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c= -github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ= +github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= From 1c881ae37fad8ebc92945e14e7f5a81616fbbb8d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:43:52 +0900 Subject: [PATCH 857/868] update containerd (2.4.1) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 15a80b3f638..21fe75e5631 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -154,5 +154,5 @@ jobs: go-version: 1.26 windows-cni-version: v0.3.3 docker-version: 5:29.8.1-1~ubuntu.26.04~resolute - windows-containerd-version: 2.4.0 - windows-containerd-sha: cadffa872e2385e5eccfaf0425d4f7be8571e6d2118dbb090efa0f681d5083d9 + windows-containerd-version: 2.4.1 + windows-containerd-sha: 57ebdac7c130dc6d17869c261325ae4282362304a6d38a2503de9da5524a4a05 diff --git a/Dockerfile b/Dockerfile index ca889d88f68..f76c74997df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.4.0@a7fe631d96c08fb14cf8eff0afdc280e99c30a94 +ARG CONTAINERD_VERSION=v2.4.1@f2551031d7276a770f65f98c9b52e57e7dad07e8 ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From c88e497f2fda6d7980634c05d663cc3812ac71e2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:44:02 +0900 Subject: [PATCH 858/868] update containerd 1.7 (1.7.36) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 21fe75e5631..e425379b1e6 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -52,7 +52,7 @@ jobs: - runner: ubuntu-26.04-arm # Additionally build for old containerd on amd - runner: ubuntu-26.04 - containerd-version: v1.7.35 + containerd-version: v1.7.36 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -81,7 +81,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.35 + containerd-version: v1.7.36 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-26.04 @@ -103,7 +103,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.35 + containerd-version: v1.7.36 # ipv6 - runner: ubuntu-26.04 target: rootful From e05436213186807aa905b0c58d2aaa4b76b2b3ab Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:44:07 +0900 Subject: [PATCH 859/868] update runc (1.5.2) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f76c74997df..0916cd797f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.4.1@f2551031d7276a770f65f98c9b52e57e7dad07e8 -ARG RUNC_VERSION=v1.5.1@8f2685a471d3347a686ad3909783d8aafc6bb208 +ARG RUNC_VERSION=v1.5.2@29dd3dc2b13b4123162e5fe132504bb4b15569f1 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From 758c94ae8de7127d647863dd6107fc3bc35cafcf Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:44:16 +0900 Subject: [PATCH 860/868] update BuildKit (0.33.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.1 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.1 diff --git a/Dockerfile b/Dockerfile index 0916cd797f8..2ca108d9d21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.5.2@29dd3dc2b13b4123162e5fe132504bb4b15569f1 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.33.0@BINARY +ARG BUILDKIT_VERSION=v0.33.1@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 deleted file mode 100644 index c8971ff5da8..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.0 +++ /dev/null @@ -1,2 +0,0 @@ -b6242896d343100808dcbe37565caf381e0a444a6a83d7255926bb1519248ead buildkit-v0.33.0.linux-amd64.tar.gz -e5acfb5929f967fde3b925ddb39f79fd481a0e96774c641fab3a0e83950d7bfa buildkit-v0.33.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.1 new file mode 100644 index 00000000000..a9c616af26d --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.33.1 @@ -0,0 +1,2 @@ +4e044bcd62a0c0bbe6a8c94d73989de2bfe4c04dbc0f9d6021cf96b72cd1d965 buildkit-v0.33.1.linux-amd64.tar.gz +4e1ba91f139761f249a1fa6c71e632dfdbdafeda416176ecadefbb95225e56c4 buildkit-v0.33.1.linux-arm64.tar.gz From bb995642be2bf7fd37de98a1b1ff01f1d219a1b6 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:44:22 +0900 Subject: [PATCH 861/868] update soci-snapshotter (0.16.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2ca108d9d21..66931905fb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=26.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.2 ARG NYDUS_VERSION=v2.4.5 -ARG SOCI_SNAPSHOTTER_VERSION=0.15.0 +ARG SOCI_SNAPSHOTTER_VERSION=0.16.1 ARG KUBO_VERSION=v0.43.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 959df3db64eaba5af9fddd9d7ba26d0001a8a99f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:44:22 +0900 Subject: [PATCH 862/868] update Docker (29.8.2) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index e425379b1e6..4029b393ff3 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -153,6 +153,6 @@ jobs: no-hyperv: ${{ matrix.canary && true || false }} go-version: 1.26 windows-cni-version: v0.3.3 - docker-version: 5:29.8.1-1~ubuntu.26.04~resolute + docker-version: 5:29.8.2-1~ubuntu.26.04~resolute windows-containerd-version: 2.4.1 windows-containerd-sha: 57ebdac7c130dc6d17869c261325ae4282362304a6d38a2503de9da5524a4a05 From f1e348b4d434c89d0be33ccfe16d2e15045ccd22 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Oct 2026 15:47:55 +0900 Subject: [PATCH 863/868] update golangci-lint (2.14.0) Disable the new revive rule `multiline-if-init` (revive v1.17), which has 34 occurrences in the tree. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Akihiro Suda --- .golangci.yml | 5 +++++ Makefile | 4 ++-- mod/tigron/Makefile | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3523c5ca349..30bd184b74f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -163,6 +163,11 @@ linters: - name: package-naming # 1 occurrence (pkg/api/types). Renaming a public package is not worth the churn. disabled: true + - name: multiline-if-init + # 34 occurrences. New in revive v1.17 (golangci-lint v2.14). + # Would force rewriting the `if err := f(...); err != nil` statements whose call + # arguments are spread over several lines. + disabled: true ##### P2: nice to have. - name: max-public-structs diff --git a/Makefile b/Makefile index fcc2e87aa1b..9de1e0d23e3 100644 --- a/Makefile +++ b/Makefile @@ -280,7 +280,7 @@ endif ########################## install-dev-tools: $(call title, $@) - # golangci: v2.13.2 (2026-08-27) + # golangci: v2.14.0 (2026-09-24) # git-validation: v1.2.2 (2025-02-26) # ltag: v0.3.0 (2025-03-04) # gotestsum: v1.13.0 (2025-09-11) @@ -288,7 +288,7 @@ install-dev-tools: # gosocialcheck: v0.2.0 (2026-09-11) @cd $(MAKEFILE_DIR) \ && go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@114493f9b3e7257d29e4130f2b4a4aadefbb6845 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install gotest.tools/gotestsum@c4a0df2e75a225d979a444342dd3db752b53619f \ diff --git a/mod/tigron/Makefile b/mod/tigron/Makefile index 3efe9f18681..80c9578a558 100644 --- a/mod/tigron/Makefile +++ b/mod/tigron/Makefile @@ -164,12 +164,12 @@ up: ########################## install-dev-tools: $(call title, $@) - # golangci: v2.13.2 (2026-08-27) + # golangci: v2.14.0 (2026-09-24) # git-validation: v1.2.2 (2025-02-26) # ltag: v0.3.0 (2025-03-04) # go-licenses: v2.0.1 (2025-09-08) @cd $(MAKEFILE_DIR) \ - && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@27774aaf853a4fd21f1dd5e69439459dc1b26e68 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@114493f9b3e7257d29e4130f2b4a4aadefbb6845 \ && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 From 29395a0456e923d11bf02541659b37813636532a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:35:31 +0000 Subject: [PATCH 864/868] build(deps): bump containerd/project-checks from 1.2.2 to 1.2.3 Bumps [containerd/project-checks](https://github.com/containerd/project-checks) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/containerd/project-checks/releases) - [Commits](https://github.com/containerd/project-checks/compare/d7751f3c375b8fe4a84c02a068184ee4c1f59bc4...9d887fad80ae4e40d6f10a68529082e7fc06a9af) --- updated-dependencies: - dependency-name: containerd/project-checks dependency-version: 1.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/job-lint-project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 849da3d1a89..74cc874c1fb 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -44,7 +44,7 @@ jobs: cache-dependency-path: src/github.com/containerd/nerdctl - name: "Run" - uses: containerd/project-checks@d7751f3c375b8fe4a84c02a068184ee4c1f59bc4 # v1.2.2 + uses: containerd/project-checks@9d887fad80ae4e40d6f10a68529082e7fc06a9af # v1.2.3 with: working-directory: src/github.com/containerd/nerdctl repo-access-token: ${{ secrets.GITHUB_TOKEN }} From fa18f83f8337a8accd0587f8901e8eebbdd538af Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Sat, 3 Oct 2026 09:39:20 +0300 Subject: [PATCH 865/868] fix(image): avoid dangling-ref name collision on `rmi -f` for running images nerdctl rmi -f on an image still used by a running container renames it before deleting the original, so containerd keeps the layers alive instead of garbage-collecting them. That dangling ref was always renamed to the literal string ":". Since containerd's image store requires unique names, force-removing a second running image's image in a separate invocation made its own rename fail with "image \":\": already exists", aborting the command. Name the dangling ref after its content digest (":") instead, so each kept-alive ref gets a distinct name. Tolerate AlreadyExists on the create call: if two different tags share the same digest and are both force-removed as running images, the second create legitimately no-ops (the digest is already pinned). The one other consumer that special-cased the exact ":" name (pkg/imgutil filtering, used by --filter reference=... to skip unparsable dangling names without erroring) is updated to match on the ":" prefix instead. Fixes #4109 Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/image/image_remove_test.go | 34 ++++++++++++++++++++++++++ pkg/cmd/image/remove.go | 28 ++++++++++++++++----- pkg/imgutil/filtering.go | 7 ++++-- pkg/imgutil/filtering_test.go | 23 +++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index 1ad1cee6a30..f33c7236118 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -149,6 +149,40 @@ func TestRemove(t *testing.T) { } }, }, + { + Description: "Issue #4109 - force-removing an in-use image does not collide with a dangling ref left by an earlier force-remove", + NoParallel: true, + Require: require.All( + // Dangling-ref naming on force-remove of an in-use image is a nerdctl-specific + // implementation detail; Docker doesn't use this scheme, so the test doesn't apply. + require.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier()+"-1", testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("run", "--quiet", "--pull", "always", "-d", "--name", data.Identifier()+"-2", testutil.BusyboxImage, "sleep", nerdtest.Infinity) + // Force-remove the first in-use image now: this creates a dangling ref to keep + // its layers alive. Before the fix, that ref was unconditionally named ":", so + // the second force-remove below (the command under test) would fail creating + // its own dangling ref with "image \":\": already exists". + helpers.Ensure("rmi", "-f", testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()+"-1") + helpers.Anyhow("rm", "-f", data.Identifier()+"-2") + }, + Command: test.Command("rmi", "-f", testutil.BusyboxImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: []error{}, + Output: func(stdout string, t tig.T) { + helpers.Command("images").Run(&test.Expected{ + Output: expect.Contains(""), + }) + }, + } + }, + }, { Description: "Remove image with created container - without -f", NoParallel: true, diff --git a/pkg/cmd/image/remove.go b/pkg/cmd/image/remove.go index 5e5c9ccd9fd..5ac57620565 100644 --- a/pkg/cmd/image/remove.go +++ b/pkg/cmd/image/remove.go @@ -22,8 +22,11 @@ import ( "fmt" "strings" + "github.com/opencontainers/go-digest" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" @@ -32,6 +35,15 @@ import ( "github.com/containerd/nerdctl/v2/pkg/platformutil" ) +// danglingRefName builds a dangling-image name unique to the given digest, so that +// force-removing more than one running image's image in the same invocation does not +// collide on image creation: containerd's image store requires unique names, and a +// fixed ":" name is shared by every dangling ref. See: +// https://github.com/containerd/nerdctl/issues/4109 +func danglingRefName(dgst digest.Digest) string { + return ":" + dgst.String() +} + // Remove removes a list of `images`. func Remove(ctx context.Context, client *containerd.Client, args []string, options types.ImageRemoveOptions) error { var delOpts []images.DeleteOpt @@ -84,10 +96,12 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio if cid, ok := runningImages[found.Image.Name]; ok { if options.Force { // This is a running image, so, we need to keep a ref on it so that containerd does not GC the layers - // First create the new image with an empty name + // First create the new image with a dangling name unique to its digest: a fixed ":" name + // collides ("image \":\": already exists") when force-removing more than one running + // image's image in the same invocation. originalName := found.Image.Name - found.Image.Name = ":" - if _, err = is.Create(ctx, found.Image); err != nil { + found.Image.Name = danglingRefName(found.Image.Target.Digest) + if _, err = is.Create(ctx, found.Image); err != nil && !errdefs.IsAlreadyExists(err) { return err } @@ -137,10 +151,12 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio if cid, ok := runningImages[found.Image.Name]; ok { if options.Force { // This is a running image, so, we need to keep a ref on it so that containerd does not GC the layers - // First create the new image with an empty name + // First create the new image with a dangling name unique to its digest: a fixed ":" name + // collides ("image \":\": already exists") when force-removing more than one running + // image's image in the same invocation. originalName := found.Image.Name - found.Image.Name = ":" - if _, err = is.Create(ctx, found.Image); err != nil { + found.Image.Name = danglingRefName(found.Image.Target.Digest) + if _, err = is.Create(ctx, found.Image); err != nil && !errdefs.IsAlreadyExists(err) { return false, err } diff --git a/pkg/imgutil/filtering.go b/pkg/imgutil/filtering.go index c7ffeb3f49e..f7efd6c739c 100644 --- a/pkg/imgutil/filtering.go +++ b/pkg/imgutil/filtering.go @@ -323,8 +323,11 @@ func matchesAllLabels(imageCfgLabels map[string]string, filterLabels map[string] func matchesReferences(image images.Image, referencePatterns []string) (bool, error) { var matches int - // Containerd returns ":" for dangling untagged images - see https://github.com/containerd/nerdctl/issues/3852 - if image.Name == ":" { + // Dangling untagged images are named ":" or ":" (see + // https://github.com/containerd/nerdctl/issues/3852 and + // https://github.com/containerd/nerdctl/issues/4109), neither of which is a + // parsable reference. + if strings.HasPrefix(image.Name, ":") { return false, nil } diff --git a/pkg/imgutil/filtering_test.go b/pkg/imgutil/filtering_test.go index 7b0b3995f74..c28d2d10ff7 100644 --- a/pkg/imgutil/filtering_test.go +++ b/pkg/imgutil/filtering_test.go @@ -271,6 +271,29 @@ func TestFilterByReference(t *testing.T) { referencePatterns: []string{"foobar"}, expectedImages: []images.Image{}, }, + { + // Dangling refs kept alive by `rmi -f` on a running image are named ":" or, since + // #4109, ":". Neither is a parsable reference, so they must be skipped + // rather than erroring out the whole filter. See issues #3852 and #4109. + name: "SkipsDanglingRefsWithoutErroring", + images: []images.Image{ + { + Name: "foo:latest", + }, + { + Name: ":", + }, + { + Name: ":sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + }, + referencePatterns: []string{"foo"}, + expectedImages: []images.Image{ + { + Name: "foo:latest", + }, + }, + }, } for _, test := range tests { From bf10e0f27892742e48e3fb499846fa4397fb9b2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:34:12 +0000 Subject: [PATCH 866/868] build(deps): bump github.com/fluent/fluent-logger-golang Bumps [github.com/fluent/fluent-logger-golang](https://github.com/fluent/fluent-logger-golang) from 1.10.1 to 1.10.2. - [Changelog](https://github.com/fluent/fluent-logger-golang/blob/master/CHANGELOG.md) - [Commits](https://github.com/fluent/fluent-logger-golang/compare/v1.10.1...v1.10.2) --- updated-dependencies: - dependency-name: github.com/fluent/fluent-logger-golang dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9537a2dbd50..df53fe64008 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined github.com/fatih/color v1.19.0 //gomodjail:unconfined - github.com/fluent/fluent-logger-golang v1.10.1 //gomodjail:unconfined + github.com/fluent/fluent-logger-golang v1.10.2 //gomodjail:unconfined github.com/fsnotify/fsnotify v1.10.1 //gomodjail:unconfined github.com/go-viper/mapstructure/v2 v2.5.0 github.com/ipfs/go-cid v0.6.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 0c383d18398..f47a69e1d8e 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fluent/fluent-logger-golang v1.10.1 h1:wu54iN1O2afll5oQrtTjhgZRwWcfOeFFzwRsEkABfFQ= -github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= +github.com/fluent/fluent-logger-golang v1.10.2 h1:1UQHk9rmXy9rkQn5naJ+qsrIdBhkozo0wa64mhe7q5E= +github.com/fluent/fluent-logger-golang v1.10.2/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb21xfNYOzn0roAtp7mfGAE= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= From 2d0158fff474c92605512197b93f4afe7d44670f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:34:26 +0000 Subject: [PATCH 867/868] build(deps): bump github.com/containerd/nydus-snapshotter Bumps [github.com/containerd/nydus-snapshotter](https://github.com/containerd/nydus-snapshotter) from 0.16.0 to 0.16.1. - [Release notes](https://github.com/containerd/nydus-snapshotter/releases) - [Commits](https://github.com/containerd/nydus-snapshotter/compare/v0.16.0...v0.16.1) --- updated-dependencies: - dependency-name: github.com/containerd/nydus-snapshotter dependency-version: 0.16.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9537a2dbd50..07d7b38c89c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/containerd/imgcrypt/v2 v2.0.3 //gomodjail:unconfined github.com/containerd/log v0.2.0 //gomodjail:unconfined github.com/containerd/nerdctl/mod/tigron v0.0.0 //gomodjail:unconfined - github.com/containerd/nydus-snapshotter v0.16.0 //gomodjail:unconfined + github.com/containerd/nydus-snapshotter v0.16.1 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 0c383d18398..493219f28d0 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/containerd/log/otel v0.1.0 h1:Az5rFFo0+c4v2yC8ROR63sWsZpKl/vhtUnUhqLSpE6U= github.com/containerd/log/otel v0.1.0/go.mod h1:65C5iYF2xIQByCyxzoO2KKKK1+/tGxD4XqhdlrTtvzE= -github.com/containerd/nydus-snapshotter v0.16.0 h1:ruNhliMvo7D5nOHIWmt6nHZbJZephVMX5MclRDYHZ/8= -github.com/containerd/nydus-snapshotter v0.16.0/go.mod h1:bOppnH1X941XidPS8FadVSijJ/pIOK8L+Mc0fGFw74A= +github.com/containerd/nydus-snapshotter v0.16.1 h1:vwZjXgVG+DGVTz8yON4n/jNAgCF1Z/Yzwc31VX+4jk4= +github.com/containerd/nydus-snapshotter v0.16.1/go.mod h1:yr9Cwv+rg+FeG0dQ4YJKIew0VwKq4tWREbDXjbGm6Ho= github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= From 2796f524b025267770e299f0af3b5601cb4c34e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:33:21 +0000 Subject: [PATCH 868/868] build(deps): bump the docker group across 1 directory with 2 updates Bumps the docker group with 2 updates in the / directory: [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/moby/moby/client` from 0.6.0 to 0.6.1 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.6.1/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.6.0...v0.6.1) Updates `github.com/moby/moby/v2` from 2.0.0-beta.24 to 2.0.0-beta.25 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.24...v2.0.0-beta.25) --- updated-dependencies: - dependency-name: github.com/moby/moby/client dependency-version: 0.6.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 40 +++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 9537a2dbd50..913c09415bf 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ //gomodjail:confined module github.com/containerd/nerdctl/v2 -go 1.26.6 +go 1.26.8 // containerd, Docker/Moby, OCI, and golang.org/x packages are trusted //gosocialcheck:trusted @@ -27,8 +27,8 @@ require ( github.com/docker/cli v29.8.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 //gomodjail:unconfined github.com/docker/go-units v0.5.0 - github.com/moby/moby/client v0.6.0 //gomodjail:unconfined - github.com/moby/moby/v2 v2.0.0-beta.24 //gomodjail:unconfined + github.com/moby/moby/client v0.6.1 //gomodjail:unconfined + github.com/moby/moby/v2 v2.0.0-beta.25 //gomodjail:unconfined github.com/moby/sys/mount v0.3.5 //gomodjail:unconfined github.com/moby/sys/signal v0.7.1 //gomodjail:unconfined github.com/moby/sys/user v0.4.1 //gomodjail:unconfined @@ -48,7 +48,7 @@ require ( require ( github.com/Masterminds/semver/v3 v3.5.0 - github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 github.com/Microsoft/hcsshim v0.15.0-rc.4 github.com/compose-spec/compose-go/v2 v2.15.0 //gomodjail:unconfined github.com/containernetworking/cni v1.3.1 //gomodjail:unconfined @@ -88,12 +88,12 @@ require ( github.com/containerd/log/otel v0.1.0 // indirect github.com/containerd/plugin v1.1.0 // indirect //gomodjail:unconfined - github.com/containerd/ttrpc v1.2.9 // indirect + github.com/containerd/ttrpc v1.2.10 // indirect //gomodjail:unconfined github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/moby/api v1.56.0 // indirect + github.com/moby/moby/api v1.56.1 // indirect //gomodjail:unconfined github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/symlink v0.3.0 // indirect @@ -102,7 +102,7 @@ require ( require ( cyphar.com/go-pathrs v0.2.5 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect //gomodjail:unconfined @@ -171,7 +171,7 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect //gomodjail:unconfined - google.golang.org/grpc v1.83.2 // indirect + google.golang.org/grpc v1.84.0 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12 // indirect //gomodjail:unconfined diff --git a/go.sum b/go.sum index 0c383d18398..8ca853fa4d2 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,14 @@ 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= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a h1:4hxax9ktNjSDoFn1tZSeL6gXMTRMpO0FzjdKfT01jgY= +github.com/Azure/go-ansiterm v0.0.0-20260917205352-e937bb47801a/go.mod h1:3EWSSOZ50kb+arhww0qIaEXHToib/3FjBj9Jj5lcP5g= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= -github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0 h1:71VU8kW/LCxJkx41+YYys1EgQO+AUnPa1TrxTSyYUCU= +github.com/Microsoft/go-winio v0.6.3-0.20260930231756-f19d9717deb0/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg= github.com/Microsoft/hcsshim v0.15.0-rc.4 h1:aZFX4LH0S20Lgjq0wG61StIClj7im4yzrxIClkaR8Z8= github.com/Microsoft/hcsshim v0.15.0-rc.4/go.mod h1:BA9CBztgu4h/6Jsvo1O1M4qjWw09PoYpaEYgexPE578= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= @@ -62,8 +62,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+XesH/6BBuJcdtV6ymGlGg= github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= -github.com/containerd/ttrpc v1.2.9 h1:ha0ak962T0s3CA/RoZ6S6xiWZQF24GrBaEpiGX1uihg= -github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= +github.com/containerd/ttrpc v1.2.10 h1:qQvy3mokhoXON7FXiOOc05O7Up6nrNj6amJF8urNvDc= +github.com/containerd/ttrpc v1.2.10/go.mod h1:YrmEQKkMbBA4EM26AzVAIwHGIYgptkL4CJ/yVTaDyBE= 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.1 h1:gnHlU/YC1bJcS+CSh+pYQKeuu4O4gsMGW1NMaLhkzW4= @@ -157,8 +157,8 @@ github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXN github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= -github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= +github.com/mdlayher/socket v0.7.0 h1:qVREPVwtUMg17pwvveQxpurq0PVisMxi3FGgpsorMYQ= +github.com/mdlayher/socket v0.7.0/go.mod h1:f7iKql2EK/rfsWYDKofKjk2Ig7I+6HQWdMIdn1tO4A4= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -169,12 +169,12 @@ 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.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= -github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= -github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= -github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= -github.com/moby/moby/v2 v2.0.0-beta.24 h1:sWv7EckkDJhpraR9yUjkw1XeVf39+MWd58E22PYloNA= -github.com/moby/moby/v2 v2.0.0-beta.24/go.mod h1:3zRfNirit+BkquckweT36STGqB+A3iUXp1zARg13JHM= +github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= +github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.1 h1:gvfKmdcWw+9MzXYP0vAUKQtBTGu1L8475K2nsmL7O98= +github.com/moby/moby/client v0.6.1/go.mod h1:XHgTFqz9NCgS/VuoxXMmEDVmVgXQ4vFEc15wsCIwb24= +github.com/moby/moby/v2 v2.0.0-beta.25 h1:T3ztFEq5TKycgaid+S9DuEIyRAGx9fH+IeCo48qzM2I= +github.com/moby/moby/v2 v2.0.0-beta.25/go.mod h1:L9F2T01kSIyYzbojuyYCcNRJEoXXSP1IxPXheNcdFR0= 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= @@ -225,8 +225,6 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= 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= @@ -330,7 +328,7 @@ golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -373,15 +371,15 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=