From 9cb28b4a31a44c79f5ccc2ecaf3b67a4b5a65a7b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sat, 10 May 2025 00:26:00 +0900 Subject: [PATCH 001/378] 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/378] 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/378] 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/378] 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/378] 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/378] (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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] [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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] --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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] (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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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/378] 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++ {