diff --git a/.ko.yaml b/.ko.yaml index 774665cec..8f61d60ea 100644 --- a/.ko.yaml +++ b/.ko.yaml @@ -21,3 +21,11 @@ defaultPlatforms: baseImageOverrides: github.com/agent-substrate/substrate/demos/sandbox: alpine github.com/agent-substrate/substrate/demos/agent-secret: alpine + # ateom-microvm owns the cloud-hypervisor boot and builds the actor's writable + # virtio-blk rootfs at runtime, which needs mkfs.ext4 (e2fsprogs) plus glibc for + # the fetched cloud-hypervisor binary. The committed debian:stable-slim base has + # glibc + coreutils but NOT mkfs.ext4, so this default cannot build the rootfs on + # its own. hack/run-microvm-demo.sh builds hack/ateom-base (debian-slim + + # e2fsprogs) and overrides this base at build time via KO_CONFIG_PATH, so running + # the demo never edits this file. The committed default stays debian:stable-slim. + github.com/agent-substrate/substrate/cmd/ateom-microvm: debian:stable-slim diff --git a/LICENSES/github.com/containerd/log/LICENSE b/LICENSES/github.com/containerd/log/LICENSE new file mode 100644 index 000000000..584149b6e --- /dev/null +++ b/LICENSES/github.com/containerd/log/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright The containerd Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/github.com/containerd/ttrpc/LICENSE b/LICENSES/github.com/containerd/ttrpc/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSES/github.com/containerd/ttrpc/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/github.com/pelletier/go-toml/v2/LICENSE b/LICENSES/github.com/pelletier/go-toml/v2/LICENSE new file mode 100644 index 000000000..991e2ae96 --- /dev/null +++ b/LICENSES/github.com/pelletier/go-toml/v2/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +go-toml v2 +Copyright (c) 2021 - 2023 Thomas Pelletier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSES/third_party/kata/LICENSE b/LICENSES/third_party/kata/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSES/third_party/kata/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply.go b/cmd/atecontroller/internal/controllers/workerpool_apply.go index 9ce6a9c4d..faa94f572 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply.go @@ -60,6 +60,7 @@ func buildDeploymentApplyConfig(wp *atev1alpha1.WorkerPool) *appsv1ac.Deployment WithType(corev1.HostPathDirectoryOrCreate))) applyWorkerPoolPodTemplate(podSpecAC, containerAC, wp.Spec.Template) + maybeApplyMicroVMPodShape(podSpecAC, containerAC, wp.Spec.SandboxClass) podSpecAC.WithContainers(containerAC) return appsv1ac.Deployment(deploymentName(wp.Name), wp.Namespace). @@ -81,6 +82,53 @@ func buildDeploymentApplyConfig(wp *atev1alpha1.WorkerPool) *appsv1ac.Deployment WithSpec(podSpecAC))) } +// maybeApplyMicroVMPodShape adds the /dev/kvm device and node placement a +// micro-VM (kata + cloud-hypervisor) worker pool needs, on top of any +// pod-template settings. No-op unless sandboxClass is the micro-VM class. +// +// TODO: this hardcodes one sandbox class's pod requirements in the controller. +// Consider making it generic so a sandbox class can declare its own pod shape +// (e.g. required devices/mounts + node placement on the SandboxConfig spec) +// instead of branching on SandboxClass here, so new classes don't need a +// controller change. +func maybeApplyMicroVMPodShape( + podSpecAC *corev1ac.PodSpecApplyConfiguration, + containerAC *corev1ac.ContainerApplyConfiguration, + sandboxClass atev1alpha1.SandboxClass, +) { + if sandboxClass != atev1alpha1.SandboxClassMicroVM { + return + } + + // The micro-VM runtime needs /dev/kvm. The container is already privileged + // (so it can also reach vhost devices), but we mount /dev/kvm explicitly. + containerAC.WithVolumeMounts(corev1ac.VolumeMount(). + WithName("dev-kvm"). + WithMountPath("/dev/kvm")) + podSpecAC.WithVolumes(corev1ac.Volume(). + WithName("dev-kvm"). + WithHostPath(corev1ac.HostPathVolumeSource(). + WithPath("/dev/kvm"). + WithType(corev1.HostPathCharDev))) + + // Pin placement to KVM-capable, nested-virt nodes via nodeSelector + + // toleration on ate.dev/sandboxClass=microvm. This is our own convention + // (GKE attaches no label/taint to nested-virt pools): applied to kind nodes + // by hack/create-kind-cluster.sh and via --node-labels at GKE pool creation. + // Additive on top of the WorkerPool's configurable scheduling fields + // (spec.template nodeSelector/tolerations/affinity, added in #247) — merge, + // don't overwrite. + if podSpecAC.NodeSelector == nil { + podSpecAC.NodeSelector = map[string]string{} + } + podSpecAC.NodeSelector["ate.dev/sandboxClass"] = string(atev1alpha1.SandboxClassMicroVM) + podSpecAC.WithTolerations(corev1ac.Toleration(). + WithKey("ate.dev/sandboxClass"). + WithOperator(corev1.TolerationOpEqual). + WithValue(string(atev1alpha1.SandboxClassMicroVM)). + WithEffect(corev1.TaintEffectNoSchedule)) +} + func applyWorkerPoolPodTemplate( podSpecAC *corev1ac.PodSpecApplyConfiguration, containerAC *corev1ac.ContainerApplyConfiguration, diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go index 7e8de95d5..1cdc972d6 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go @@ -209,6 +209,58 @@ func TestBuildDeploymentApplyConfig(t *testing.T) { } } +// TestMicroVMPodShape asserts the micro-VM sandbox class adds the /dev/kvm +// device (volume + container mount) and node placement (nodeSelector + +// toleration on ate.dev/sandboxClass); other classes get none of it. +func TestMicroVMPodShape(t *testing.T) { + tests := []struct { + name string + class atev1alpha1.SandboxClass + wantMicroVM bool + }{ + {"gvisor default", "", false}, + {"gvisor explicit", atev1alpha1.SandboxClassGvisor, false}, + {"microvm", atev1alpha1.SandboxClassMicroVM, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wp := testWorkerPoolApplyConfig(nil) + wp.Spec.SandboxClass = tt.class + ps := buildDeploymentApplyConfig(wp).Spec.Template.Spec + + hasVol := false + for _, v := range ps.Volumes { + if v.Name != nil && *v.Name == "dev-kvm" { + hasVol = true + if v.HostPath == nil || v.HostPath.Path == nil || *v.HostPath.Path != "/dev/kvm" || + v.HostPath.Type == nil || *v.HostPath.Type != corev1.HostPathCharDev { + t.Errorf("dev-kvm volume = %+v, want /dev/kvm CharDevice", v.HostPath) + } + } + } + hasMount := false + for _, c := range ps.Containers { + for _, m := range c.VolumeMounts { + if m.MountPath != nil && *m.MountPath == "/dev/kvm" { + hasMount = true + } + } + } + _, hasSelector := ps.NodeSelector["ate.dev/sandboxClass"] + hasTol := false + for _, tol := range ps.Tolerations { + if tol.Key != nil && *tol.Key == "ate.dev/sandboxClass" { + hasTol = true + } + } + if hasVol != tt.wantMicroVM || hasMount != tt.wantMicroVM || hasSelector != tt.wantMicroVM || hasTol != tt.wantMicroVM { + t.Errorf("microvm shape: vol=%v mount=%v selector=%v toleration=%v, want all %v", + hasVol, hasMount, hasSelector, hasTol, tt.wantMicroVM) + } + }) + } +} + func testWorkerPoolApplyConfig(tmpl *atev1alpha1.WorkerPoolPodTemplate) *atev1alpha1.WorkerPool { return &atev1alpha1.WorkerPool{ ObjectMeta: metav1.ObjectMeta{Name: "pool", Namespace: "default", UID: "uid"}, diff --git a/cmd/atelet/internal/ategcs/gcs.go b/cmd/atelet/internal/ategcs/gcs.go index 4332bd0e5..8b7c00f91 100644 --- a/cmd/atelet/internal/ategcs/gcs.go +++ b/cmd/atelet/internal/ategcs/gcs.go @@ -35,6 +35,15 @@ func (g *gcsClient) GetObject(ctx context.Context, bucket, object string) (io.Re return g.client.Bucket(bucket).Object(object).NewReader(ctx) } +// supportsStreamingPut is the streamingPutter marker: the GCS client's PutObject +// accepts a non-seekable streaming body without buffering (it copies the reader +// straight into a storage.Writer — no Content-Length / signing requirement), so +// callers can pipe compression directly into the upload (overlap) instead of +// staging a seekable temp file. (S3's PutObject needs a seekable body, so s3Client +// does NOT implement this — see objects.go sendZstd.) Never called: its presence is +// the signal. +func (g *gcsClient) supportsStreamingPut() {} + func (g *gcsClient) PutObject(ctx context.Context, bucket, object string, reader io.Reader) error { wc := g.client.Bucket(bucket).Object(object).NewWriter(ctx) // io.Copy reports local read errors; wc.Close() reports the actual diff --git a/cmd/atelet/internal/ategcs/objects.go b/cmd/atelet/internal/ategcs/objects.go index 3fa9f0e61..66e4d32ba 100644 --- a/cmd/atelet/internal/ategcs/objects.go +++ b/cmd/atelet/internal/ategcs/objects.go @@ -17,12 +17,15 @@ package ategcs import ( "bytes" "context" + "encoding/binary" "fmt" "io" "log/slog" "net/url" "os" + "runtime" "strings" + "time" "github.com/klauspost/compress/zstd" "go.opentelemetry.io/otel" @@ -106,20 +109,80 @@ func SendLocalFileToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL } }() - if err := sendToGCSWithZstd(ctx, client, gsURL, localFile); err != nil { - return fmt.Errorf("in sendToGCSWithZstd: %w", err) + if err := sendZstd(ctx, client, gsURL, localFile); err != nil { + return fmt.Errorf("in sendZstd: %w", err) } return nil } -func sendToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL string, content io.Reader) (err error) { +// streamingPutter marks an ObjectStorage whose PutObject accepts a non-seekable +// streaming body without buffering (e.g. GCS): implementing the interface is the +// signal, so the marker method is never called. See gcsClient. +type streamingPutter interface{ supportsStreamingPut() } + +// writeContentResult reports what writeContent compressed. +type writeContentResult struct { + // logicalBytes is the total logical size of the source, including the holes + // for a sparse file. + logicalBytes int64 + // populatedBytes is the count of bytes actually read + compressed: the non-hole + // (resident) set for the sparse-extent format, == logicalBytes for a plain stream. + populatedBytes int64 + // sparse is true when the sparse-extent format was used (the source was a file). + sparse bool +} + +// writeContent compresses content to out, choosing the sparse-extent format for a +// seekable *os.File (compress only the populated extents, skip the holes) or a +// plain zstd stream otherwise. It touches only io, so it is unit-testable without +// an object store, and is shared by the buffered and streaming upload paths. +func writeContent(out io.Writer, content io.Reader) (writeContentResult, error) { + if f, ok := content.(*os.File); ok { + logical, populated, err := writeSparseZstd(out, f) + if err != nil { + return writeContentResult{}, err + } + return writeContentResult{logicalBytes: logical, populatedBytes: populated, sparse: true}, nil + } + logical, err := plainZstd(out, content) + if err != nil { + return writeContentResult{}, err + } + return writeContentResult{logicalBytes: logical, populatedBytes: logical}, nil +} + +// sendZstd zstd-compresses content and uploads it to gsURL. +// +// The snapshot memory-ranges is the large object here (the whole guest RAM image, +// mostly zero) on the SUSPEND critical path, so we compress with SpeedFastest across +// all CPUs — high-ratio levels scan the multi-GiB image far slower for little size +// gain on near-zero data, and the decoder auto-detects the level so restore + older +// snapshots are unaffected. +// +// Upload strategy depends on the backend: +// - Streaming backends (GCS) accept a non-seekable body, so we pipe the compressor +// straight into PutObject: the compress overlaps the network PUT and we never +// stage the ~100MiB compressed payload to a temp file. +// - S3/rustfs PutObject hands the body to the AWS SDK, which needs a seekable body +// to sign + set Content-Length (a non-seekable pipe hangs there), so we compress +// to a SEEKABLE temp file first. +func sendZstd(ctx context.Context, client ObjectStorage, gsURL string, content io.Reader) error { bucket, object, err := parseGCSURL(gsURL) if err != nil { return fmt.Errorf("while parsing URL: %w", err) } + tStart := time.Now() + if _, ok := client.(streamingPutter); ok { + return sendStreamingZstd(ctx, client, bucket, object, content, tStart) + } + return sendBufferedZstd(ctx, client, bucket, object, content, tStart) +} - // Create a temporary file to store compressed data +// sendBufferedZstd compresses content to a seekable temp file, then uploads it. +// Used for backends (S3/rustfs) whose PutObject needs a seekable body to sign and +// set Content-Length; the streaming counterpart is sendStreamingZstd. +func sendBufferedZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time) error { tmpFile, err := os.CreateTemp("", "substrate-upload-compress-") if err != nil { return fmt.Errorf("while creating temp compress file: %w", err) @@ -127,32 +190,82 @@ func sendToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL string, defer os.Remove(tmpFile.Name()) defer tmpFile.Close() - zwc, err := zstd.NewWriter(tmpFile) + t0 := time.Now() + res, err := writeContent(tmpFile, content) if err != nil { - return fmt.Errorf("while creating zstd writer: %w", err) + return fmt.Errorf("while compressing %q: %w", object, err) } + dCompress := time.Since(t0) - _, err = io.Copy(zwc, content) - if err != nil { - zwc.Close() - return fmt.Errorf("while compressing data to temp file: %w", err) + if _, err := tmpFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("while seeking temp file: %w", err) } - if err := zwc.Close(); err != nil { - return fmt.Errorf("while closing zstd writer: %w", err) + if err := client.PutObject(ctx, bucket, object, tmpFile); err != nil { + return fmt.Errorf("while putting object %q: %w", object, err) } + slog.InfoContext(ctx, "Compressed zstd upload", + slog.String("object", object), slog.Bool("sparse", res.sparse), + slog.Int64("logical_bytes", res.logicalBytes), slog.Int64("populated_bytes", res.populatedBytes), + slog.Duration("compress", dCompress), slog.Duration("total", time.Since(tStart))) + return nil +} - // Seek back to the beginning of the temp file - if _, err := tmpFile.Seek(0, 0); err != nil { - return fmt.Errorf("while seeking temp file: %w", err) +// sendStreamingZstd compresses content and uploads it in one overlapped pass: a +// goroutine writes the (sparse-extent or plain) zstd stream into an io.Pipe while +// PutObject streams the read end to the object store. No seekable temp file, and +// the compress runs concurrently with the network PUT. Used only for streaming +// backends (GCS); see sendZstd. +func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time) error { + type result struct { + res writeContentResult + err error } + pr, pw := io.Pipe() + ch := make(chan result, 1) + go func() { + res, err := writeContent(pw, content) + // Closing the writer delivers EOF (or the compress error) to PutObject. + _ = pw.CloseWithError(err) + ch <- result{res: res, err: err} + }() - // Upload the seekable temp file - if err := client.PutObject(ctx, bucket, object, tmpFile); err != nil { - return fmt.Errorf("while putting object: %w", err) + putErr := client.PutObject(ctx, bucket, object, pr) + if putErr != nil { + // PutObject bailed (e.g. mid-stream); unblock the compressor goroutine so it + // can finish and we don't deadlock on the channel receive below. + _ = pr.CloseWithError(putErr) + } + r := <-ch + if putErr != nil { + return fmt.Errorf("while putting object %q: %w", object, putErr) + } + if r.err != nil { + return fmt.Errorf("while compressing %q: %w", object, r.err) } + slog.InfoContext(ctx, "Compressed zstd upload", + slog.String("object", object), slog.Bool("sparse", r.res.sparse), slog.Bool("streaming", true), + slog.Int64("logical_bytes", r.res.logicalBytes), slog.Int64("populated_bytes", r.res.populatedBytes), + slog.Duration("total", time.Since(tStart))) return nil } +// plainZstd writes src to w as a single plain zstd stream (SpeedFastest, all +// cores) and returns the uncompressed byte count. +func plainZstd(w io.Writer, src io.Reader) (int64, error) { + zw, err := zstd.NewWriter(w, + zstd.WithEncoderLevel(zstd.SpeedFastest), + zstd.WithEncoderConcurrency(runtime.GOMAXPROCS(0))) + if err != nil { + return 0, err + } + n, err := io.Copy(zw, src) + if err != nil { + zw.Close() + return n, err + } + return n, zw.Close() +} + func FetchLocalFileFromGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL string, localFilePath string) (err error) { ctx, span := tracer.Start(ctx, "fetchLocalFileFromGCSWithZstd") defer span.End() @@ -202,18 +315,131 @@ func fetchFromGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL strin } }() - zrc, err := zstd.NewReader(rc, zstd.WithDecoderConcurrency(1)) + t0 := time.Now() + res, err := decodeContent(out, rc) if err != nil { - return fmt.Errorf("in zstd.NewReader: %w", err) + return err + } + slog.InfoContext(ctx, "Decompressed zstd download", + slog.Bool("sparse", res.sparse), slog.Int64("logical_bytes", res.logicalBytes), + slog.Int64("written_bytes", res.writtenBytes), slog.Duration("took", time.Since(t0))) + return nil +} + +// decodeContentResult reports what decodeContent decompressed. +type decodeContentResult struct { + // logicalBytes is the logical size written to out (the original image size). + logicalBytes int64 + // writtenBytes is the count of non-hole bytes actually written on the sparse + // file path; 0 on the io.Copy fallback (non-file destination). + writtenBytes int64 + // sparse is true when the input used the sparse-extent format. + sparse bool +} + +// decodeContent decompresses src into out, auto-detecting the format from the +// leading magic: the sparse-extent format (sparseMagic) vs a plain zstd stream +// (older snapshots, or the non-file upload path). When out is an *os.File the plain +// path writes SPARSE (skips zero blocks → holes) so only the resident set is +// written, not a dense multi-GiB image. It touches only io, so it is unit-testable +// without an object store, mirroring writeContent. +func decodeContent(out io.Writer, src io.Reader) (decodeContentResult, error) { + magic := make([]byte, len(sparseMagic)) + n, rerr := io.ReadFull(src, magic) + if rerr == nil && string(magic) == sparseMagic { + f, ok := out.(*os.File) + if !ok { + return decodeContentResult{}, fmt.Errorf("sparse-extent snapshot requires a file destination, got %T", out) + } + size, derr := readSparseZstd(f, src) // src is positioned just after the magic + if derr != nil { + return decodeContentResult{}, fmt.Errorf("in sparse-extent decode: %w", derr) + } + return decodeContentResult{logicalBytes: size, sparse: true}, nil + } + if rerr != nil && rerr != io.EOF && rerr != io.ErrUnexpectedEOF { + return decodeContentResult{}, fmt.Errorf("while reading object header: %w", rerr) } - defer zrc.Close() - _, err = io.Copy(out, zrc) + // Plain zstd stream: put back the peeked bytes, then decompress. + r := io.MultiReader(bytes.NewReader(magic[:n]), src) + zrc, err := zstd.NewReader(r, zstd.WithDecoderConcurrency(1)) if err != nil { - return fmt.Errorf("in io.Copy: %w", err) + return decodeContentResult{}, fmt.Errorf("in zstd.NewReader: %w", err) + } + defer zrc.Close() + if f, ok := out.(*os.File); ok { + size, written, derr := copyZstdSparse(f, zrc) + if derr != nil { + return decodeContentResult{}, fmt.Errorf("in sparse decompress: %w", derr) + } + return decodeContentResult{logicalBytes: size, writtenBytes: written}, nil + } + size, cerr := io.Copy(out, zrc) + if cerr != nil { + return decodeContentResult{}, fmt.Errorf("in io.Copy: %w", cerr) + } + return decodeContentResult{logicalBytes: size}, nil +} + +// copyZstdSparse writes src into dst skipping all-zero blocks, so dst becomes a +// sparse file (the skipped regions are holes). Returns the logical size (total bytes +// consumed from src) and the bytes actually written (non-zero). dst is truncated to +// empty first (so skipped regions are real holes, not stale bytes) and to the +// logical size at the end (so trailing zero regions become a hole and the size is +// exact). dst must be a regular file opened for writing. +func copyZstdSparse(dst *os.File, src io.Reader) (size int64, written int64, err error) { + // Start from an empty file so the holes we skip can't expose pre-existing bytes: + // this writes out only the non-zero chunks, it does not overlay onto dst. + if err := dst.Truncate(0); err != nil { + return 0, 0, fmt.Errorf("truncating dst: %w", err) + } + // 64KiB blocks: a multiple of the 4KiB fs block (so skipped runs align to whole + // hole-able blocks) while keeping the zero-scan + WriteAt syscall count modest. + const block = 64 << 10 + buf := make([]byte, block) + var pos int64 + for { + n, rerr := io.ReadFull(src, buf) + if n > 0 { + chunk := buf[:n] + if !allZero(chunk) { + if _, werr := dst.WriteAt(chunk, pos); werr != nil { + return 0, 0, werr + } + written += int64(n) + } + pos += int64(n) + } + if rerr == io.EOF || rerr == io.ErrUnexpectedEOF { + break + } + if rerr != nil { + return 0, 0, rerr + } } + // Materialize the exact logical size: extends past the last written byte with a + // hole when the tail was zero (skipped), and is a no-op otherwise. + if terr := dst.Truncate(pos); terr != nil { + return 0, 0, terr + } + return pos, written, nil +} - return nil +// allZero reports whether b is all zero bytes, checking 8 bytes at a time. +func allZero(b []byte) bool { + i := 0 + for ; i+8 <= len(b); i += 8 { + if binary.LittleEndian.Uint64(b[i:]) != 0 { + return false + } + } + for ; i < len(b); i++ { + if b[i] != 0 { + return false + } + } + return true } func parseGCSURL(gsURL string) (string, string, error) { diff --git a/cmd/atelet/internal/ategcs/objects_test.go b/cmd/atelet/internal/ategcs/objects_test.go new file mode 100644 index 000000000..513d37b2b --- /dev/null +++ b/cmd/atelet/internal/ategcs/objects_test.go @@ -0,0 +1,508 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ategcs + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "syscall" + "testing" +) + +// memStore is an in-memory ObjectStorage for round-trip tests. +type memStore struct{ m map[string][]byte } + +func newMemStore() *memStore { return &memStore{m: map[string][]byte{}} } + +func (s *memStore) PutObject(_ context.Context, bucket, object string, r io.Reader) error { + b, err := io.ReadAll(r) + if err != nil { + return err + } + s.m[bucket+"/"+object] = b + return nil +} + +func (s *memStore) GetObject(_ context.Context, bucket, object string) (io.ReadCloser, error) { + b, ok := s.m[bucket+"/"+object] + if !ok { + return nil, fmt.Errorf("object %q/%q not found", bucket, object) + } + return io.NopCloser(bytes.NewReader(b)), nil +} + +// streamingMemStore is a memStore that advertises streaming PutObject support, so +// sendZstd takes the pipe (compress∥upload overlap) path used for GCS +// instead of staging a seekable temp file. +type streamingMemStore struct{ *memStore } + +func (s *streamingMemStore) supportsStreamingPut() {} + +// TestSparseUploadStreamingRoundTrip drives the STREAMING upload path (GCS-like +// backend) end-to-end through the real entry points: the object must still be the +// sparse-extent format (magic) and download byte-exact. This guards the pipe path +// that overlaps compression with the upload. +func TestSparseUploadStreamingRoundTrip(t *testing.T) { + const size = 8 << 20 + want := make([]byte, size) + regions := [][2]int{{0, 4096}, {2 << 20, 70000}, {size - 9000, 5000}} + for _, e := range regions { + for i := e[0]; i < e[0]+e[1]; i++ { + want[i] = byte((i*7)%251 + 1) + } + } + dir := t.TempDir() + srcPath := filepath.Join(dir, "memory-ranges") + src, err := os.Create(srcPath) + if err != nil { + t.Fatal(err) + } + if err := src.Truncate(size); err != nil { + t.Fatal(err) + } + for _, e := range regions { + if _, err := src.WriteAt(want[e[0]:e[0]+e[1]], int64(e[0])); err != nil { + t.Fatal(err) + } + } + src.Close() + + store := &streamingMemStore{newMemStore()} + ctx := context.Background() + const gsURL = "gs://bucket/snap/memory-ranges.zstd" + if err := SendLocalFileToGCSWithZstd(ctx, store, gsURL, srcPath); err != nil { + t.Fatalf("streaming upload: %v", err) + } + stored := store.m["bucket/snap/memory-ranges.zstd"] + if len(stored) < len(sparseMagic) || string(stored[:len(sparseMagic)]) != sparseMagic { + t.Fatalf("streaming-stored object is not sparse-extent format (magic=%q)", stored[:min(len(stored), len(sparseMagic))]) + } + if int64(len(stored)) >= size/2 { + t.Errorf("stored %d bytes; expected far less than logical %d (holes not skipped)", len(stored), size) + } + dstPath := filepath.Join(dir, "restored") + if err := FetchLocalFileFromGCSWithZstd(ctx, store, gsURL, dstPath); err != nil { + t.Fatalf("download: %v", err) + } + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("streaming round-trip mismatch: len(got)=%d len(want)=%d", len(got), len(want)) + } +} + +// TestSparseUploadDownloadRoundTrip drives the real upload+download entry points +// through an in-memory store: a genuinely sparse source file (multiple data extents +// separated by holes) must upload in the sparse-extent format (magic) and download +// byte-exact AND sparse on disk. This is the guest memory image, so correctness is +// non-negotiable. +func TestSparseUploadDownloadRoundTrip(t *testing.T) { + const size = 8 << 20 // 8 MiB logical + want := make([]byte, size) + // Three populated extents at varied offsets/sizes (aligned + unaligned), the + // rest holes — mirrors scattered resident pages in free RAM. + fill := func(start, n int) { + for i := start; i < start+n; i++ { + want[i] = byte((i*7)%251 + 1) // never zero + } + } + fill(0, 4096) // first page + fill(2<<20, 70000) // interior, crosses 64KiB boundaries + fill(size-9000, 5000) // near end, leaving a trailing hole + + // Write the source as a genuinely sparse file (holes between the extents). + dir := t.TempDir() + srcPath := filepath.Join(dir, "memory-ranges") + src, err := os.Create(srcPath) + if err != nil { + t.Fatal(err) + } + if err := src.Truncate(size); err != nil { + t.Fatal(err) + } + for _, e := range [][2]int{{0, 4096}, {2 << 20, 70000}, {size - 9000, 5000}} { + if _, err := src.WriteAt(want[e[0]:e[0]+e[1]], int64(e[0])); err != nil { + t.Fatal(err) + } + } + src.Close() + + store := newMemStore() + ctx := context.Background() + const gsURL = "gs://bucket/snap/memory-ranges.zstd" + if err := SendLocalFileToGCSWithZstd(ctx, store, gsURL, srcPath); err != nil { + t.Fatalf("upload: %v", err) + } + // The stored object must use the sparse-extent format (magic header). + stored := store.m["bucket/snap/memory-ranges.zstd"] + if len(stored) < len(sparseMagic) || string(stored[:len(sparseMagic)]) != sparseMagic { + t.Fatalf("stored object is not sparse-extent format (magic=%q)", stored[:min(len(stored), len(sparseMagic))]) + } + // The compressed object must be far smaller than the logical size (holes skipped). + if int64(len(stored)) >= size/2 { + t.Errorf("stored %d bytes; expected far less than logical %d (holes not skipped)", len(stored), size) + } + + dstPath := filepath.Join(dir, "restored") + if err := FetchLocalFileFromGCSWithZstd(ctx, store, gsURL, dstPath); err != nil { + t.Fatalf("download: %v", err) + } + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("round-trip mismatch: len(got)=%d len(want)=%d", len(got), len(want)) + } + if fi, err := os.Stat(dstPath); err == nil { + if blk := diskBlocks(fi); blk > 0 { + t.Logf("restored sparse: apparent=%d actual=%d", size, blk*512) + if blk*512 >= int64(size) { + t.Logf("note: restored file not sparse on this fs — correctness still holds") + } + } + } +} + +// TestSparseVersionRejected confirms the reader refuses a sparse snapshot whose +// format version it doesn't understand (rather than misparsing it). This is the +// guest memory image, so a future incompatible layout must fail loudly. +func TestSparseVersionRejected(t *testing.T) { + dir := t.TempDir() + srcPath := filepath.Join(dir, "memory-ranges") + f, err := os.Create(srcPath) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(1 << 20); err != nil { + t.Fatal(err) + } + if _, err := f.WriteAt([]byte("hello sparse"), 4096); err != nil { + t.Fatal(err) + } + if err := f.Sync(); err != nil { + t.Fatal(err) + } + if _, err := f.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + if _, _, err := writeSparseZstd(&buf, f); err != nil { + t.Fatalf("writeSparseZstd: %v", err) + } + f.Close() + + blob := buf.Bytes() + // The version is the little-endian uint32 immediately after the 8-byte magic; + // corrupt it so it no longer matches sparseVersion. + blob[len(sparseMagic)] ^= 0xFF + + out, err := os.Create(filepath.Join(dir, "out")) + if err != nil { + t.Fatal(err) + } + defer out.Close() + // readSparseZstd is called with the reader positioned just after the magic. + if _, err := readSparseZstd(out, bytes.NewReader(blob[len(sparseMagic):])); err == nil { + t.Fatal("expected an unsupported-version error, got nil") + } +} + +// TestWriteSparseZstdSkipsHoles asserts the encoder feeds ONLY the populated +// extents to zstd (not the whole logical image). Gated on the fs actually reporting +// the source as sparse (ext4/Linux does; macOS/APFS in CI dev may not — skipped there). +func TestWriteSparseZstdSkipsHoles(t *testing.T) { + const size = 8 << 20 + dir := t.TempDir() + srcPath := filepath.Join(dir, "memory-ranges") + src, err := os.Create(srcPath) + if err != nil { + t.Fatal(err) + } + if err := src.Truncate(size); err != nil { + t.Fatal(err) + } + data := make([]byte, 70000) + for i := range data { + data[i] = byte(i%251 + 1) + } + if _, err := src.WriteAt(data, 2<<20); err != nil { // one ~70KB extent in a sea of holes + t.Fatal(err) + } + if err := src.Sync(); err != nil { + t.Fatal(err) + } + if fi, err := os.Stat(srcPath); err != nil { + t.Fatal(err) + } else if blk := diskBlocks(fi); blk == 0 || blk*512 >= int64(size) { + t.Skipf("fs did not make the source sparse (actual=%d, logical=%d) — can't assert hole-skipping here", blk*512, size) + } + if _, err := src.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + logical, dataBytes, err := writeSparseZstd(&buf, src) + if err != nil { + t.Fatalf("writeSparseZstd: %v", err) + } + if logical != size { + t.Errorf("logical=%d, want %d", logical, size) + } + if dataBytes >= int64(size)/2 { + t.Errorf("dataBytes=%d fed to zstd; expected ~70KB (holes not skipped)", dataBytes) + } + t.Logf("fed %d data bytes for a %d logical image (holes skipped)", dataBytes, size) +} + +// TestPlainZstdBackwardCompatRoundTrip drives the NON-file upload path (plain zstd, +// no magic) and confirms the download auto-detects + restores it — i.e. snapshots +// written before the sparse-extent format still restore. +func TestPlainZstdBackwardCompatRoundTrip(t *testing.T) { + want := bytes.Repeat([]byte("agent-substrate snapshot payload\n"), 4096) + store := newMemStore() + ctx := context.Background() + const gsURL = "gs://bucket/snap/config.json.zstd" + // SendBytesToGCS is uncompressed; use sendZstd with a non-file reader to + // hit the plain-zstd branch (no magic). + if err := sendZstd(ctx, store, gsURL, bytes.NewReader(want)); err != nil { + t.Fatalf("upload: %v", err) + } + stored := store.m["bucket/snap/config.json.zstd"] + if len(stored) >= len(sparseMagic) && string(stored[:len(sparseMagic)]) == sparseMagic { + t.Fatal("non-file upload unexpectedly used the sparse-extent format") + } + dir := t.TempDir() + dstPath := filepath.Join(dir, "config.json") + if err := FetchLocalFileFromGCSWithZstd(ctx, store, gsURL, dstPath); err != nil { + t.Fatalf("download: %v", err) + } + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("plain round-trip mismatch: len(got)=%d len(want)=%d", len(got), len(want)) + } +} + +// diskBlocks returns the number of 512-byte blocks the file occupies on disk (for +// the sparseness check), or 0 if unavailable on this platform/fs. +func diskBlocks(fi os.FileInfo) int64 { + if st, ok := fi.Sys().(*syscall.Stat_t); ok { + return int64(st.Blocks) + } + return 0 +} + +// TestCopyZstdSparse checks the sparse decompress is byte-exact (it's the guest +// memory image — corruption = a dead guest) and actually punches holes for the +// zero regions. +func TestCopyZstdSparse(t *testing.T) { + // A mostly-zero image with non-zero data at the start, an interior block, the + // tail-but-not-end, and aligned + unaligned sizes. Mirrors a guest memory-ranges: + // scattered resident pages in a sea of zero (free) RAM. + const size = 4 << 20 // 4 MiB + want := make([]byte, size) + for i := 0; i < 4096; i++ { // first page + want[i] = byte(i%251 + 1) + } + for i := 1 << 20; i < (1<<20)+9000; i++ { // interior, crosses a 64KiB block boundary + want[i] = byte(i%253 + 1) + } + for i := size - 5000; i < size-1000; i++ { // near the end, leaving a trailing zero hole + want[i] = byte(i%249 + 1) + } + + dir := t.TempDir() + out := filepath.Join(dir, "memory-ranges") + f, err := os.Create(out) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + size64, written, err := copyZstdSparse(f, bytes.NewReader(want)) + if err != nil { + t.Fatalf("copyZstdSparse: %v", err) + } + if size64 != int64(len(want)) { + t.Errorf("logical size = %d, want %d", size64, len(want)) + } + if written >= int64(len(want)) { + t.Errorf("written %d bytes; expected far less than %d for a mostly-zero image (not sparse)", written, len(want)) + } + + got, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("round-trip mismatch: len(got)=%d len(want)=%d", len(got), len(want)) + } + + // Best-effort sparseness check: the file should occupy fewer 512-byte blocks than + // its apparent size (holes punched). fs-dependent, so only log if unavailable. + if fi, err := os.Stat(out); err == nil { + if st := diskBlocks(fi); st > 0 { + actual := st * 512 + t.Logf("sparse: apparent=%d actual=%d written=%d", len(want), actual, written) + if actual >= int64(len(want)) { + t.Logf("note: file not sparse on this fs (actual=%d >= apparent=%d) — correctness still holds", actual, len(want)) + } + } + } +} + +// TestWriteDecodeContentRoundTrip exercises the io-only compress/decompress halves +// (writeContent / decodeContent) directly — no object store — for both the sparse +// (file source) and plain (non-file reader) paths. +func TestWriteDecodeContentRoundTrip(t *testing.T) { + dir := t.TempDir() + + t.Run("sparse file", func(t *testing.T) { + const size = 8 << 20 + srcPath := filepath.Join(dir, "src") + src, err := os.Create(srcPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + if err := src.Truncate(size); err != nil { + t.Fatal(err) + } + data := make([]byte, 70000) + for i := range data { + data[i] = byte(i%251 + 1) + } + if _, err := src.WriteAt(data, 2<<20); err != nil { // one extent in a sea of holes + t.Fatal(err) + } + if err := src.Sync(); err != nil { + t.Fatal(err) + } + if _, err := src.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + wres, err := writeContent(&buf, src) + if err != nil { + t.Fatalf("writeContent: %v", err) + } + if !wres.sparse { + t.Error("writeContent: sparse=false for a file source") + } + if wres.logicalBytes != size { + t.Errorf("writeContent logicalBytes=%d, want %d", wres.logicalBytes, size) + } + + dstPath := filepath.Join(dir, "dst") + dst, err := os.Create(dstPath) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + dres, err := decodeContent(dst, &buf) + if err != nil { + t.Fatalf("decodeContent: %v", err) + } + if !dres.sparse { + t.Error("decodeContent: sparse=false for a sparse-extent stream") + } + if dres.logicalBytes != size { + t.Errorf("decodeContent logicalBytes=%d, want %d", dres.logicalBytes, size) + } + + want, err := os.ReadFile(srcPath) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("sparse round-trip mismatch: len(got)=%d len(want)=%d", len(got), len(want)) + } + }) + + t.Run("plain reader", func(t *testing.T) { + want := bytes.Repeat([]byte("substrate payload\n"), 1000) + var buf bytes.Buffer + wres, err := writeContent(&buf, bytes.NewReader(want)) + if err != nil { + t.Fatalf("writeContent: %v", err) + } + if wres.sparse { + t.Error("writeContent: sparse=true for a non-file reader") + } + var out bytes.Buffer + if _, err := decodeContent(&out, &buf); err != nil { + t.Fatalf("decodeContent: %v", err) + } + if !bytes.Equal(out.Bytes(), want) { + t.Fatalf("plain round-trip mismatch: len(got)=%d len(want)=%d", out.Len(), len(want)) + } + }) +} + +// TestCopyZstdSparseClearsStaleData exercises the defensive dst.Truncate(0): a dst +// that already holds bytes — here larger than and different from the new content — +// must come out byte-exact, with the would-be holes reading back as zero (not the +// stale bytes) and the file shrunk to the new logical size. +func TestCopyZstdSparseClearsStaleData(t *testing.T) { + const size = 2 << 20 + want := make([]byte, size) + for i := 0; i < 4096; i++ { // first page + want[i] = byte(i%251 + 1) + } + for i := 1 << 20; i < (1<<20)+4096; i++ { // an interior page, rest stays a hole + want[i] = byte(i%253 + 1) + } + + out := filepath.Join(t.TempDir(), "dst") + f, err := os.Create(out) + if err != nil { + t.Fatal(err) + } + defer f.Close() + // Pre-fill with stale non-zero bytes, larger than the new content, so the test + // also covers shrinking to the exact logical size. + stale := make([]byte, size+size/2) + for i := range stale { + stale[i] = 0xFF + } + if _, err := f.Write(stale); err != nil { + t.Fatal(err) + } + + if _, _, err := copyZstdSparse(f, bytes.NewReader(want)); err != nil { + t.Fatalf("copyZstdSparse: %v", err) + } + + got, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("stale data not cleared / wrong size: len(got)=%d len(want)=%d", len(got), len(want)) + } +} diff --git a/cmd/atelet/internal/ategcs/sparsezstd.go b/cmd/atelet/internal/ategcs/sparsezstd.go new file mode 100644 index 000000000..ad348b7e5 --- /dev/null +++ b/cmd/atelet/internal/ategcs/sparsezstd.go @@ -0,0 +1,196 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ategcs + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "os" + "runtime" + + "github.com/klauspost/compress/zstd" + "golang.org/x/sys/unix" +) + +// sparseMagic marks the sparse-extent snapshot format (see writeSparseZstd). It is +// 8 bytes and deliberately NOT a valid zstd frame magic, so a reader can tell this +// format from a plain zstd stream (no magic) by the first 8 bytes. The magic is +// version-neutral; the format version follows it (see sparseVersion). +const sparseMagic = "ATESPRSE" + +// sparseVersion is the sparse-extent format version, written as a little-endian +// uint32 immediately after sparseMagic (in the clear, before the zstd stream). Bump +// it on any incompatible layout change so readers reject snapshots they don't +// understand instead of misparsing them. +const sparseVersion uint32 = 2 + +// sparseEndOffset is the end-of-stream sentinel: an extent-frame offset of -1 marks +// the end of the frames (a real extent offset is always >= 0). Using an end marker +// keeps the format streamable — the writer need not know the extent count up front +// and the reader stops when it sees the sentinel. +const sparseEndOffset int64 = -1 + +// writeSparseZstd encodes a sparse file src to dst in the sparse-extent format: +// +// magic[8] | version:u32 | zstd( totalSize:i64 | (off:i64, len:i64, data[len])* | -1:i64 ) +// +// The magic + version are in the clear so a reader can dispatch on them; everything +// after is a single zstd stream of the metadata interleaved with ONLY the populated +// extents' data (holes are neither read nor compressed), terminated by the +// end-offset sentinel. The extents are discovered and emitted incrementally +// (SEEK_DATA/SEEK_HOLE), so the format is streamable — no extent count is written +// up front. +// +// This is the upload mirror of the sparse DOWNLOAD: a guest memory-ranges image is +// mostly holes (free RAM), so feeding only the real extents to zstd cuts the +// compress from "scan the whole logical image" (e.g. 2GiB) to "scan the resident +// set" (e.g. ~150MiB). Returns the logical size and the populated (pre-compression) +// byte count. All integers are little-endian. +func writeSparseZstd(dst io.Writer, src *os.File) (logical, dataBytes int64, err error) { + fi, err := src.Stat() + if err != nil { + return 0, 0, err + } + size := fi.Size() + + // magic + version in the clear (buffered: a couple of tiny writes). + bw := bufio.NewWriter(dst) + if _, err := bw.WriteString(sparseMagic); err != nil { + return 0, 0, err + } + if err := binary.Write(bw, binary.LittleEndian, sparseVersion); err != nil { + return 0, 0, err + } + if err := bw.Flush(); err != nil { + return 0, 0, err + } + + zw, err := zstd.NewWriter(dst, + zstd.WithEncoderLevel(zstd.SpeedFastest), + zstd.WithEncoderConcurrency(runtime.GOMAXPROCS(0))) + if err != nil { + return 0, 0, err + } + // fail closes the encoder before returning err (Close flushes/frees state). + fail := func(e error) (int64, int64, error) { + zw.Close() + return 0, 0, e + } + if err := binary.Write(zw, binary.LittleEndian, size); err != nil { + return fail(err) + } + + fd := int(src.Fd()) + off := int64(0) + for off < size { + ds, serr := unix.Seek(fd, off, unix.SEEK_DATA) + if serr != nil { + if serr == unix.ENXIO { // no more data: the rest is a hole + break + } + return fail(fmt.Errorf("SEEK_DATA: %w", serr)) + } + de, serr := unix.Seek(fd, ds, unix.SEEK_HOLE) + if serr != nil { + return fail(fmt.Errorf("SEEK_HOLE: %w", serr)) + } + length := de - ds + if err := binary.Write(zw, binary.LittleEndian, ds); err != nil { + return fail(err) + } + if err := binary.Write(zw, binary.LittleEndian, length); err != nil { + return fail(err) + } + if _, err := src.Seek(ds, io.SeekStart); err != nil { + return fail(err) + } + n, cerr := io.CopyN(zw, src, length) + dataBytes += n + if cerr != nil { + return fail(fmt.Errorf("reading extent @%d+%d: %w", ds, length, cerr)) + } + off = de + } + if err := binary.Write(zw, binary.LittleEndian, sparseEndOffset); err != nil { + return fail(err) + } + if err := zw.Close(); err != nil { + return 0, 0, err + } + return size, dataBytes, nil +} + +// readSparseZstd decodes the sparse-extent format into dst, which becomes a sparse +// file (the holes between extents are never written). src must be positioned just +// AFTER the magic (the caller reads + dispatches on it). dst is truncated to the +// logical size so trailing holes + the exact size are represented. +func readSparseZstd(dst *os.File, src io.Reader) (logical int64, err error) { + var ver uint32 + if err := binary.Read(src, binary.LittleEndian, &ver); err != nil { + return 0, fmt.Errorf("reading sparse format version: %w", err) + } + if ver != sparseVersion { + return 0, fmt.Errorf("unsupported sparse snapshot format version %d (this build supports %d)", ver, sparseVersion) + } + + zr, err := zstd.NewReader(src, zstd.WithDecoderConcurrency(1)) + if err != nil { + return 0, err + } + defer zr.Close() + + var size int64 + if err := binary.Read(zr, binary.LittleEndian, &size); err != nil { + return 0, fmt.Errorf("reading totalSize: %w", err) + } + if size < 0 { + return 0, fmt.Errorf("negative totalSize %d", size) + } + if err := dst.Truncate(size); err != nil { + return 0, err + } + + // Replay the extent frames written by writeSparseZstd. Each frame is an offset + // (i64), a length (i64), then that many data bytes; the stream ends with a + // terminator frame whose offset is sparseEndOffset (-1) and carries no len/data. + for { + var off int64 + if err := binary.Read(zr, binary.LittleEndian, &off); err != nil { + return 0, fmt.Errorf("reading extent offset: %w", err) + } + if off == sparseEndOffset { + break + } + var length int64 + if err := binary.Read(zr, binary.LittleEndian, &length); err != nil { + return 0, fmt.Errorf("reading extent length: %w", err) + } + // Validate against the declared size (the stream is the downloaded snapshot): + // an out-of-range extent would seek/write past the file or wrap on the + // off+length arithmetic. size-off is safe because off <= size. + if off < 0 || length < 0 || off > size || length > size-off { + return 0, fmt.Errorf("sparse extent out of range (off=%d len=%d size=%d)", off, length, size) + } + if _, err := dst.Seek(off, io.SeekStart); err != nil { + return 0, err + } + if _, err := io.CopyN(dst, zr, length); err != nil { + return 0, fmt.Errorf("writing extent @%d+%d: %w", off, length, err) + } + } + return size, nil +} diff --git a/cmd/atelet/internal/ategcs/sparsezstd_test.go b/cmd/atelet/internal/ategcs/sparsezstd_test.go new file mode 100644 index 000000000..37feac10f --- /dev/null +++ b/cmd/atelet/internal/ategcs/sparsezstd_test.go @@ -0,0 +1,232 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ategcs + +import ( + "bytes" + "encoding/binary" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/klauspost/compress/zstd" +) + +// region is a run of identical non-zero bytes written at off in a test source. +type region struct { + off int64 + len int64 + fill byte +} + +// writeSparseSource creates a file of logical size `size` with `regions` written +// in (the gaps stay zero, becoming holes on a sparse fs — but the round-trip is +// byte-exact regardless of whether the fs actually punches holes). +func writeSparseSource(t *testing.T, path string, size int64, regions []region) { + t.Helper() + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + for _, r := range regions { + buf := make([]byte, r.len) + for i := range buf { + buf[i] = r.fill + } + if _, err := f.WriteAt(buf, r.off); err != nil { + t.Fatal(err) + } + } + if err := f.Sync(); err != nil { + t.Fatal(err) + } +} + +// TestSparseZstdRoundTrip checks writeSparseZstd -> readSparseZstd is byte-exact +// across hole/data layouts (the correctness property is fs-independent: whether or +// not the fs makes real holes, the decoded file must equal the source). +func TestSparseZstdRoundTrip(t *testing.T) { + const M = 1 << 20 + cases := []struct { + name string + size int64 + regions []region + }{ + {"empty", 0, nil}, + {"all-hole", 4 * M, nil}, + {"all-data", 256 << 10, []region{{0, 256 << 10, 0xAB}}}, + {"leading-hole", 2 * M, []region{{1 * M, 64 << 10, 0x11}}}, + {"trailing-hole", 2 * M, []region{{0, 64 << 10, 0x22}}}, + {"single-extent-midfile", 4 * M, []region{{2 * M, 70000, 0x33}}}, + {"multi-extent", 8 * M, []region{ + {0, 4096, 0x44}, {2 * M, 100000, 0x55}, {5 * M, 8192, 0x66}, + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + srcPath := filepath.Join(dir, "src") + writeSparseSource(t, srcPath, tc.size, tc.regions) + src, err := os.Open(srcPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + var buf bytes.Buffer + logical, _, err := writeSparseZstd(&buf, src) + if err != nil { + t.Fatalf("writeSparseZstd: %v", err) + } + if logical != tc.size { + t.Errorf("writeSparseZstd logical=%d, want %d", logical, tc.size) + } + if got := buf.Bytes(); len(got) < len(sparseMagic) || string(got[:len(sparseMagic)]) != sparseMagic { + t.Fatal("output missing sparse magic") + } + + dstPath := filepath.Join(dir, "dst") + dst, err := os.Create(dstPath) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + size, err := readSparseZstd(dst, bytes.NewReader(buf.Bytes()[len(sparseMagic):])) + if err != nil { + t.Fatalf("readSparseZstd: %v", err) + } + if size != tc.size { + t.Errorf("readSparseZstd size=%d, want %d", size, tc.size) + } + + want, err := os.ReadFile(srcPath) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("round-trip mismatch (len got=%d want=%d)", len(got), len(want)) + } + }) + } +} + +// craftSparseStream builds a sparse-extent stream (after the magic) with arbitrary +// header values, so tests can feed malformed input to readSparseZstd. Each frame is +// (off, len) followed by len zero bytes; an end sentinel is appended when asked. +func craftSparseStream(t *testing.T, version uint32, size int64, frames [][2]int64, sentinel bool) []byte { + t.Helper() + var buf bytes.Buffer + if err := binary.Write(&buf, binary.LittleEndian, version); err != nil { + t.Fatal(err) + } + zw, err := zstd.NewWriter(&buf) + if err != nil { + t.Fatal(err) + } + mustWrite := func(v int64) { + if err := binary.Write(zw, binary.LittleEndian, v); err != nil { + zw.Close() + t.Fatal(err) + } + } + mustWrite(size) + for _, f := range frames { + mustWrite(f[0]) + mustWrite(f[1]) + if f[1] > 0 { + if _, err := zw.Write(make([]byte, f[1])); err != nil { + zw.Close() + t.Fatal(err) + } + } + } + if sentinel { + mustWrite(sparseEndOffset) + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func TestReadSparseZstdRejectsMalformed(t *testing.T) { + cases := []struct { + name string + stream []byte + want string + }{ + { + name: "wrong version", + stream: craftSparseStream(t, sparseVersion+1, 0, nil, true), + want: "unsupported sparse snapshot format version", + }, + { + name: "negative size", + stream: craftSparseStream(t, sparseVersion, -1, nil, true), + want: "negative totalSize", + }, + { + name: "extent past end", + stream: craftSparseStream(t, sparseVersion, 100, [][2]int64{{50, 100}}, true), + want: "out of range", + }, + { + name: "negative extent offset (not sentinel)", + stream: craftSparseStream(t, sparseVersion, 100, [][2]int64{{-5, 1}}, true), + want: "out of range", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dst, err := os.Create(filepath.Join(t.TempDir(), "dst")) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + _, err = readSparseZstd(dst, bytes.NewReader(tc.stream)) + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.want) + } + if !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %q, want substring %q", err.Error(), tc.want) + } + }) + } +} + +// TestReadSparseZstdTruncated confirms a stream cut short (no end sentinel / partial +// extent) is reported as an error rather than silently producing a short file. +func TestReadSparseZstdTruncated(t *testing.T) { + // A valid stream with one extent but NO end sentinel: the reader should hit EOF + // looking for the next frame offset. + stream := craftSparseStream(t, sparseVersion, 1<<20, [][2]int64{{0, 4096}}, false) + dst, err := os.Create(filepath.Join(t.TempDir(), "dst")) + if err != nil { + t.Fatal(err) + } + defer dst.Close() + if _, err := readSparseZstd(dst, bytes.NewReader(stream)); err == nil { + t.Fatal("expected an error for a stream missing its end sentinel, got nil") + } +} diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 7094f7315..50802d7d7 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -26,6 +26,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "cloud.google.com/go/storage" "github.com/agent-substrate/substrate/cmd/atelet/internal/ategcs" @@ -221,7 +222,7 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (*atele if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } - runscPath, err := s.ensureSandboxBinary(ctx, sandboxRec) + assetPaths, err := s.ensureSandboxAssets(ctx, sandboxRec) if err != nil { return nil, err } @@ -248,13 +249,14 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (*atele return nil, err } - // Tell ateom to do runsc create + runsc start for pause container and - // all application containers. + // Tell ateom to start the workload. gVisor uses RunscPath; the micro-VM + // runtime uses the full RuntimeAssetPaths set. if _, err := client.RunWorkload(ctx, &ateompb.RunWorkloadRequest{ ActorTemplateNamespace: ns, ActorTemplateName: tmpl, ActorId: actorID, - RunscPath: runscPath, + RunscPath: runscPathFor(assetPaths), + RuntimeAssetPaths: assetPaths, Spec: buildAteomWorkloadSpec(req.GetSpec()), }); err != nil { return nil, fmt.Errorf("while calling ateom.RunWorkload: %w", err) @@ -314,7 +316,7 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe if err != nil { return nil, fmt.Errorf("while loading recorded sandbox assets: %w", err) } - runscPath, err := s.ensureSandboxBinary(ctx, sandboxRec) + assetPaths, err := s.ensureSandboxAssets(ctx, sandboxRec) if err != nil { return nil, err } @@ -326,16 +328,24 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe return nil, err } - // Tell ateom to take checkpoint and delete containers. - if _, err := client.CheckpointWorkload(ctx, &ateompb.CheckpointWorkloadRequest{ + // Tell ateom to take the checkpoint and delete containers. ateom reports the + // exact files it wrote so we ship precisely that set (gVisor's image files, + // cloud-hypervisor's snapshot set, ...) rather than a hardcoded list. + resp, err := client.CheckpointWorkload(ctx, &ateompb.CheckpointWorkloadRequest{ ActorTemplateNamespace: ns, ActorTemplateName: tmpl, ActorId: actorID, - RunscPath: runscPath, + RunscPath: runscPathFor(assetPaths), + RuntimeAssetPaths: assetPaths, Spec: buildAteomWorkloadSpec(req.GetSpec()), - }); err != nil { + }) + if err != nil { return nil, fmt.Errorf("while calling ateom.CheckpointWorkload: %w", err) } + sandboxRec.SnapshotFiles = resp.GetSnapshotFiles() + if len(sandboxRec.SnapshotFiles) == 0 { + return nil, fmt.Errorf("ateom reported no snapshot files for checkpoint") + } switch req.GetType() { case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: @@ -365,7 +375,8 @@ func (s *AteomHerder) moveLocalCheckpoint(ctx context.Context, req *ateletpb.Che ns, tmpl := req.GetActorTemplateNamespace(), req.GetActorTemplateName() - for _, fileName := range []string{"checkpoint.img", "pages.img", "pages_meta.img"} { + // Move exactly the files ateom reported. + for _, fileName := range rec.SnapshotFiles { src := filepath.Join(checkpointDir, fileName) dst := filepath.Join(localCheckpointPath, fileName) recordSnapshotSize(ctx, strings.TrimSuffix(fileName, ".img"), src, ns, tmpl) @@ -375,8 +386,8 @@ func (s *AteomHerder) moveLocalCheckpoint(ctx context.Context, req *ateletpb.Che } } - // Pin the sandbox binaries into a manifest beside the images so a later - // Restore is self-describing. + // Pin the sandbox binaries + snapshot file list into a manifest beside the + // images so a later Restore is self-describing. manifest, err := json.Marshal(rec) if err != nil { return fmt.Errorf("while marshaling snapshot manifest: %w", err) @@ -392,37 +403,25 @@ func (s *AteomHerder) uploadExternalCheckpoint(ctx context.Context, req *ateletp ns, tmpl := req.GetActorTemplateNamespace(), req.GetActorTemplateName() prefix := strings.TrimSuffix(req.GetExternalConfig().GetSnapshotUriPrefix(), "/") - checkpointImgPath := filepath.Join(checkpointDir, "checkpoint.img") - pagesImgPath := filepath.Join(checkpointDir, "pages.img") - pagesMetaImgPath := filepath.Join(checkpointDir, "pages_meta.img") - - recordSnapshotSize(ctx, "checkpoint", checkpointImgPath, ns, tmpl) - - // Upload checkpoint from local dir. - if err := ategcs.SendLocalFileToGCSWithZstd(ctx, s.gcsClient, - prefix+"/checkpoint.img.zstd", - checkpointImgPath, - ); err != nil { - return fmt.Errorf("while uploading checkpoint.img to GCS: %w", err) - } - - recordSnapshotSize(ctx, "pages", pagesImgPath, ns, tmpl) - if err := uploadIfExists(ctx, s.gcsClient, - prefix+"/pages.img.zstd", - pagesImgPath, - ); err != nil { - return err + // Upload exactly the files ateom reported (each zstd-compressed). + g, gCtx := errgroup.WithContext(ctx) + for _, fileName := range rec.SnapshotFiles { + fileName := fileName + local := filepath.Join(checkpointDir, fileName) + recordSnapshotSize(ctx, strings.TrimSuffix(fileName, ".img"), local, ns, tmpl) + g.Go(func() error { + if err := ategcs.SendLocalFileToGCSWithZstd(gCtx, s.gcsClient, prefix+"/"+fileName+".zstd", local); err != nil { + return fmt.Errorf("while uploading %s to GCS: %w", fileName, err) + } + return nil + }) } - recordSnapshotSize(ctx, "pages_meta", pagesMetaImgPath, ns, tmpl) - if err := uploadIfExists(ctx, s.gcsClient, - prefix+"/pages_meta.img.zstd", - pagesMetaImgPath, - ); err != nil { + if err := g.Wait(); err != nil { return err } - // Pin the sandbox binaries into a manifest beside the images so a Restore on - // any node is self-describing. + // Pin the sandbox binaries + snapshot file list into a manifest beside the + // images, written last, so a Restore on any node is self-describing. manifest, err := json.Marshal(rec) if err != nil { return fmt.Errorf("while marshaling snapshot manifest: %w", err) @@ -446,9 +445,18 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) checkpointDir := ateompath.RestoreStateDir(ns, tmpl, actorID) + // Per-step timing so we can attribute resume latency between the rustfs + // download/decompress, the OCI image unpack, and ateom's own work. Logged at the end. + tStart := time.Now() + var dDownload, dBundles, dAteom time.Duration + // The snapshot is self-describing: recover the sandbox binaries that created // it from the manifest stored beside the checkpoint images (the Restore // request no longer carries the sandbox config). + // The snapshot is self-describing: recover the sandbox binaries that created + // it from the manifest stored beside the checkpoint images (the Restore + // request no longer carries the sandbox config). Fetch the (small) manifest + // first — both the checkpoint download and the OCI/asset prep below need it. var sandboxRec *sandboxAssetsRecord switch req.GetType() { case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: @@ -457,40 +465,60 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) if err != nil { return nil, fmt.Errorf("while fetching snapshot manifest: %w", err) } - sandboxRec, err = unmarshalSandboxRecord(manifest) - if err != nil { - return nil, err - } - if err := s.downloadExternalCheckpoint(ctx, prefix, checkpointDir); err != nil { + if sandboxRec, err = unmarshalSandboxRecord(manifest); err != nil { return nil, err } case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - // TODO(dberkov): the old pause checkpoint files are not deleted after they are copied to checkpointDir. This needs to be fixed in following PR. localCheckpointDir := ateompath.LocalCheckpointsDir(ns, tmpl, actorID) snapshotPrefix := req.GetLocalConfig().GetSnapshotPrefix() manifest, err := os.ReadFile(filepath.Join(localCheckpointDir, snapshotPrefix, sandboxManifestName)) if err != nil { return nil, fmt.Errorf("while reading local snapshot manifest: %w", err) } - sandboxRec, err = unmarshalSandboxRecord(manifest) - if err != nil { - return nil, err - } - if err := s.copyLocalCheckpoint(ctx, snapshotPrefix, localCheckpointDir, checkpointDir); err != nil { + if sandboxRec, err = unmarshalSandboxRecord(manifest); err != nil { return nil, err } default: return nil, fmt.Errorf("unexpected checkpoint type: %v", req.GetType()) } - runscPath, err := s.ensureSandboxBinary(ctx, sandboxRec) - if err != nil { - return nil, err - } - - if err := s.prepareOCIBundles(ctx, ns, tmpl, actorID, - req.GetSpec(), req.GetTargetAteomUid(), - ); err != nil { + // Download the memory snapshot and prepare the sandbox assets + OCI bundle + // CONCURRENTLY. They are independent — only the final ateom.RestoreWorkload + // needs both — so overlapping the GCS download (~0.5s warm) with the asset + // fetch + image unpack hides whichever leg is shorter, and on a cold node + // (uncached assets + image, ~2.5s unpack) that overlap is large. + // TODO(dberkov): the old pause checkpoint files are not deleted after they are + // copied to checkpointDir for the LOCAL case. + var assetPaths map[string]string + g, gctx := errgroup.WithContext(ctx) + g.Go(func() error { + t := time.Now() + switch req.GetType() { + case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: + if err := s.downloadExternalCheckpoint(gctx, req.GetExternalConfig().GetSnapshotUriPrefix(), checkpointDir, sandboxRec.SnapshotFiles); err != nil { + return err + } + case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: + if err := s.copyLocalCheckpoint(gctx, req.GetLocalConfig().GetSnapshotPrefix(), ateompath.LocalCheckpointsDir(ns, tmpl, actorID), checkpointDir, sandboxRec.SnapshotFiles); err != nil { + return err + } + } + dDownload = time.Since(t) + return nil + }) + g.Go(func() error { + var err error + if assetPaths, err = s.ensureSandboxAssets(gctx, sandboxRec); err != nil { + return err + } + t := time.Now() + if err := s.prepareOCIBundles(gctx, ns, tmpl, actorID, req.GetSpec(), req.GetTargetAteomUid()); err != nil { + return err + } + dBundles = time.Since(t) + return nil + }) + if err := g.Wait(); err != nil { return nil, err } @@ -501,15 +529,18 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) // Tell ateom to do runsc create + runsc restore for pause container and // all application containers. + tAteom := time.Now() if _, err := client.RestoreWorkload(ctx, &ateompb.RestoreWorkloadRequest{ ActorTemplateNamespace: ns, ActorTemplateName: tmpl, ActorId: actorID, - RunscPath: runscPath, + RunscPath: runscPathFor(assetPaths), + RuntimeAssetPaths: assetPaths, Spec: buildAteomWorkloadSpec(req.GetSpec()), }); err != nil { return nil, fmt.Errorf("while calling ateom.RestoreWorkload: %w", err) } + dAteom = time.Since(tAteom) // Record the (manifest-pinned) sandbox binaries on-node so a subsequent // Checkpoint of this restored actor can re-pin the same version. @@ -517,11 +548,16 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) return nil, fmt.Errorf("while recording sandbox assets: %w", err) } + slog.InfoContext(ctx, "Restore timing breakdown", slog.String("actor", actorID), + slog.Duration("download", dDownload), // rustfs/GCS fetch + decompress (or local copy) + slog.Duration("oci_unpack", dBundles), // prepareOCIBundles: unpack the OCI image to the bundle + slog.Duration("ateom_restore", dAteom), // ateom.RestoreWorkload (see its own breakdown) + slog.Duration("total", time.Since(tStart))) return &ateletpb.RestoreResponse{}, nil } -func (s *AteomHerder) copyLocalCheckpoint(ctx context.Context, snapshotPrefix string, srcDir, dstDir string) error { - for _, fileName := range []string{"checkpoint.img", "pages.img", "pages_meta.img"} { +func (s *AteomHerder) copyLocalCheckpoint(ctx context.Context, snapshotPrefix string, srcDir, dstDir string, files []string) error { + for _, fileName := range files { if ctx.Err() != nil { return fmt.Errorf("context cancelled: %w", ctx.Err()) } @@ -560,22 +596,15 @@ func copyFile(src, dst string) (int64, error) { return nBytes, err } -func (s *AteomHerder) downloadExternalCheckpoint(ctx context.Context, snapshotUriPrefix string, dstDir string) error { - checkpointImgPath := filepath.Join(dstDir, "checkpoint.img") - pagesImgPath := filepath.Join(dstDir, "pages.img") - pagesMetaImgPath := filepath.Join(dstDir, "pages_meta.img") - +func (s *AteomHerder) downloadExternalCheckpoint(ctx context.Context, snapshotUriPrefix string, dstDir string, files []string) error { prefix := strings.TrimSuffix(snapshotUriPrefix, "/") g, gCtx := errgroup.WithContext(ctx) - for _, dl := range []struct{ remote, local string }{ - {prefix + "/checkpoint.img.zstd", checkpointImgPath}, - {prefix + "/pages.img.zstd", pagesImgPath}, - {prefix + "/pages_meta.img.zstd", pagesMetaImgPath}, - } { - dl := dl + for _, fileName := range files { + fileName := fileName + local := filepath.Join(dstDir, fileName) g.Go(func() error { - if err := ategcs.FetchLocalFileFromGCSWithZstd(gCtx, s.gcsClient, dl.remote, dl.local); err != nil { - return fmt.Errorf("while downloading %s from GCS: %w", filepath.Base(dl.remote), err) + if err := ategcs.FetchLocalFileFromGCSWithZstd(gCtx, s.gcsClient, prefix+"/"+fileName+".zstd", local); err != nil { + return fmt.Errorf("while downloading %s from GCS: %w", fileName, err) } return nil }) @@ -685,19 +714,6 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) *ateompb.WorkloadSpec { return out } -// uploadIfExists uploads a local file to GCS (zstd-compressed) only if -// the file is present. Missing files are silently skipped — used for -// optional checkpoint side-files (pages.img, pages_meta.img). -func uploadIfExists(ctx context.Context, gcs ategcs.ObjectStorage, remoteURI, localPath string) error { - if _, err := os.Stat(localPath); err != nil { - return nil - } - if err := ategcs.SendLocalFileToGCSWithZstd(ctx, gcs, remoteURI, localPath); err != nil { - return fmt.Errorf("while uploading %s to GCS: %w", filepath.Base(localPath), err) - } - return nil -} - type AteomDialer struct { conns *lru.Cache } diff --git a/cmd/atelet/sandbox_assets.go b/cmd/atelet/sandbox_assets.go index 876a4b9f0..4ec0b54f7 100644 --- a/cmd/atelet/sandbox_assets.go +++ b/cmd/atelet/sandbox_assets.go @@ -59,6 +59,12 @@ type assetEntry struct { type sandboxAssetsRecord struct { SandboxClass string `json:"sandboxClass"` Assets map[string]assetEntry `json:"assets"` + // SnapshotFiles are the (relative) names of the files ateom wrote into the + // checkpoint directory, as reported by CheckpointWorkloadResponse. Recorded + // in the snapshot manifest so Restore ships/downloads exactly this set + // (gVisor's image files, cloud-hypervisor's snapshot set, ...). Empty in the + // on-node record written at Run/Restore; populated at Checkpoint. + SnapshotFiles []string `json:"snapshotFiles,omitempty"` } // recordFromRequest projects a request's per-architecture SandboxAssets onto the @@ -82,22 +88,29 @@ func recordFromRequest(sa *ateletpb.SandboxAssets) (*sandboxAssetsRecord, error) return rec, nil } -// ensureSandboxBinary fetches the sandbox binary an actor needs and returns its -// local path. For gVisor this is the single "runsc" asset, passed to ateom as -// RunscPath. Binaries are content-addressed and cached, so re-fetching at -// Checkpoint/Restore is a no-op once present. -func (s *AteomHerder) ensureSandboxBinary(ctx context.Context, rec *sandboxAssetsRecord) (string, error) { +// ensureSandboxAssets fetches every asset in the record content-addressed and +// returns a map of asset name to local path. gVisor has a single "runsc" asset; +// the micro-VM runtime has several (kata-shim, cloud-hypervisor, ...). Assets are +// cached, so re-fetching at Checkpoint/Restore is a no-op once present. +func (s *AteomHerder) ensureSandboxAssets(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { if err := os.MkdirAll(ateompath.StaticFilesDir, 0o700); err != nil { - return "", fmt.Errorf("while creating static files dir: %w", err) + return nil, fmt.Errorf("while creating static files dir: %w", err) } - // gVisor uses a single "runsc" asset. - entry, ok := rec.Assets["runsc"] - if !ok { - return "", status.Errorf(codes.InvalidArgument, "sandbox assets for class %q missing required %q file", rec.SandboxClass, "runsc") + paths := make(map[string]string, len(rec.Assets)) + for name, entry := range rec.Assets { + p, err := s.fetchAsset(ctx, entry) + if err != nil { + return nil, fmt.Errorf("while fetching sandbox asset %q: %w", name, err) + } + paths[name] = p } - return s.fetchAsset(ctx, entry) + return paths, nil } +// runscPathFor returns the local path of the gVisor "runsc" asset from a fetched +// asset-path map, or "" if the runtime has none (e.g. micro-VM). +func runscPathFor(paths map[string]string) string { return paths["runsc"] } + // fetchAsset downloads one content-addressed asset (verifying its sha256) into // the shared static-files cache and returns its local path. On a cache hit it // returns immediately. @@ -114,10 +127,14 @@ func (s *AteomHerder) fetchAsset(ctx context.Context, entry assetEntry) (string, return "", fmt.Errorf("while stat-ing local file: %w", err) } - // gVisor's runsc lives in the public gs://gvisor bucket, so the anonymous - // client suffices. TODO: drive authenticated asset fetches from atelet - // configuration for assets in private buckets. - rc, err := ategcs.Open(ctx, s.anonGCSClient, entry.URL) + // Assets live in one of two places: public buckets (gVisor's runsc in + // gs://gvisor — read anonymously) or the cluster's own object store (micro-VM + // kata/CH assets staged into the snapshot bucket — read with the main client, + // which is rustfs/S3 in kind and authenticated GCS on GKE). Auth is an + // atelet-level decision, not per-asset: try the anonymous client first so the + // common public-gVisor path stays fast, then fall back to the main client. The + // asset is streamed (not buffered) to disk below. + rc, err := s.openAsset(ctx, entry.URL) if err != nil { return "", fmt.Errorf("while fetching %v: %w", entry.URL, err) } @@ -163,6 +180,26 @@ func (s *AteomHerder) fetchAsset(ctx context.Context, entry assetEntry) (string, return localPath, nil } +// openAsset streams url, trying the anonymous client first (public buckets like +// gs://gvisor) then the main object-storage client (the cluster's own bucket, e.g. +// micro-VM assets in rustfs/S3 or an authenticated GCS bucket). The caller closes +// the returned reader. Streaming (rather than buffering the whole asset) keeps a +// multi-hundred-MiB guest image off the heap. +func (s *AteomHerder) openAsset(ctx context.Context, url string) (io.ReadCloser, error) { + rc, anonErr := ategcs.Open(ctx, s.anonGCSClient, url) + if anonErr == nil { + return rc, nil + } + if s.gcsClient == nil { + return nil, anonErr + } + rc, mainErr := ategcs.Open(ctx, s.gcsClient, url) + if mainErr != nil { + return nil, fmt.Errorf("anonymous open failed (%v); main client open failed: %w", anonErr, mainErr) + } + return rc, nil +} + // writeSandboxRecord persists the actor's running sandbox assets on-node so a // later Checkpoint (whose request no longer carries the sandbox config) can // re-fetch the same binaries and pin them into the snapshot manifest. diff --git a/cmd/ateom-gvisor/main.go b/cmd/ateom-gvisor/main.go index 389db5b71..7e543c7ac 100644 --- a/cmd/ateom-gvisor/main.go +++ b/cmd/ateom-gvisor/main.go @@ -24,10 +24,11 @@ import ( "net" "os" "runtime" + "sort" "sync" "cloud.google.com/go/compute/metadata" - "github.com/agent-substrate/substrate/cmd/ateom-gvisor/internal/ateom" + "github.com/agent-substrate/substrate/internal/actorlog" "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/contextlogging" @@ -86,7 +87,7 @@ func do(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() - syncedWriter := ateom.NewSyncedWriter(os.Stdout) + syncedWriter := actorlog.NewSyncedWriter(os.Stdout) logger := slog.New(contextlogging.NewHandler(slog.NewJSONHandler(syncedWriter, nil))) slog.SetDefault(logger) @@ -134,7 +135,7 @@ func do(ctx context.Context) error { return fmt.Errorf("while creating ateom-interior netns: %w", err) } - actorLogger := ateom.NewActorLogger(syncedWriter, metadata.OnGCE()) + actorLogger := actorlog.NewActorLogger(syncedWriter, metadata.OnGCE()) ateomService := NewService(interiorNetNS, actorLogger) svr := grpc.NewServer( @@ -161,13 +162,13 @@ type AteomService struct { lock sync.Mutex interiorNetNS netns.NsHandle - actorLogger *ateom.ActorLogger + actorLogger *actorlog.ActorLogger } var _ ateompb.AteomServer = (*AteomService)(nil) // NewService creates a new AteomService. -func NewService(interiorNetNS netns.NsHandle, actorLogger *ateom.ActorLogger) *AteomService { +func NewService(interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger) *AteomService { svc := &AteomService{ interiorNetNS: interiorNetNS, actorLogger: actorLogger, @@ -273,9 +274,33 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec s.cleanupActorNetworkOrExit(ctx, "Failed to clean up actor network after checkpoint") + // Report exactly the files runsc wrote so atelet ships precisely this set + // (checkpoint.img plus any pages images), rather than a hardcoded list. + snapshotFiles, err := listSnapshotFiles(checkpointPath) + if err != nil { + return nil, fmt.Errorf("while listing checkpoint files: %w", err) + } + s.actorLogger.EmitLifecycleLog("Actor checkpointed", req.GetActorId(), req.GetActorTemplateName(), req.GetActorTemplateNamespace()) - return nil, nil + return &ateompb.CheckpointWorkloadResponse{SnapshotFiles: snapshotFiles}, nil +} + +// listSnapshotFiles returns the (relative) names of regular files directly under +// dir, which atelet ships to object storage as the snapshot. +func listSnapshotFiles(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + var files []string + for _, e := range entries { + if e.Type().IsRegular() { + files = append(files, e.Name()) + } + } + sort.Strings(files) + return files, nil } func (r *runsc) cleanupContainersAfterCheckpoint(ctx context.Context, containers []*ateompb.Container) error { diff --git a/cmd/ateom-microvm/checkpoint.go b/cmd/ateom-microvm/checkpoint.go new file mode 100644 index 000000000..7461b73d3 --- /dev/null +++ b/cmd/ateom-microvm/checkpoint.go @@ -0,0 +1,215 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "time" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" +) + +// CheckpointWorkload suspends the actor and writes a portable CH snapshot. +// +// Contract with atelet (mirrors ateom-gvisor): after we return, atelet uploads +// the checkpoint dir to object storage, then tears down bundles and resets the +// actor dir. +// +// ateom drives the ateom-owned CH's REST api-socket: pause -> snapshot +// file:// (config.json + state.json + sparse memory-ranges) -> +// tear the VMM down. The actor's rootfs lives on the host-backed /dev/vdb, not a +// guest tmpfs overlay-upper, so the snapshot is naturally memory-only and small — +// no RAM-backed upper to wipe and no balloon to inflate before snapshot. +func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.CheckpointWorkloadRequest) (*ateompb.CheckpointWorkloadResponse, error) { + s.lock.Lock() + defer s.lock.Unlock() + + ns := req.GetActorTemplateNamespace() + name := req.GetActorTemplateName() + id := req.GetActorId() + + s.actorLogger.EmitLifecycleLog("Actor checkpointing", id, name, ns) + + // The actor's CH was booted by RunWorkload or relaunched by RestoreWorkload; + // either way ateom owns it and tracks its api-socket. + ra := s.running[id] + chSocket := kata.CLHSocketPath(id) + if ra != nil && ra.apiSocket != "" { + chSocket = ra.apiSocket + } + client := ch.NewClient(chSocket) + if err := client.WaitReady(ctx, 10*time.Second); err != nil { + return nil, fmt.Errorf("while waiting for CH api-socket: %w", err) + } + + tPause := time.Now() + if err := client.Pause(ctx); err != nil { + return nil, fmt.Errorf("while pausing guest: %w", err) + } + dPause := time.Since(tPause) + + checkpointDir := ateompath.CheckpointStateDir(ns, name, id) + // Start from a clean dir so CH's snapshot files are the only contents. + if err := os.RemoveAll(checkpointDir); err != nil { + return nil, fmt.Errorf("while clearing checkpoint dir %q: %w", checkpointDir, err) + } + if err := os.MkdirAll(checkpointDir, 0o700); err != nil { + return nil, fmt.Errorf("while creating checkpoint dir %q: %w", checkpointDir, err) + } + + // Record the FROZEN base id (the id the guest's virtio-fs find-paths are pinned + // to, /rootfs). For a cold (owned-boot) actor this is its own id; for a + // restored actor it is the golden id propagated via ra.baseID (set from the + // snapshot we restored from). RestoreWorkload reads this to lay the + // reconstructed-from-image base at the path the guest expects. We can NOT derive + // it from config.json (its socket paths get rewritten to the current id on every + // restore, losing the invariant golden id). + baseID := id + if ra != nil && ra.baseID != "" { + baseID = ra.baseID + } + if err := os.WriteFile(filepath.Join(checkpointDir, baseIDFile), []byte(baseID), 0o600); err != nil { + return nil, fmt.Errorf("while writing %s: %w", baseIDFile, err) + } + + slog.InfoContext(ctx, "Snapshotting guest", slog.String("id", id), slog.String("dir", checkpointDir)) + tSnapshot := time.Now() + if err := client.Snapshot(ctx, checkpointDir); err != nil { + return nil, fmt.Errorf("while snapshotting guest: %w", err) + } + dSnapshot := time.Since(tSnapshot) + + // Diff-snapshot completion for an OnDemand-restored actor: CH's snapshot here is + // sparse — only the pages faulted in since the OnDemand restore — so on its own + // it's INCOMPLETE (the un-faulted pages were being demand-paged from the restore + // source). Overlay it onto that source to rebuild a COMPLETE memory-ranges, so the + // snapshot is self-contained and re-restorable. (A cold-run actor has no restore + // source and its snapshot is already complete — no merge.) + if ra != nil && ra.restoreSourceDir != "" { + base := filepath.Join(ra.restoreSourceDir, "memory-ranges") + delta := filepath.Join(checkpointDir, "memory-ranges") + tMerge := time.Now() + // Reuse base's on-disk working set (rename + overlay) instead of copying it — + // CH is paused and about to be torn down, and base is discarded after. See + // MergeDeltaIntoBase. (Falls back to the copying merge across filesystems.) + if err := ch.MergeDeltaIntoBase(ctx, base, delta); err != nil { + return nil, fmt.Errorf("while merging OnDemand delta into restore source: %w", err) + } + slog.InfoContext(ctx, "Merged OnDemand delta into base (complete snapshot)", + slog.String("id", id), slog.Duration("merge", time.Since(tMerge))) + } + + // reset-to-golden support: save the actor's /dev/vdb AS-OF this (paused, + // consistent) snapshot as a verbatim golden template, so future restores can + // recreate the disk byte-identical to what the snapshot's guest RAM expects + // while discarding the actor's later rootfs writes. Saved once (the first/golden + // checkpoint) and kept; best-effort (without it, restore reopens the live disk = + // continuity). TODO: ship the template with the snapshot for cross-node restore + // (it's golden, shipped once per template, like the OCI base). + actorDir := ateompath.ActorPath(ns, name, id) + if tmpl := filepath.Join(actorDir, goldenRootfsDiskName); fileMissing(tmpl) { + if cerr := copyDiskFile(ctx, filepath.Join(actorDir, actorRootfsDiskName), tmpl); cerr != nil { + slog.WarnContext(ctx, "Failed to save golden rootfs template; restore will reopen live disk", slog.Any("err", cerr)) + } else { + slog.InfoContext(ctx, "Saved golden rootfs disk template", slog.String("id", id)) + } + } + + // Report exactly the files we wrote so atelet ships precisely the CH snapshot + // (config.json + state.json + memory-ranges + base-id), not gVisor's fixed set. + // Memory-only: the RO base is reconstructed from the OCI image at restore. + snapshotFiles, err := listFiles(checkpointDir) + if err != nil { + return nil, fmt.Errorf("while listing snapshot files: %w", err) + } + + // Tear down: the actor returns to "available". Best-effort; the snapshot is + // already on disk for atelet to ship. + tTeardown := time.Now() + s.teardownActor(ctx, id, ra, client) + dTeardown := time.Since(tTeardown) + delete(s.running, id) + + // Tear down the per-activation actor network (mirrors gVisor). + if err := s.cleanupActorNetwork(ctx); err != nil { + slog.WarnContext(ctx, "Failed to clean up actor network after checkpoint", slog.Any("err", err)) + } + + s.actorLogger.EmitLifecycleLog("Actor checkpointed", id, name, ns) + slog.InfoContext(ctx, "Actor checkpointed", slog.String("id", id), slog.Any("snapshot_files", snapshotFiles), + slog.Duration("pause", dPause), + slog.Duration("snapshot", dSnapshot), slog.Duration("teardown", dTeardown)) + return &ateompb.CheckpointWorkloadResponse{SnapshotFiles: snapshotFiles}, nil +} + +// listFiles returns the (relative) names of regular files directly under dir. +func listFiles(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + var files []string + for _, e := range entries { + if e.Type().IsRegular() { + files = append(files, e.Name()) + } + } + return files, nil +} + +// teardownActor stops the ateom-owned CH VMM for an actor. Best-effort: the +// snapshot is already on disk, so this only needs to release resources. ra may be +// nil (e.g. ateom restarted and lost in-memory state). +func (s *AteomService) teardownActor(ctx context.Context, id string, ra *runningActor, client *ch.Client) { + if client != nil { + tShutdown := time.Now() + shutCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + if err := client.Shutdown(shutCtx); err != nil { + slog.WarnContext(ctx, "CH shutdown failed (continuing teardown)", slog.Any("err", err)) + } + cancel() + slog.InfoContext(ctx, "CH API shutdown done", slog.Duration("took", time.Since(tShutdown))) + } + + if ra != nil { + // Close the kata-agent client kept open for stdout/stderr forwarding. This + // fails the forwarding goroutines' in-flight ReadStdout/ReadStderr calls, so + // they return io.EOF and exit (no goroutine leak). Guarded so a second + // teardown / a never-forwarded actor is a no-op. + if ra.logAgent != nil { + _ = ra.logAgent.Close() + ra.logAgent = nil + } + + // Kill the CH process ateom launched. + if ra.chCmd != nil && ra.chCmd.Process != nil { + _ = ra.chCmd.Process.Kill() + _, _ = ra.chCmd.Process.Wait() + } + } + + // Sweep any leftover per-sandbox host-side state + orphaned per-sandbox + // processes. This is ateom's own cleanup (process kill + unmount + rm). + kata.CleanupSandboxState(ctx, id) +} diff --git a/cmd/ateom-microvm/internal/ch/api.go b/cmd/ateom-microvm/internal/ch/api.go new file mode 100644 index 000000000..97bf6832a --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/api.go @@ -0,0 +1,130 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" +) + +// apiBase is a placeholder host; the real transport always dials the unix +// api-socket, so the host portion of the URL is ignored. +const apiBase = "http://localhost" + +// apiClient speaks the cloud-hypervisor REST API over its unix api-socket. +// +// cloud-hypervisor serves an HTTP/1.1 REST API on the api-socket, and we drive snapshot/restore +// through it (vm.pause, vm.snapshot, vm.resume, vmm.ping, ...). +type apiClient struct { + http *http.Client +} + +func newAPIClient(socketPath string) *apiClient { + return &apiClient{ + http: &http.Client{ + Transport: &http.Transport{ + // CH's API server closes idle connections (and can get heavily + // swapped out during reclaim). Reusing a kept-alive connection + // then blocks forever on the next request (observed + // empirically: vm.resume hangs on a reused connection while + // a fresh one works instantly). Force a fresh connection per + // request. + DisableKeepAlives: true, + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + }, + } +} + +// get issues a GET and checks for a 2xx status. +func (c *apiClient) get(ctx context.Context, path string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiBase+path, nil) + if err != nil { + return err + } + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + _, _ = io.Copy(io.Discard, resp.Body) + if resp.StatusCode >= 300 { + return fmt.Errorf("GET %s: status %d", path, resp.StatusCode) + } + return nil +} + +// getJSON issues a GET and decodes the 2xx JSON response into out. +func (c *apiClient) getJSON(ctx context.Context, path string, out any) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiBase+path, nil) + if err != nil { + return err + } + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode >= 300 { + return fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, bytes.TrimSpace(b)) + } + return json.Unmarshal(b, out) +} + +// put issues a PUT with an optional JSON body and checks for a 2xx status. +func (c *apiClient) put(ctx context.Context, path string, body any) error { + var rdr io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return err + } + rdr = bytes.NewReader(b) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPut, apiBase+path, rdr) + if err != nil { + return err + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + msg, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 300 { + return fmt.Errorf("PUT %s: status %d: %s", path, resp.StatusCode, bytes.TrimSpace(msg)) + } + return nil +} + +// snapshotConfig is the body of /api/v1/vm.snapshot. +type snapshotConfig struct { + DestinationURL string `json:"destination_url"` +} diff --git a/cmd/ateom-microvm/internal/ch/ch.go b/cmd/ateom-microvm/internal/ch/ch.go new file mode 100644 index 000000000..4a011606a --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/ch.go @@ -0,0 +1,112 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ch drives a single cloud-hypervisor instance over its REST +// api-socket: pause, snapshot, resume against a running VMM (e.g. the socket +// kata creates at /run/vc/vm//clh-api.sock), plus relaunching a fresh VMM +// from a snapshot directory for restore. +// +// This is the snapshot/restore half of the ateom-microvm model: kata +// owns RUN (boot the micro-VM + run the OCI container), and ateom drives the CH +// REST API underneath for suspend (pause+snapshot) and owns the bare-CH +// relaunch for restore (see LaunchVMM + RestoreWithNetFDs in restorefds.go). The +// REST wire format is the one cloud-hypervisor documents for snapshot/restore. +package ch + +import ( + "context" + "fmt" + "os" + "time" +) + +// Client talks to one cloud-hypervisor VMM over its unix api-socket. +type Client struct { + apiSocket string + api *apiClient +} + +// NewClient returns a Client bound to a cloud-hypervisor api-socket path. The +// socket need not exist yet; use WaitReady to block until the VMM answers. +func NewClient(apiSocket string) *Client { + return &Client{apiSocket: apiSocket, api: newAPIClient(apiSocket)} +} + +// Ping returns nil if the VMM api-socket answers vmm.ping. +func (c *Client) Ping(ctx context.Context) error { + return c.api.get(ctx, "/api/v1/vmm.ping") +} + +// WaitReady blocks until the api-socket answers vmm.ping or the deadline passes. +func (c *Client) WaitReady(ctx context.Context, deadline time.Duration) error { + end := time.Now().Add(deadline) + for { + if err := c.Ping(ctx); err == nil { + return nil + } + if !time.Now().Before(end) { + return fmt.Errorf("cloud-hypervisor api socket %q not ready after %s", c.apiSocket, deadline) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(10 * time.Millisecond): + } + } +} + +// State returns the VM state as reported by vm.info (e.g. "Running", "Paused"). +func (c *Client) State(ctx context.Context) (string, error) { + var info struct { + State string `json:"state"` + } + if err := c.api.getJSON(ctx, "/api/v1/vm.info", &info); err != nil { + return "", err + } + return info.State, nil +} + +// Pause pauses the running guest (quiescing it before snapshot). Idempotent: +// already-paused is success (CH itself 500s on pausing a paused VM, which would +// otherwise wedge checkpoint retries after a partial earlier attempt). +func (c *Client) Pause(ctx context.Context) error { + if state, err := c.State(ctx); err == nil && state == "Paused" { + return nil + } + return c.api.put(ctx, "/api/v1/vm.pause", nil) +} + +// Resume resumes a paused guest (after snapshot or restore). +func (c *Client) Resume(ctx context.Context) error { + return c.api.put(ctx, "/api/v1/vm.resume", nil) +} + +// Snapshot writes the (paused) guest's state to destDir as a CH snapshot +// (config.json + state.json + memory-ranges). The guest must be paused first. +func (c *Client) Snapshot(ctx context.Context, destDir string) error { + if err := os.MkdirAll(destDir, 0o755); err != nil { + return fmt.Errorf("while creating snapshot dir %q: %w", destDir, err) + } + return c.api.put(ctx, "/api/v1/vm.snapshot", snapshotConfig{DestinationURL: SnapshotURL(destDir)}) +} + +// Shutdown best-effort tears down the VM and the VMM process behind the socket. +func (c *Client) Shutdown(ctx context.Context) error { + _ = c.api.put(ctx, "/api/v1/vm.shutdown", nil) + return c.api.put(ctx, "/api/v1/vmm.shutdown", nil) +} + +// SnapshotURL returns the file:// URL cloud-hypervisor expects for a snapshot +// destination or restore source directory. +func SnapshotURL(dir string) string { return "file://" + dir } diff --git a/cmd/ateom-microvm/internal/ch/ch_test.go b/cmd/ateom-microvm/internal/ch/ch_test.go new file mode 100644 index 000000000..4fbac88bd --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/ch_test.go @@ -0,0 +1,142 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestSnapshotURL(t *testing.T) { + if got, want := SnapshotURL("/var/lib/snap"), "file:///var/lib/snap"; got != want { + t.Errorf("SnapshotURL = %q, want %q", got, want) + } +} + +// fakeCH is a stand-in cloud-hypervisor REST server on a unix socket. It records +// the requests it receives so tests can assert on method/path/body. +type fakeCH struct { + mu sync.Mutex + requests []recordedReq + srv *http.Server +} + +type recordedReq struct { + method string + path string + body string +} + +func startFakeCH(t *testing.T) (*Client, *fakeCH) { + t.Helper() + sockPath := filepath.Join(t.TempDir(), "ch.sock") + lis, err := net.Listen("unix", sockPath) + if err != nil { + t.Fatalf("listen unix: %v", err) + } + + f := &fakeCH{} + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + f.mu.Lock() + f.requests = append(f.requests, recordedReq{method: r.Method, path: r.URL.Path, body: string(body)}) + f.mu.Unlock() + w.WriteHeader(http.StatusNoContent) + }) + f.srv = &http.Server{Handler: mux} + go f.srv.Serve(lis) + t.Cleanup(func() { _ = f.srv.Close() }) + + return NewClient(sockPath), f +} + +func (f *fakeCH) recorded() []recordedReq { + f.mu.Lock() + defer f.mu.Unlock() + out := make([]recordedReq, len(f.requests)) + copy(out, f.requests) + return out +} + +func TestClientLifecycleCalls(t *testing.T) { + client, fake := startFakeCH(t) + ctx := context.Background() + + if err := client.WaitReady(ctx, time.Second); err != nil { + t.Fatalf("WaitReady: %v", err) + } + if err := client.Pause(ctx); err != nil { + t.Fatalf("Pause: %v", err) + } + snapDir := filepath.Join(t.TempDir(), "snap") + if err := client.Snapshot(ctx, snapDir); err != nil { + t.Fatalf("Snapshot: %v", err) + } + if err := client.Resume(ctx); err != nil { + t.Fatalf("Resume: %v", err) + } + + reqs := fake.recorded() + // First request is the WaitReady ping; assert the meaningful ones after it. + var got []recordedReq + for _, r := range reqs { + if r.path == "/api/v1/vmm.ping" { + continue + } + got = append(got, r) + } + want := []recordedReq{ + // Pause checks vm.info first (idempotency); the fake's empty reply is an + // unparseable state, so Pause falls through to the actual vm.pause PUT. + {method: http.MethodGet, path: "/api/v1/vm.info", body: ""}, + {method: http.MethodPut, path: "/api/v1/vm.pause", body: ""}, + {method: http.MethodPut, path: "/api/v1/vm.snapshot"}, + {method: http.MethodPut, path: "/api/v1/vm.resume", body: ""}, + } + if len(got) != len(want) { + t.Fatalf("got %d non-ping requests %+v, want %d", len(got), got, len(want)) + } + for i := range want { + if got[i].method != want[i].method || got[i].path != want[i].path { + t.Errorf("request %d = %s %s, want %s %s", i, got[i].method, got[i].path, want[i].method, want[i].path) + } + } + + // The snapshot body must carry the file:// destination URL. + var snap snapshotConfig + if err := json.Unmarshal([]byte(got[2].body), &snap); err != nil { + t.Fatalf("snapshot body not JSON: %v (%q)", err, got[2].body) + } + if snap.DestinationURL != SnapshotURL(snapDir) { + t.Errorf("snapshot destination_url = %q, want %q", snap.DestinationURL, SnapshotURL(snapDir)) + } +} + +func TestWaitReadyTimesOut(t *testing.T) { + // Socket that never exists -> WaitReady should time out, not hang. + client := NewClient(filepath.Join(t.TempDir(), "nonexistent.sock")) + err := client.WaitReady(context.Background(), 50*time.Millisecond) + if err == nil { + t.Fatal("WaitReady returned nil for a dead socket, want timeout error") + } +} diff --git a/cmd/ateom-microvm/internal/ch/createvm.go b/cmd/ateom-microvm/internal/ch/createvm.go new file mode 100644 index 000000000..f24266653 --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/createvm.go @@ -0,0 +1,107 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "context" + "fmt" +) + +// VmConfig is the body of /api/v1/vm.create — the subset of cloud-hypervisor's +// VmConfig ateom sets to boot a kata guest itself (the "ateom owns the boot" +// path, replacing the kata shim). Modeled on kata's clh driver +// (src/runtime/virtcontainers/clh.go) and the proven suspend-bench vmConfig. +// vm.create + vm.boot use PUT (empirically accepted by CH, like the bench). +type VmConfig struct { + Cpus CpusConfig `json:"cpus"` + Memory MemoryConfig `json:"memory"` + Payload PayloadConfig `json:"payload"` + Disks []DiskConfig `json:"disks,omitempty"` + Rng *RngConfig `json:"rng,omitempty"` + Serial *ConsoleConfig `json:"serial,omitempty"` + Console *ConsoleConfig `json:"console,omitempty"` + Vsock *VsockConfig `json:"vsock,omitempty"` +} + +// CpusConfig sets the boot/max vCPU counts. +type CpusConfig struct { + BootVcpus int32 `json:"boot_vcpus"` + MaxVcpus int32 `json:"max_vcpus"` +} + +// MemoryConfig sets guest RAM. Shared=true makes CH back RAM with a memfd, which +// is what lets vm.snapshot write a SPARSE image (the memory-only snapshot the +// rest of ateom relies on). +type MemoryConfig struct { + Size int64 `json:"size"` + Shared bool `json:"shared"` +} + +// PayloadConfig points at the guest kernel + its cmdline (initramfs/firmware +// unused: the kata guest boots from a virtio-blk image disk, root=/dev/vda1). +type PayloadConfig struct { + Kernel string `json:"kernel"` + Cmdline string `json:"cmdline"` +} + +// DiskConfig is one virtio-blk disk. The kata guest image is disk 0 (/dev/vda, +// readonly); ateom appends the actor rootfs as disk 1 (/dev/vdb, writable). The +// guest sees disks in config order. NumQueues/QueueSize mirror kata's clh +// (num_queues = vcpus, queue_size = 1024). +type DiskConfig struct { + Path string `json:"path"` + Readonly bool `json:"readonly"` + Direct bool `json:"direct"` + NumQueues int32 `json:"num_queues,omitempty"` + QueueSize int32 `json:"queue_size,omitempty"` + ImageType string `json:"image_type,omitempty"` +} + +// RngConfig sets the entropy source (kata uses /dev/urandom). +type RngConfig struct { + Src string `json:"src"` +} + +// ConsoleConfig is a serial/console device. Mode "Off" disables it; "File" with +// File set captures the guest console (for boot debugging); "Tty" to a pty. +type ConsoleConfig struct { + Mode string `json:"mode"` + File string `json:"file,omitempty"` +} + +// VsockConfig is the hybrid-vsock the kata-agent listens on. Cid is the guest +// CID (kata uses 3); Socket is the host unix socket (kata.VsockSocketPath) that +// ateom then dials (DialAgent) to drive the agent. +type VsockConfig struct { + Cid int64 `json:"cid"` + Socket string `json:"socket"` +} + +// CreateVM creates (but does not boot) the VM from cfg via /api/v1/vm.create. +// The VMM must already be up (LaunchVMM). After this the VM is in "Created". +func (c *Client) CreateVM(ctx context.Context, cfg VmConfig) error { + if err := c.api.put(ctx, "/api/v1/vm.create", cfg); err != nil { + return fmt.Errorf("vm.create: %w", err) + } + return nil +} + +// BootVM boots a created VM via /api/v1/vm.boot (transitions Created -> Running). +func (c *Client) BootVM(ctx context.Context) error { + if err := c.api.put(ctx, "/api/v1/vm.boot", nil); err != nil { + return fmt.Errorf("vm.boot: %w", err) + } + return nil +} diff --git a/cmd/ateom-microvm/internal/ch/merge.go b/cmd/ateom-microvm/internal/ch/merge.go new file mode 100644 index 000000000..746a3bb11 --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/merge.go @@ -0,0 +1,214 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + + "golang.org/x/sys/unix" +) + +// MergeSparseOverlay reconstructs a COMPLETE memory snapshot from an OnDemand +// (userfaultfd) restore. CH's new snapshot (deltaFile) contains only the pages +// the guest faulted in since the OnDemand restore; every other page is unchanged +// from the snapshot it restored FROM (baseFile). So the complete current memory +// = baseFile, with deltaFile's populated pages overlaid. +// +// It writes outFile = a sparse copy of baseFile, then overlays every DATA region +// of deltaFile (located via SEEK_DATA/SEEK_HOLE, so holes — the un-faulted pages — +// are skipped) at the same byte offsets. baseFile and deltaFile MUST be flat images +// of identical size and layout (CH memory-ranges of the same guest + CH version), +// which holds across a restore/snapshot of one actor. This is a Firecracker-style +// differential snapshot implemented on top of CH (which has no native diff +// snapshot): it keeps OnDemand's fast, non-densifying restore while still producing +// complete, re-restorable snapshots for the suspend/resume chain. +func MergeSparseOverlay(ctx context.Context, baseFile, deltaFile, outFile string) error { + bi, err := os.Stat(baseFile) + if err != nil { + return fmt.Errorf("stat base %q: %w", baseFile, err) + } + // outFile := sparse copy of baseFile (preserves holes so it stays sparse). + tmp := outFile + ".merge.tmp" + _ = os.Remove(tmp) + if o, err := exec.CommandContext(ctx, "cp", "--sparse=always", baseFile, tmp).CombinedOutput(); err != nil { + return fmt.Errorf("cp base->tmp: %w: %s", err, o) + } + + d, err := os.Open(deltaFile) + if err != nil { + return fmt.Errorf("open delta %q: %w", deltaFile, err) + } + defer d.Close() + di, err := d.Stat() + if err != nil { + return err + } + if di.Size() != bi.Size() { + // Same guest => identical memory-ranges length. A mismatch means the overlay + // offsets wouldn't line up, so refuse rather than corrupt. + return fmt.Errorf("MergeSparseOverlay: size mismatch base=%d delta=%d", bi.Size(), di.Size()) + } + + o, err := os.OpenFile(tmp, os.O_RDWR, 0o600) + if err != nil { + return err + } + defer o.Close() + + if _, err := copySparseRegions(d, o); err != nil { + return err + } + // No fsync: atelet ships the merged image to GCS (the durability point), so a + // partial local file after a node crash is just discarded + the suspend retried; + // paying an ~150MiB fsync on the suspend critical path buys nothing. + if err := o.Close(); err != nil { + return err + } + return os.Rename(tmp, outFile) +} + +// MergeDeltaIntoBase overlays deltaFile's populated pages onto baseFile in place +// and leaves the complete merged snapshot at deltaFile's path — the same result as +// MergeSparseOverlay, but WITHOUT copying baseFile's working set on every suspend. +// +// baseFile is the per-actor restore staging file (restore-state/memory-ranges), +// demand-paged only by the now-paused CH we are about to tear down and discarded +// afterward. So rather than `cp`-ing its whole working set (e.g. ~150MiB of a 2GiB +// guest, ~0.8s on the suspend critical path), we rename baseFile next to deltaFile, +// overlay deltaFile's (small) faulted pages onto it, and swap it into deltaFile's +// place — turning an O(working-set) copy into an O(delta) write plus two renames. +// +// baseFile and deltaFile are siblings under the actor dir (restore-state/ and +// checkpoint-state/), so the renames are same-filesystem (metadata-only). If they +// straddle a mount boundary (EXDEV) it falls back to the copying MergeSparseOverlay +// (baseFile is untouched until the first rename succeeds). +func MergeDeltaIntoBase(ctx context.Context, baseFile, deltaFile string) error { + bi, err := os.Stat(baseFile) + if err != nil { + return fmt.Errorf("stat base %q: %w", baseFile, err) + } + di, err := os.Stat(deltaFile) + if err != nil { + return fmt.Errorf("stat delta %q: %w", deltaFile, err) + } + if di.Size() != bi.Size() { + // Same guest => identical memory-ranges length; a mismatch would misalign the + // overlay offsets, so refuse rather than corrupt. + return fmt.Errorf("MergeDeltaIntoBase: size mismatch base=%d delta=%d", bi.Size(), di.Size()) + } + + // Move baseFile (with its already-on-disk working set) next to deltaFile. If this + // fails with EXDEV the two are on different filesystems and baseFile is still + // intact, so fall back to the copying merge. + merged := deltaFile + ".merged.tmp" + _ = os.Remove(merged) + if err := os.Rename(baseFile, merged); err != nil { + if errors.Is(err, unix.EXDEV) { + return MergeSparseOverlay(ctx, baseFile, deltaFile, deltaFile) + } + return fmt.Errorf("rename base->merged: %w", err) + } + + d, err := os.Open(deltaFile) + if err != nil { + return fmt.Errorf("open delta %q: %w", deltaFile, err) + } + defer d.Close() + m, err := os.OpenFile(merged, os.O_RDWR, 0o600) + if err != nil { + return err + } + defer m.Close() + if _, err := copySparseRegions(d, m); err != nil { + return err + } + // No fsync: atelet ships the merged image to GCS (the durability point), so a + // partial local file after a crash is just discarded + the suspend retried. + if err := m.Close(); err != nil { + return err + } + // Put the merged image at deltaFile's name. Unlink the old delta FIRST, then + // rename onto the now-free name: renaming OVER an existing file makes ext4 + // (data=ordered) synchronously write back the renamed file's dirty pages, and + // `merged` carries ~150MiB of dirty download pages, so a replace-rename costs + // ~0.5-0.8s. Renaming to a non-existent name skips that flush (the dirty pages + // stay in page cache for atelet to ship), taking the merge ~840ms→~5ms. + if err := os.Remove(deltaFile); err != nil { + return fmt.Errorf("remove old delta: %w", err) + } + return os.Rename(merged, deltaFile) +} + +// copySparseRegions overwrites dst with every populated (non-hole) region of src +// at the same byte offsets, leaving dst's other bytes untouched. Holes in src are +// located via SEEK_DATA/SEEK_HOLE and skipped. src and dst are assumed to be the +// same logical size (the caller validates this). +func copySparseRegions(src, dst *os.File) (copied int64, err error) { + si, err := src.Stat() + if err != nil { + return 0, err + } + size := si.Size() + sfd := int(src.Fd()) + buf := make([]byte, 1<<20) + off := int64(0) + for off < size { + // Next populated region [ds, de) in src. + ds, err := unix.Seek(sfd, off, unix.SEEK_DATA) + if err != nil { + if errors.Is(err, unix.ENXIO) { + break // no more data + } + return copied, fmt.Errorf("SEEK_DATA: %w", err) + } + de, err := unix.Seek(sfd, ds, unix.SEEK_HOLE) + if err != nil { + return copied, fmt.Errorf("SEEK_HOLE: %w", err) + } + if _, err := src.Seek(ds, io.SeekStart); err != nil { + return copied, err + } + if _, err := dst.Seek(ds, io.SeekStart); err != nil { + return copied, err + } + remaining := de - ds + for remaining > 0 { + n := int64(len(buf)) + if n > remaining { + n = remaining + } + r, err := io.ReadFull(src, buf[:n]) + if r > 0 { + if _, werr := dst.Write(buf[:r]); werr != nil { + return copied, werr + } + copied += int64(r) + } + if err != nil { + return copied, fmt.Errorf("reading data region: %w", err) + } + remaining -= int64(r) + } + off = de + } + return copied, nil +} diff --git a/cmd/ateom-microvm/internal/ch/merge_test.go b/cmd/ateom-microvm/internal/ch/merge_test.go new file mode 100644 index 000000000..43a628d2a --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/merge_test.go @@ -0,0 +1,171 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "bytes" + "context" + "os" + "path/filepath" + "syscall" + "testing" +) + +// region is a populated byte range written into a sparse test image. +type region struct { + off int64 + data []byte +} + +// writeSparse creates a sparse file of logical size with the given populated +// regions (the gaps are holes). It mirrors a CH memory-ranges image: scattered +// resident pages in a sea of zero (free) RAM. +func writeSparse(t *testing.T, path string, size int64, regions []region) { + t.Helper() + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if err := f.Truncate(size); err != nil { + t.Fatal(err) + } + for _, r := range regions { + if _, err := f.WriteAt(r.data, r.off); err != nil { + t.Fatal(err) + } + } + if err := f.Sync(); err != nil { + t.Fatal(err) + } +} + +// fill returns n bytes of nonzero pseudo-data keyed by seed (never zero, so the +// regions are genuinely "data" and distinguishable from holes). +func fill(seed, n int) []byte { + b := make([]byte, n) + for i := range b { + b[i] = byte((i+seed)%251 + 1) + } + return b +} + +// TestMergeDeltaIntoBase asserts the fast (rename+overlay) merge produces exactly +// base-with-delta-overlaid — byte-identical to the reference copying merge +// (MergeSparseOverlay). This is the guest memory image, so any divergence = a dead +// or corrupted guest on resume. +func TestMergeDeltaIntoBase(t *testing.T) { + const size = 8 << 20 // 8 MiB logical + + // base = a complete restore source: data at three scattered offsets. + baseRegions := []region{ + {off: 0, data: fill(1, 4096)}, // first page + {off: 1 << 20, data: fill(2, 70000)}, // interior, crosses 64KiB boundaries + {off: size - 8192, data: fill(3, 4096)}, // near the end + } + // delta = CH's post-restore faulted pages: one OVERLAPS base (newer content, + // must win), one lands in a base HOLE (newly faulted page), rest holes. + deltaRegions := []region{ + {off: 1 << 20, data: fill(99, 70000)}, // overwrites base's interior region + {off: 4 << 20, data: fill(42, 12345)}, // new page where base had a hole + } + + // Build the expected merged image in memory: base, with delta overlaid. + want := make([]byte, size) + for _, r := range baseRegions { + copy(want[r.off:], r.data) + } + for _, r := range deltaRegions { + copy(want[r.off:], r.data) + } + + ctx := context.Background() + + // --- fast path: MergeDeltaIntoBase (consumes base, result lands in delta) --- + dir := t.TempDir() + base := filepath.Join(dir, "restore-state", "memory-ranges") + delta := filepath.Join(dir, "checkpoint-state", "memory-ranges") + if err := os.MkdirAll(filepath.Dir(base), 0o700); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(delta), 0o700); err != nil { + t.Fatal(err) + } + writeSparse(t, base, size, baseRegions) + writeSparse(t, delta, size, deltaRegions) + + if err := MergeDeltaIntoBase(ctx, base, delta); err != nil { + t.Fatalf("MergeDeltaIntoBase: %v", err) + } + got, err := os.ReadFile(delta) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("MergeDeltaIntoBase result != expected (len got=%d want=%d)", len(got), len(want)) + } + // base must have been consumed (renamed away), not left behind as a stale copy. + if _, err := os.Stat(base); !os.IsNotExist(err) { + t.Errorf("base still present after MergeDeltaIntoBase (err=%v); expected it consumed", err) + } + // The merged image must stay sparse (the holes between regions are not allocated). + if fi, err := os.Stat(delta); err == nil { + if st, ok := fi.Sys().(*syscall.Stat_t); ok && st.Blocks > 0 { + if st.Blocks*512 >= size { + t.Logf("note: merged not sparse on this fs (actual=%d >= logical=%d) — correctness still holds", st.Blocks*512, size) + } + } + } + + // --- reference path: MergeSparseOverlay must produce the identical bytes --- + rdir := t.TempDir() + rbase := filepath.Join(rdir, "memory-ranges-base") + rdelta := filepath.Join(rdir, "memory-ranges-delta") + writeSparse(t, rbase, size, baseRegions) + writeSparse(t, rdelta, size, deltaRegions) + if err := MergeSparseOverlay(ctx, rbase, rdelta, rdelta); err != nil { + t.Fatalf("MergeSparseOverlay: %v", err) + } + ref, err := os.ReadFile(rdelta) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(ref, want) { + t.Fatalf("MergeSparseOverlay result != expected (sanity check on the reference)") + } + if !bytes.Equal(got, ref) { + t.Fatalf("MergeDeltaIntoBase and MergeSparseOverlay disagree") + } +} + +// TestMergeDeltaIntoBaseSizeMismatch verifies a base/delta size mismatch is +// refused (misaligned overlay would corrupt the image) rather than silently +// producing garbage. +func TestMergeDeltaIntoBaseSizeMismatch(t *testing.T) { + dir := t.TempDir() + base := filepath.Join(dir, "base") + delta := filepath.Join(dir, "delta") + writeSparse(t, base, 1<<20, []region{{off: 0, data: fill(1, 4096)}}) + writeSparse(t, delta, 2<<20, []region{{off: 0, data: fill(2, 4096)}}) + if err := MergeDeltaIntoBase(context.Background(), base, delta); err == nil { + t.Fatal("expected size-mismatch error, got nil") + } + // base must be untouched (no destructive rename happened before the check). + if _, err := os.Stat(base); err != nil { + t.Errorf("base should be intact after a refused merge: %v", err) + } +} diff --git a/cmd/ateom-microvm/internal/ch/restorefds.go b/cmd/ateom-microvm/internal/ch/restorefds.go new file mode 100644 index 000000000..88e2c96df --- /dev/null +++ b/cmd/ateom-microvm/internal/ch/restorefds.go @@ -0,0 +1,241 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ch + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "net" + "os" + "os/exec" + "strings" + "time" + + "golang.org/x/sys/unix" +) + +// RestoredNet identifies one fd-backed network device in a snapshot and the +// fresh tap FDs to back it with on restore. kata boots CH virtio-net devices +// from tap FDs, so the snapshot's config requires net_fds on restore +// (RestoreMissingRequiredNetId otherwise); CH reopens the device on the FDs we +// pass over the api-socket via SCM_RIGHTS. +type RestoredNet struct { + // ID is the device id from the snapshot's config.json (e.g. "_net1"). + ID string + // FDs are open tap fds (one per queue pair) for CH to adopt. + FDs []int +} + +// LaunchVMMOptions configures starting a bare VMM (no VM) for an FD-passing +// restore. +type LaunchVMMOptions struct { + // Binary is the cloud-hypervisor executable (defaults to "cloud-hypervisor"). + Binary string + // APISocket is the api-socket path the new VMM should listen on. + APISocket string + // Stdout/Stderr receive the VMM's output. + Stdout, Stderr interface{ Write([]byte) (int, error) } +} + +// LaunchVMM starts a cloud-hypervisor process with only an api-socket (no VM) +// and waits until it answers. Use Client.RestoreWithNetFDs to then restore a +// snapshot that has fd-backed net devices. The caller owns cmd. +func LaunchVMM(ctx context.Context, o LaunchVMMOptions) (*exec.Cmd, *Client, error) { + if o.APISocket == "" { + return nil, nil, fmt.Errorf("LaunchVMMOptions.APISocket is required") + } + bin := o.Binary + if bin == "" { + bin = "cloud-hypervisor" + } + _ = os.Remove(o.APISocket) + // Deliberately NOT exec.CommandContext: the VMM must outlive the RPC whose + // ctx launched it. The caller owns cmd; WaitReady honors ctx. + cmd := exec.Command(bin, "--api-socket", o.APISocket) + cmd.Stdout = o.Stdout + cmd.Stderr = o.Stderr + if err := cmd.Start(); err != nil { + return nil, nil, fmt.Errorf("while starting cloud-hypervisor: %w", err) + } + client := NewClient(o.APISocket) + if err := client.WaitReady(ctx, 15*time.Second); err != nil { + _ = cmd.Process.Kill() + _, _ = cmd.Process.Wait() + return nil, nil, fmt.Errorf("while waiting for VMM api-socket: %w", err) + } + return cmd, client, nil +} + +// RestoreWithNetFDs issues vm.restore for a snapshot dir, passing fresh tap FDs +// for the snapshot's fd-backed net devices via SCM_RIGHTS on the api-socket +// (the only way CH accepts net FDs on restore; mirrors ch-remote's +// send_with_fds). The VM comes back paused; call Resume after. +// +// memMode selects guest-RAM restore: "" / "Copy" = eager copy (CH default), or +// "OnDemand" = userfaultfd demand-paging. OnDemand keeps the (memfd-backed) guest +// memory SPARSE — it only faults in the pages the guest touches, instead of eager +// copy densifying the whole memfd — so a subsequent snapshot writes just the +// working set (fast) instead of full RAM. Confirmed on CH v52: the REST +// RestoreConfig accepts memory_restore_mode (enum Copy|OnDemand) alongside the +// SCM_RIGHTS net_fds, so ondemand + fd-backed net DO compose over REST (an earlier +// note claimed memory_restore_mode was CLI-only; that was a pre-v52 limitation). +// NOTE: with OnDemand, CH demand-pages from the snapshot's memory file for the +// VM's whole lifetime, so sourceDir must stay present until the actor is torn down. +func (c *Client) RestoreWithNetFDs(ctx context.Context, sourceDir string, nets []RestoredNet, memMode string) error { + type restoredNetConfig struct { + ID string `json:"id"` + NumFDs int `json:"num_fds"` + } + cfg := struct { + SourceURL string `json:"source_url"` + MemoryRestoreMode string `json:"memory_restore_mode,omitempty"` + NetFDs []restoredNetConfig `json:"net_fds,omitempty"` + }{SourceURL: SnapshotURL(sourceDir), MemoryRestoreMode: memMode} + var fds []int + for _, n := range nets { + cfg.NetFDs = append(cfg.NetFDs, restoredNetConfig{ID: n.ID, NumFDs: len(n.FDs)}) + fds = append(fds, n.FDs...) + } + body, err := json.Marshal(cfg) + if err != nil { + return err + } + + raddr, err := net.ResolveUnixAddr("unix", c.apiSocket) + if err != nil { + return err + } + conn, err := net.DialUnix("unix", nil, raddr) + if err != nil { + return fmt.Errorf("dialing api-socket: %w", err) + } + defer conn.Close() + if dl, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(dl) + } else { + _ = conn.SetDeadline(time.Now().Add(60 * time.Second)) + } + + // Raw HTTP/1.1 over the unix socket: net/http cannot attach SCM_RIGHTS, and + // CH's micro_http collects fds from the recvmsg ancillary data of the + // request that carries them. + req := fmt.Sprintf("PUT /api/v1/vm.restore HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body) + var oob []byte + if len(fds) > 0 { + oob = unix.UnixRights(fds...) + } + if _, _, err := conn.WriteMsgUnix([]byte(req), oob, nil); err != nil { + return fmt.Errorf("sending vm.restore with fds: %w", err) + } + + status, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return fmt.Errorf("reading vm.restore response: %w", err) + } + parts := strings.SplitN(strings.TrimSpace(status), " ", 3) + if len(parts) < 2 || !strings.HasPrefix(parts[1], "2") { + return fmt.Errorf("vm.restore failed: %s", strings.TrimSpace(status)) + } + return nil +} + +// AddNetWithFDs hotplugs a virtio-net device into a freshly-created (pre-boot or +// running) VM, passing the tap FDs via SCM_RIGHTS — the boot-path analog of +// RestoreWithNetFDs. kata adds net this way between vm.create and vm.boot +// (clh.go vmAddNetPut). mac may be empty (CH assigns one); numQueues should be +// 2*queuePairs (rx+tx) and len(fds) == queuePairs. +func (c *Client) AddNetWithFDs(ctx context.Context, mac string, numQueues int, fds []int) error { + cfg := struct { + Mac string `json:"mac,omitempty"` + NumQueues int `json:"num_queues,omitempty"` + NumFDs int `json:"num_fds,omitempty"` + }{Mac: mac, NumQueues: numQueues, NumFDs: len(fds)} + body, err := json.Marshal(cfg) + if err != nil { + return err + } + raddr, err := net.ResolveUnixAddr("unix", c.apiSocket) + if err != nil { + return err + } + conn, err := net.DialUnix("unix", nil, raddr) + if err != nil { + return fmt.Errorf("dialing api-socket: %w", err) + } + defer conn.Close() + if dl, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(dl) + } else { + _ = conn.SetDeadline(time.Now().Add(30 * time.Second)) + } + req := fmt.Sprintf("PUT /api/v1/vm.add-net HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body) + var oob []byte + if len(fds) > 0 { + oob = unix.UnixRights(fds...) + } + if _, _, err := conn.WriteMsgUnix([]byte(req), oob, nil); err != nil { + return fmt.Errorf("sending vm.add-net with fds: %w", err) + } + status, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return fmt.Errorf("reading vm.add-net response: %w", err) + } + parts := strings.SplitN(strings.TrimSpace(status), " ", 3) + if len(parts) < 2 || !strings.HasPrefix(parts[1], "2") { + return fmt.Errorf("vm.add-net failed: %s", strings.TrimSpace(status)) + } + return nil +} + +// SnapshotNetDevice describes one net device found in a CH snapshot's +// config.json. Restore must supply net_fds for every one of them. +type SnapshotNetDevice struct { + // ID is the CH device id (e.g. "_net1"). + ID string + // QueuePairs is the number of tap FDs the device needs (num_queues/2). + QueuePairs int + // MAC is the guest-visible MAC address of the device. + MAC string +} + +// SnapshotNetDevices parses a CH snapshot's config.json and returns its net +// devices, in order. +func SnapshotNetDevices(snapshotDir string) ([]SnapshotNetDevice, error) { + b, err := os.ReadFile(snapshotDir + "/config.json") + if err != nil { + return nil, err + } + var cfg struct { + Net []struct { + ID string `json:"id"` + NumQueues int `json:"num_queues"` + MAC string `json:"mac"` + } `json:"net"` + } + if err := json.Unmarshal(b, &cfg); err != nil { + return nil, fmt.Errorf("parsing snapshot config.json: %w", err) + } + var out []SnapshotNetDevice + for _, n := range cfg.Net { + qp := n.NumQueues / 2 + if qp < 1 { + qp = 1 + } + out = append(out, SnapshotNetDevice{ID: n.ID, QueuePairs: qp, MAC: n.MAC}) + } + return out, nil +} diff --git a/cmd/ateom-microvm/internal/kata/agentclient.go b/cmd/ateom-microvm/internal/kata/agentclient.go new file mode 100644 index 000000000..42537f8f9 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/agentclient.go @@ -0,0 +1,273 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb" + "github.com/containerd/ttrpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +// agentVsockPort is the guest port the kata-agent's ttrpc server listens on. +const agentVsockPort = 1024 + +// debugConsoleVsockPort is the guest port kata's debug console listens on when +// debug_console_enabled=true. It's a raw shell over the hybrid vsock. +const debugConsoleVsockPort = 1026 + +// DebugConsoleDump connects to the guest's kata debug console (vsock 1026) and +// runs cmd, returning its combined output. Diagnostic only (requires +// debug_console_enabled=true in the kata config). Best-effort: returns the error +// text on failure rather than failing the caller. +func DebugConsoleDump(ctx context.Context, vsockPath, cmd string) string { + d := net.Dialer{} + dctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + conn, err := d.DialContext(dctx, "unix", vsockPath) + if err != nil { + return "debug-console dial: " + err.Error() + } + defer conn.Close() + _ = conn.SetDeadline(time.Now().Add(8 * time.Second)) + if _, err := fmt.Fprintf(conn, "CONNECT %d\n", debugConsoleVsockPort); err != nil { + return "debug-console CONNECT: " + err.Error() + } + br := bufio.NewReader(conn) + if _, err := br.ReadString('\n'); err != nil { // the "OK " line + return "debug-console CONNECT reply: " + err.Error() + } + // The kata debug console is an INTERACTIVE shell on a PTY (console.rs spawns + // /bin/bash|/bin/sh), so it ECHOES the command line back before running it. We + // must not let the echo trip the end sentinel: write the sentinel split by '' + // (which the shell strips) so the echoed command contains "__ATE''_END__" (no + // match) while the shell's OUTPUT is "__ATE_END__" (match). Read until the + // output sentinel (or EOF/deadline). + if _, err := fmt.Fprintf(conn, "{ %s ; } 2>&1; echo __ATE''_END__\n", cmd); err != nil { + return "debug-console write: " + err.Error() + } + const sentinel = "__ATE_END__" + var out strings.Builder + for { + line, err := br.ReadString('\n') + if line != "" { + if strings.Contains(line, sentinel) { + break // the shell's echo of the sentinel line (output), not its command echo + } + out.WriteString(line) + } + if err != nil { + break + } + } + return out.String() +} + +// AgentClient is a thin ttrpc client for the kata-agent RPCs ateom drives +// directly. ateom owns the cloud-hypervisor boot (no kata shim) and drives the +// kata-agent over ttrpc itself: alongside UpdateInterface/UpdateRoutes for guest +// networking, it issues CreateContainer/StartContainer to assemble the container +// rootfs directly, instead of relying on the kata runtime's hooks +// (ShareRootFilesystem) to emit the storages. It dials the agent through CH's +// hybrid-vsock unix socket — the same channel the kata shim would use. +type AgentClient struct { + conn net.Conn + client *ttrpc.Client +} + +// DialAgent connects to the kata-agent through the hybrid-vsock socket at +// vsockPath (VsockSocketPath(id)): plain-text "CONNECT " handshake with +// the VMM, then ttrpc over the stream. +func DialAgent(ctx context.Context, vsockPath string) (*AgentClient, error) { + d := net.Dialer{} + conn, err := d.DialContext(ctx, "unix", vsockPath) + if err != nil { + return nil, fmt.Errorf("dialing hybrid vsock %q: %w", vsockPath, err) + } + if dl, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(dl) + } else { + _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) + } + if _, err := fmt.Fprintf(conn, "CONNECT %d\n", agentVsockPort); err != nil { + conn.Close() + return nil, fmt.Errorf("hybrid vsock CONNECT: %w", err) + } + line, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + conn.Close() + return nil, fmt.Errorf("hybrid vsock CONNECT response: %w", err) + } + if !strings.HasPrefix(line, "OK ") { + conn.Close() + return nil, fmt.Errorf("hybrid vsock CONNECT refused: %q", strings.TrimSpace(line)) + } + _ = conn.SetDeadline(time.Time{}) // ttrpc manages its own timeouts via ctx + return &AgentClient{conn: conn, client: ttrpc.NewClient(conn)}, nil +} + +// Close shuts the ttrpc client and underlying connection. +func (a *AgentClient) Close() error { + err := a.client.Close() + _ = a.conn.Close() + return err +} + +// CreateContainer asks the agent to create a container: mount its storages (in +// order) and build the rootfs, then fork the parked init process. This is the +// hook point — the agent mounts storages[] (here: a bind of the virtio-fs lower +// followed by the tmpfs-upper overlay) before init_rootfs consumes the rootfs. +// Mirrors grpc.AgentService/CreateContainer (returns google.protobuf.Empty). +func (a *AgentClient) CreateContainer(ctx context.Context, req *agentpb.CreateContainerRequest) error { + if err := a.client.Call(ctx, "grpc.AgentService", "CreateContainer", req, &emptypb.Empty{}); err != nil { + return fmt.Errorf("agent CreateContainer: %w", err) + } + return nil +} + +// StartContainer execs the container's init process (pivots into the rootfs the +// storages assembled). Mirrors grpc.AgentService/StartContainer. +func (a *AgentClient) StartContainer(ctx context.Context, containerID string) error { + req := &agentpb.StartContainerRequest{ContainerId: containerID} + if err := a.client.Call(ctx, "grpc.AgentService", "StartContainer", req, &emptypb.Empty{}); err != nil { + return fmt.Errorf("agent StartContainer: %w", err) + } + return nil +} + +// CreateSandbox establishes the agent's sandbox context (sandbox id, hostname, +// sandbox pidns) before any container is created. The kata shim normally issues +// this once at VM boot; on the ateom-owned-boot path (no shim) ateom must call it +// itself so the agent has a sandbox to attach containers to. Storages is empty — +// the actor rootfs arrives as a per-container "blk" storage, not a sandbox mount. +// Mirrors grpc.AgentService/CreateSandbox (returns google.protobuf.Empty). +func (a *AgentClient) CreateSandbox(ctx context.Context, req *agentpb.CreateSandboxRequest) error { + if err := a.client.Call(ctx, "grpc.AgentService", "CreateSandbox", req, &emptypb.Empty{}); err != nil { + return fmt.Errorf("agent CreateSandbox: %w", err) + } + return nil +} + +// UpdateInterface configures a guest network interface (the kata shim's job; on +// the owned-boot path ateom does it). The agent matches the link by HwAddr, then +// applies the name/IP/MTU. Mirrors grpc.AgentService/UpdateInterface (returns the +// resulting Interface). +func (a *AgentClient) UpdateInterface(ctx context.Context, iface *agentpb.Interface) error { + req := &agentpb.UpdateInterfaceRequest{Interface: iface} + if err := a.client.Call(ctx, "grpc.AgentService", "UpdateInterface", req, &agentpb.Interface{}); err != nil { + return fmt.Errorf("agent UpdateInterface: %w", err) + } + return nil +} + +// UpdateRoutes replaces the guest's route table with routes (the agent flushes +// and re-adds). Pass the connected (scope-link) route AND the default route so +// the gateway stays reachable. Mirrors grpc.AgentService/UpdateRoutes. +func (a *AgentClient) UpdateRoutes(ctx context.Context, routes []*agentpb.Route) error { + req := &agentpb.UpdateRoutesRequest{Routes: &agentpb.Routes{Routes: routes}} + if err := a.client.Call(ctx, "grpc.AgentService", "UpdateRoutes", req, &agentpb.Routes{}); err != nil { + return fmt.Errorf("agent UpdateRoutes: %w", err) + } + return nil +} + +// AddARPNeighbors installs static ARP entries in the guest — used to pin the +// gateway (169.254.17.1) to its FIXED MAC so a restored guest's frozen neighbor +// entry stays valid across pods. Mirrors grpc.AgentService/AddARPNeighbors. +func (a *AgentClient) AddARPNeighbors(ctx context.Context, neighbors []*agentpb.ARPNeighbor) error { + req := &agentpb.AddARPNeighborsRequest{Neighbors: &agentpb.ARPNeighbors{ARPNeighbors: neighbors}} + if err := a.client.Call(ctx, "grpc.AgentService", "AddARPNeighbors", req, &emptypb.Empty{}); err != nil { + return fmt.Errorf("agent AddARPNeighbors: %w", err) + } + return nil +} + +// ReadStdout reads up to max bytes from the container process's stdout. It is a +// unary RPC (NOT a server stream): each call returns whatever bytes the agent has +// buffered (up to max), so callers loop until it returns an error — the agent +// returns an error/EOF-like status once the stream ends (container exit / connection +// close). Mirrors grpc.AgentService/ReadStdout. The kata-agent keys the stream by +// ExecId, which the owned-boot path sets equal to ContainerId (see StartBlkWorkload). +func (a *AgentClient) ReadStdout(ctx context.Context, containerID, execID string, max uint32) ([]byte, error) { + resp := &agentpb.ReadStreamResponse{} + req := &agentpb.ReadStreamRequest{ContainerId: containerID, ExecId: execID, Len: max} + if err := a.client.Call(ctx, "grpc.AgentService", "ReadStdout", req, resp); err != nil { + return nil, err + } + return resp.GetData(), nil +} + +// ReadStderr reads up to max bytes from the container process's stderr. Same +// semantics as ReadStdout (unary, loop-until-error). Mirrors +// grpc.AgentService/ReadStderr. +func (a *AgentClient) ReadStderr(ctx context.Context, containerID, execID string, max uint32) ([]byte, error) { + resp := &agentpb.ReadStreamResponse{} + req := &agentpb.ReadStreamRequest{ContainerId: containerID, ExecId: execID, Len: max} + if err := a.client.Call(ctx, "grpc.AgentService", "ReadStderr", req, resp); err != nil { + return nil, err + } + return resp.GetData(), nil +} + +// StreamReader adapts the agent's repeated ReadStdout/ReadStderr unary calls into +// an io.Reader, so the consumer can pump the container's output through the shared +// actorlog forwarder like any other stream. Each Read issues one RPC with Len set +// to len(p); on RPC error (the agent signals EOF/container-exit via an error +// status) it returns io.EOF so the consuming goroutine terminates cleanly. The +// reader stops when its context is cancelled OR the underlying ttrpc connection is +// closed (both surface as RPC errors), so it never outlives the AgentClient. +type StreamReader struct { + ctx context.Context + ac *AgentClient + containerID string + execID string + stderr bool +} + +// NewStdioReader returns an io.Reader over the container's stdout (stderr=false) +// or stderr (stderr=true). execID matches the value passed to StartBlkWorkload +// (equal to containerID on the owned-boot path). +func NewStdioReader(ctx context.Context, ac *AgentClient, containerID, execID string, stderr bool) *StreamReader { + return &StreamReader{ctx: ctx, ac: ac, containerID: containerID, execID: execID, stderr: stderr} +} + +// Read issues a single ReadStdout/ReadStderr RPC for up to len(p) bytes, copying +// the returned data into p. It returns io.EOF on any RPC error so the consumer +// stops cleanly when the container exits or the connection closes. +func (r *StreamReader) Read(p []byte) (int, error) { + var ( + data []byte + err error + ) + if r.stderr { + data, err = r.ac.ReadStderr(r.ctx, r.containerID, r.execID, uint32(len(p))) + } else { + data, err = r.ac.ReadStdout(r.ctx, r.containerID, r.execID, uint32(len(p))) + } + if err != nil { + return 0, io.EOF + } + n := copy(p, data) + return n, nil +} diff --git a/cmd/ateom-microvm/internal/kata/cleanup_linux.go b/cmd/ateom-microvm/internal/kata/cleanup_linux.go new file mode 100644 index 000000000..28a4d9ab2 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/cleanup_linux.go @@ -0,0 +1,101 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +// CleanupSandboxState removes leftover host-side state for a sandbox id (the +// virtio-fs shared sandbox dir and the per-VM runtime dir), lazily unmounting +// anything still mounted underneath them first, and kills orphaned per-sandbox +// processes. ateom owns the cloud-hypervisor boot directly (no kata shim, no +// containerd), so a failed Create does not fully self-clean; the deterministic +// sandbox id (= actor id) then collides on the next attempt: "listen unix +// .../virtiofsd.sock: bind: address already in use", "Could not bind mount +// .../shared/sandboxes//mounts", "directory not empty". Calling this +// before each run gives a clean slate. +func CleanupSandboxState(ctx context.Context, id string) { + dirs := []string{ + filepath.Join("/run/kata-containers/shared/sandboxes", id), + filepath.Join(vcVMDir, id), + } + if b, err := os.ReadFile("/proc/self/mountinfo"); err == nil { + var mounts []string + for _, line := range strings.Split(string(b), "\n") { + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + mp := fields[4] // mount point + for _, d := range dirs { + if mp == d || strings.HasPrefix(mp, d+"/") { + mounts = append(mounts, mp) + break + } + } + } + // Deepest paths first so child mounts unmount before their parents. + sort.Slice(mounts, func(i, j int) bool { return len(mounts[i]) > len(mounts[j]) }) + for _, mp := range mounts { + if err := unix.Unmount(mp, unix.MNT_DETACH); err != nil { + slog.WarnContext(ctx, "Failed to unmount leftover sandbox mount", + slog.String("mount", mp), slog.Any("err", err)) + } + } + } + for _, d := range dirs { + if err := os.RemoveAll(d); err != nil { + slog.WarnContext(ctx, "Failed to remove leftover sandbox dir", + slog.String("dir", d), slog.Any("err", err)) + } + } + // Kill orphaned per-sandbox processes (cloud-hypervisor / virtiofsd) left by + // a prior killed attempt: a canceled Create leaves the CH it spawned running + // (reparented to us) holding guest RAM and stale sockets. Matched strictly by + // the sandbox id (an actor UUID) appearing in the cmdline, so nothing + // unrelated can match. + entries, err := os.ReadDir("/proc") + if err != nil { + return + } + for _, e := range entries { + pid, perr := strconv.Atoi(e.Name()) + if perr != nil || pid == os.Getpid() { + continue + } + cmdline, rerr := os.ReadFile(filepath.Join("/proc", e.Name(), "cmdline")) + if rerr != nil || !strings.Contains(string(cmdline), id) { + continue + } + argv0 := strings.SplitN(string(cmdline), "\x00", 2)[0] + if strings.Contains(argv0, "cloud-hypervisor") || strings.Contains(argv0, "virtiofsd") { + if err := unix.Kill(pid, unix.SIGKILL); err != nil { + slog.WarnContext(ctx, "Failed to kill orphaned sandbox process", + slog.Int("pid", pid), slog.String("argv0", argv0), slog.Any("err", err)) + } + } + } +} diff --git a/cmd/ateom-microvm/internal/kata/config.go b/cmd/ateom-microvm/internal/kata/config.go new file mode 100644 index 000000000..553059d49 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/config.go @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "fmt" + "strings" + + toml "github.com/pelletier/go-toml/v2" +) + +// KataConfig holds the values ateom reads from a kata configuration.toml. ateom +// owns the cloud-hypervisor boot and points it at the runtime-fetched asset paths +// directly, so the only things it needs from the config are the guest sizing and +// the agent kernel command line. +type KataConfig struct { + // MemoryMiB is the guest RAM size ([hypervisor.clh] default_memory). + MemoryMiB int + // VCPUs is the guest vCPU count ([hypervisor.clh] default_vcpus). + VCPUs int + // KernelParams is the guest kernel command line ([hypervisor.clh] + // kernel_params): the kata-agent parameters (agent.log, the systemd target, + // etc.). The owned boot appends these to the cloud-hypervisor payload cmdline, + // since there is no kata shim to inject them. + KernelParams string +} + +// clhConfigTOML mirrors the subset of a kata configuration.toml ateom reads. +// Unmarshalling ignores every other key, so it stays valid across kata releases. +type clhConfigTOML struct { + Hypervisor struct { + CLH struct { + DefaultMemory int `toml:"default_memory"` + DefaultVCPUs int `toml:"default_vcpus"` + KernelParams string `toml:"kernel_params"` + } `toml:"clh"` + } `toml:"hypervisor"` +} + +// ParseConfig reads the guest sizing and kernel_params from a kata +// configuration.toml. memDefault/vcpuDefault are substituted when the key is +// absent or non-positive (kata also accepts default_vcpus = -1 meaning "all host +// CPUs", which the owned boot does not support). +func ParseConfig(base []byte, memDefault, vcpuDefault int) (KataConfig, error) { + var c clhConfigTOML + if err := toml.Unmarshal(base, &c); err != nil { + return KataConfig{}, fmt.Errorf("parsing kata config: %w", err) + } + cfg := KataConfig{ + MemoryMiB: c.Hypervisor.CLH.DefaultMemory, + VCPUs: c.Hypervisor.CLH.DefaultVCPUs, + KernelParams: c.Hypervisor.CLH.KernelParams, + } + if cfg.MemoryMiB <= 0 { + cfg.MemoryMiB = memDefault + } + if cfg.VCPUs <= 0 { + cfg.VCPUs = vcpuDefault + } + return cfg, nil +} + +// WithDebugConsole appends the kata-agent debug-console kernel parameters so the +// guest agent binds a root debug shell on vsock port 1026, which DebugConsoleDump +// connects to for in-guest diagnostics. Both params are required: agent.debug_console +// enables the console and agent.debug_console_vport=1026 makes the agent bind it on +// the vsock port (the agent only binds a vsock listener when the vport is > 0). +// Idempotent. +func WithDebugConsole(kernelParams string) string { + return appendKernelParams(kernelParams, "agent.debug_console_vport", + "agent.debug_console agent.debug_console_vport=1026") +} + +// WithAgentDebug appends agent.log=debug so the guest kata-agent emits +// debug-level logs (including the failing path on errors) over its vsock log +// channel. Idempotent. +func WithAgentDebug(kernelParams string) string { + return appendKernelParams(kernelParams, "agent.log=", "agent.log=debug agent.debug_console") +} + +// appendKernelParams appends add to a kernel_params string unless marker is +// already present (so repeated calls are no-ops). +func appendKernelParams(kernelParams, marker, add string) string { + if strings.Contains(kernelParams, marker) { + return kernelParams + } + if kernelParams == "" { + return add + } + return kernelParams + " " + add +} diff --git a/cmd/ateom-microvm/internal/kata/config_test.go b/cmd/ateom-microvm/internal/kata/config_test.go new file mode 100644 index 000000000..776aa7e00 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/config_test.go @@ -0,0 +1,96 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "strings" + "testing" +) + +// stockConfig mirrors the [hypervisor.clh] keys ateom reads from a kata +// configuration.toml (every other key is ignored by ParseConfig). +const stockConfig = `[hypervisor.clh] +path = "/usr/local/bin/cloud-hypervisor" +kernel = "/opt/kata/share/kata-containers/vmlinux.container" +image = "/opt/kata/share/kata-containers/kata-containers.img" +default_memory = 512 +default_vcpus = 2 +kernel_params = "agent.foo=bar systemd.unit=kata-containers.target" +shared_fs = "virtio-fs" +` + +func TestParseConfig(t *testing.T) { + cfg, err := ParseConfig([]byte(stockConfig), 2048, 1) + if err != nil { + t.Fatalf("ParseConfig: %v", err) + } + if cfg.MemoryMiB != 512 { + t.Errorf("MemoryMiB = %d, want 512", cfg.MemoryMiB) + } + if cfg.VCPUs != 2 { + t.Errorf("VCPUs = %d, want 2", cfg.VCPUs) + } + if want := "agent.foo=bar systemd.unit=kata-containers.target"; cfg.KernelParams != want { + t.Errorf("KernelParams = %q, want %q", cfg.KernelParams, want) + } +} + +// TestParseConfigDefaults asserts the mem/vcpu defaults kick in when the keys are +// absent or non-positive (kata also accepts default_vcpus = -1 meaning "all host +// CPUs", which the owned boot does not support). +func TestParseConfigDefaults(t *testing.T) { + for _, tc := range []struct { + name string + toml string + }{ + {"absent", "[hypervisor.clh]\nkernel_params = \"x\"\n"}, + {"nonpositive", "[hypervisor.clh]\ndefault_memory = 0\ndefault_vcpus = -1\n"}, + } { + t.Run(tc.name, func(t *testing.T) { + cfg, err := ParseConfig([]byte(tc.toml), 2048, 1) + if err != nil { + t.Fatalf("ParseConfig: %v", err) + } + if cfg.MemoryMiB != 2048 { + t.Errorf("MemoryMiB = %d, want default 2048", cfg.MemoryMiB) + } + if cfg.VCPUs != 1 { + t.Errorf("VCPUs = %d, want default 1", cfg.VCPUs) + } + }) + } +} + +func TestWithDebugConsole(t *testing.T) { + got := WithDebugConsole("root=/dev/vda1") + if !strings.Contains(got, "agent.debug_console") || !strings.Contains(got, "agent.debug_console_vport=1026") { + t.Errorf("WithDebugConsole did not append the debug-console params: %q", got) + } + // Idempotent: a second call must not append the params again. + if again := WithDebugConsole(got); again != got { + t.Errorf("WithDebugConsole not idempotent:\n first = %q\nsecond = %q", got, again) + } +} + +func TestWithAgentDebug(t *testing.T) { + got := WithAgentDebug("root=/dev/vda1") + if !strings.Contains(got, "agent.log=debug") { + t.Errorf("WithAgentDebug did not append agent.log=debug: %q", got) + } + // Idempotent: a second call must not append agent.log again. + if again := WithAgentDebug(got); again != got { + t.Errorf("WithAgentDebug not idempotent:\n first = %q\nsecond = %q", got, again) + } +} diff --git a/cmd/ateom-microvm/internal/kata/disk.go b/cmd/ateom-microvm/internal/kata/disk.go new file mode 100644 index 000000000..ae03353f7 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/disk.go @@ -0,0 +1,140 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "context" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strconv" +) + +// rootfsDiskScratchBytes is the free-space headroom added on top of a bundle's +// contents when sizing its writable rootfs disk: room for the actor to write +// during a single activation. It stays sparse (unused space is holes), so it +// costs nothing in the image file or the memory-only snapshot. +const rootfsDiskScratchBytes = 512 << 20 + +// rootfsDiskGeometry walks srcDir and returns the ext4 image size (MiB) and the +// inode count to build a writable rootfs disk holding that tree plus headroom for +// ext4 metadata and the actor's in-activation scratch writes. Both are +// DETERMINISTIC functions of the tree's apparent contents (summed regular-file +// sizes and entry count, NOT host block allocation), so the cold-boot build and +// the restore-time rebuild from the same OCI image produce an identically-sized +// disk — required because the guest resumes with the ext4 superblock cached in RAM. +func rootfsDiskGeometry(srcDir string) (sizeMiB int, inodes int64, err error) { + var contentBytes, entries int64 + if werr := filepath.WalkDir(srcDir, func(_ string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + entries++ // every entry (file, dir, symlink, device) needs an inode + if d.Type().IsRegular() { + info, ierr := d.Info() + if ierr != nil { + return ierr + } + contentBytes += info.Size() + } + return nil + }); werr != nil { + return 0, 0, werr + } + + const ( + mib = 1 << 20 + inodeSizeBytes = 256 // ext4 default; over-estimates the table if it's 128 + ) + // One inode per entry plus 25% and a fixed reserve, so the actor can create new + // files during its activation without exhausting inodes (the default + // size-derived ratio can starve a file-heavy rootfs). + inodes = entries + entries/4 + 8192 + // Contents + the eagerly-written inode table + ~6% for bitmaps/directory/extent + // metadata + the scratch reserve. Unused space stays sparse (holes). + sizeBytes := contentBytes + inodes*inodeSizeBytes + contentBytes/16 + rootfsDiskScratchBytes + sizeMiB = int((sizeBytes + mib - 1) / mib) + return sizeMiB, inodes, nil +} + +// BuildExt4Image creates a raw ext4 disk image at outPath, sized dynamically from +// srcDir (see rootfsDiskGeometry), pre-populated with srcDir's contents in a single +// mkfs pass (`mkfs.ext4 -d ...`). This is how the ateom-owned-boot path +// turns the actor's OCI bundle rootfs into a writable virtio-blk disk (/dev/vdb): +// the guest mounts it as the container rootfs, so rootfs writes land on this +// host-backed file (off guest RAM) -> memory-only CH snapshot, no balloon. +// +// The size is a deterministic function of srcDir's contents, so the cold-boot +// build and the restore-time rebuild from the same OCI image agree (the guest +// resumes with the ext4 superblock cached in RAM, which must match the disk). +// +// Requires mkfs.ext4 (e2fsprogs) on PATH in the worker image. The image is +// recreated from scratch each call (reset-to-golden recreates it from the golden +// bundle), so any prior file at outPath is truncated. +// +// mkfs.ext4 -d copies srcDir's tree (perms, ownership, symlinks, xattrs) into the +// new filesystem without needing a loop mount or root's mount privileges — it +// writes the filesystem structures directly to the image file. +func BuildExt4Image(ctx context.Context, srcDir, outPath string) error { + if fi, err := os.Stat(srcDir); err != nil || !fi.IsDir() { + return fmt.Errorf("BuildExt4Image: source %q is not a directory: %v", srcDir, err) + } + sizeMiB, inodes, err := rootfsDiskGeometry(srcDir) + if err != nil { + return fmt.Errorf("BuildExt4Image: sizing from %q: %w", srcDir, err) + } + + // Truncate to size first so mkfs writes into a sparse file of the right size + // (mkfs.ext4 also accepts a size argument, but a pre-sized file is unambiguous + // and keeps the on-disk size predictable for the snapshot config). + if err := os.Remove(outPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("BuildExt4Image: removing stale image %q: %w", outPath, err) + } + f, err := os.OpenFile(outPath, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return fmt.Errorf("BuildExt4Image: creating image %q: %w", outPath, err) + } + if err := f.Truncate(int64(sizeMiB) * 1024 * 1024); err != nil { + f.Close() + return fmt.Errorf("BuildExt4Image: sizing image %q: %w", outPath, err) + } + f.Close() + + // -F: don't prompt (operating on a regular file, not a block device). + // -q: quiet. -d: populate from srcDir. -N: fix the inode count to the tree's + // entries + slack (the default size-derived ratio can starve a file-heavy + // rootfs of inodes). -E lazy_*=0: write tables eagerly so the image is fully + // materialized (deterministic on-disk bytes, important for the reset-to-golden + // "verbatim copy" approach). -O ^has_journal: a reset-each-restore rootfs gains + // nothing from a journal and it adds nondeterminism. + args := []string{ + "-F", "-q", + "-N", strconv.FormatInt(inodes, 10), + "-E", "lazy_itable_init=0,lazy_journal_init=0", + "-O", "^has_journal", + "-d", srcDir, + outPath, + strconv.Itoa(sizeMiB) + "M", + } + cmd := exec.CommandContext(ctx, "mkfs.ext4", args...) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("BuildExt4Image: mkfs.ext4 %v: %w: %s", args, err, out) + } + return nil +} diff --git a/cmd/ateom-microvm/internal/kata/disk_test.go b/cmd/ateom-microvm/internal/kata/disk_test.go new file mode 100644 index 000000000..f06fb483b --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/disk_test.go @@ -0,0 +1,76 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRootfsDiskGeometry(t *testing.T) { + dir := t.TempDir() + if err := os.Mkdir(filepath.Join(dir, "sub"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "a"), make([]byte, 1<<20), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "sub", "b"), make([]byte, 2<<20), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink("a", filepath.Join(dir, "link")); err != nil { + t.Fatal(err) + } + // Entries the walk should count: the root dir, sub, a, sub/b, link = 5. + const wantEntries = 5 + + size1, inodes1, err := rootfsDiskGeometry(dir) + if err != nil { + t.Fatalf("rootfsDiskGeometry: %v", err) + } + + // Determinism is required: the cold-boot build and the restore-time rebuild + // must produce an identically-sized disk for the same tree. + size2, inodes2, err := rootfsDiskGeometry(dir) + if err != nil { + t.Fatal(err) + } + if size1 != size2 || inodes1 != inodes2 { + t.Errorf("non-deterministic geometry: (%d MiB, %d inodes) vs (%d MiB, %d inodes)", size1, inodes1, size2, inodes2) + } + + // Size must cover the ~3 MiB of contents plus the scratch reserve. + if floorMiB := rootfsDiskScratchBytes/(1<<20) + 3; size1 < floorMiB { + t.Errorf("size %d MiB below expected floor %d MiB", size1, floorMiB) + } + + // Inodes must cover every entry plus the reserve (so a file-heavy rootfs can + // still create files), and never fewer than the entries present. + if inodes1 < wantEntries { + t.Errorf("inodes %d < %d entries", inodes1, wantEntries) + } + if inodes1 < 8192 { + t.Errorf("inodes %d missing the fixed reserve", inodes1) + } +} + +func TestRootfsDiskGeometryMissingDir(t *testing.T) { + if _, _, err := rootfsDiskGeometry(filepath.Join(t.TempDir(), "does-not-exist")); err == nil { + t.Fatal("rootfsDiskGeometry on a missing dir: want error, got nil") + } +} diff --git a/cmd/ateom-microvm/internal/kata/kata.go b/cmd/ateom-microvm/internal/kata/kata.go new file mode 100644 index 000000000..f6008db23 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/kata.go @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package kata holds the helpers ateom uses to boot and drive a kata guest in a +// cloud-hypervisor micro-VM WITHOUT the kata shim: ateom boots cloud-hypervisor +// itself (see internal/ch), then drives the stock kata-agent over its +// hybrid-vsock ttrpc API (DialAgent / AgentClient) to create the sandbox and +// start the actor's container on a writable virtio-blk rootfs (StartBlkWorkload). +// +// It also renders the kata configuration.toml (for the agent kernel_params + +// guest sizing) from runtime-fetched assets (config.go), builds the actor's ext4 +// rootfs disk (BuildExt4Image), and sweeps leftover per-sandbox host-side state +// (CleanupSandboxState). +package kata + +import ( + "path/filepath" +) + +// vcVMDir is the per-sandbox runtime dir convention kata uses (it holds the +// cloud-hypervisor API socket and the hybrid-vsock socket). +const vcVMDir = "/run/vc/vm" + +// CLHSocketPath returns the default cloud-hypervisor API socket path for the +// sandbox with the given id (the per-sandbox runtime dir). ateom records the +// actual api-socket it launched the VMM on, but uses this as the fallback. +func CLHSocketPath(id string) string { + return filepath.Join(vcVMDir, id, "clh-api.sock") +} diff --git a/cmd/ateom-microvm/internal/kata/restore.go b/cmd/ateom-microvm/internal/kata/restore.go new file mode 100644 index 000000000..2dccbca8f --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/restore.go @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "path/filepath" +) + +// Per-sandbox runtime paths. The CH snapshot's config.json references the +// per-sandbox hybrid-vsock socket as an absolute path, so restore must recreate +// it (or rewrite config.json) for the sandbox id. + +// VMDir is the per-sandbox runtime dir (holds the cloud-hypervisor API socket and +// the hybrid-vsock socket). +func VMDir(id string) string { return filepath.Join(vcVMDir, id) } + +// VsockSocketPath is the hybrid-vsock socket the CH snapshot's vsock device +// references; CH recreates the listener here on restore. +func VsockSocketPath(id string) string { return filepath.Join(VMDir(id), "clh.sock") } diff --git a/cmd/ateom-microvm/internal/kata/specconv.go b/cmd/ateom-microvm/internal/kata/specconv.go new file mode 100644 index 000000000..4ac75f992 --- /dev/null +++ b/cmd/ateom-microvm/internal/kata/specconv.go @@ -0,0 +1,178 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kata + +import ( + "context" + "fmt" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// StartBlkWorkload starts the actor container with its rootfs backed by a single +// boot-time virtio-blk disk (devPath, e.g. "/dev/vdb") — the virtio-blk-rootfs +// path. There is NO overlay, NO virtio-fs, NO tmpfs upper: the agent direct-mounts +// devPath (ext4) as the container rootfs, so rootfs writes land on the host-backed +// disk file (off guest RAM) and the CH snapshot stays memory-only with no balloon. +// +// One "blk" storage: source is the /dev node (kata's block storage handler mounts +// it directly when source starts with /dev — no uevent/auto-enumeration wait, +// unlike a hotplugged disk), fstype ext4, mounted at the container rootfs path. +// The spec's Root.Path is set to that mount point, which the agent's setup_bundle +// then uses as the container root. +func (a *AgentClient) StartBlkWorkload(ctx context.Context, containerID, devPath string, spec *specs.Spec) error { + rootfs := "/run/kata-containers/" + containerID + "/rootfs" + storages := []*agentpb.Storage{ + { + Driver: "blk", + Source: devPath, + Fstype: "ext4", + MountPoint: rootfs, + Options: []string{"rw"}, + }, + } + + pbSpec := SpecToAgentPB(spec) + pbSpec.Root = &agentpb.Root{Path: rootfs, Readonly: false} + + if err := a.CreateContainer(ctx, &agentpb.CreateContainerRequest{ + ContainerId: containerID, + ExecId: containerID, + Storages: storages, + OCI: pbSpec, + }); err != nil { + return fmt.Errorf("creating blk workload %q: %w", containerID, err) + } + if err := a.StartContainer(ctx, containerID); err != nil { + return fmt.Errorf("starting blk workload %q: %w", containerID, err) + } + return nil +} + +// SpecToAgentPB converts an OCI runtime spec into the kata-agent's protobuf Spec +// (agentpb.Spec) for a CreateContainer ttrpc call. The shim normally does this +// conversion; ateom does it itself when it drives the agent directly ("be your +// own hook scheduler"). A blind json round-trip does NOT work: agentpb's Spec +// JSON tags are PascalCase (from oci.proto), while OCI config.json is lowercase. +// +// Only the fields the kata-agent needs to create + start a container are mapped +// (process, root, mounts, linux namespaces/resources/cgroup/masked+readonly +// paths). The container rootfs is provided out-of-band as storages; the caller +// is expected to set the returned spec's Root.Path to the overlay mount point. +func SpecToAgentPB(s *specs.Spec) *agentpb.Spec { + if s == nil { + return nil + } + out := &agentpb.Spec{ + Version: s.Version, + Hostname: s.Hostname, + Annotations: s.Annotations, + } + + if s.Process != nil { + p := &agentpb.Process{ + Args: s.Process.Args, + Env: s.Process.Env, + Cwd: s.Process.Cwd, + NoNewPrivileges: s.Process.NoNewPrivileges, + User: &agentpb.User{ + UID: s.Process.User.UID, + GID: s.Process.User.GID, + AdditionalGids: s.Process.User.AdditionalGids, + Username: s.Process.User.Username, + }, + } + if c := s.Process.Capabilities; c != nil { + p.Capabilities = &agentpb.LinuxCapabilities{ + Bounding: c.Bounding, + Effective: c.Effective, + Inheritable: c.Inheritable, + Permitted: c.Permitted, + Ambient: c.Ambient, + } + } + for _, rl := range s.Process.Rlimits { + p.Rlimits = append(p.Rlimits, &agentpb.POSIXRlimit{ + Type: rl.Type, Hard: rl.Hard, Soft: rl.Soft, + }) + } + out.Process = p + } + + if s.Root != nil { + out.Root = &agentpb.Root{Path: s.Root.Path, Readonly: s.Root.Readonly} + } + + for _, m := range s.Mounts { + out.Mounts = append(out.Mounts, &agentpb.Mount{ + Destination: m.Destination, + Source: m.Source, + Type: m.Type, + Options: m.Options, + }) + } + + if s.Linux != nil { + l := &agentpb.Linux{ + CgroupsPath: s.Linux.CgroupsPath, + MaskedPaths: s.Linux.MaskedPaths, + ReadonlyPaths: s.Linux.ReadonlyPaths, + } + // TODO: forward the remaining OCI security knobs the kata-agent supports + // for parity with the OCI spec — Linux.Seccomp and Linux.Sysctl here, and + // Process.ApparmorProfile / Process.SelinuxLabel above. The MVP runs the + // actor with kata's defaults for these. + for _, ns := range s.Linux.Namespaces { + // Mirror the kata shim (kata_agent.go constrainGRPCSpec): the + // network/cgroup/time namespaces are handled on the host / unsupported + // in the guest agent, so DROP them (dropping the network ns makes the + // container share the guest sandbox network = eth0/actor IP). Every + // other namespace's host Path MUST be emptied, else the agent tries to + // join a host namespace path inside the guest and fails ENOENT. + switch ns.Type { + case specs.NetworkNamespace, specs.CgroupNamespace, specs.TimeNamespace: + continue + } + l.Namespaces = append(l.Namespaces, &agentpb.LinuxNamespace{Type: string(ns.Type)}) + } + if r := s.Linux.Resources; r != nil { + res := &agentpb.LinuxResources{} + for _, d := range r.Devices { + dc := &agentpb.LinuxDeviceCgroup{Allow: d.Allow, Type: d.Type, Access: d.Access} + if d.Major != nil { + dc.Major = *d.Major + } + if d.Minor != nil { + dc.Minor = *d.Minor + } + res.Devices = append(res.Devices, dc) + } + if r.CPU != nil { + cpu := &agentpb.LinuxCPU{} + if r.CPU.Shares != nil { + cpu.Shares = *r.CPU.Shares + } + res.CPU = cpu + } + l.Resources = res + } + out.Linux = l + } + + return out +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/LICENSE b/cmd/ateom-microvm/internal/third_party/kata/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cmd/ateom-microvm/internal/third_party/kata/PROVENANCE.md b/cmd/ateom-microvm/internal/third_party/kata/PROVENANCE.md new file mode 100644 index 000000000..c35f42bca --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/PROVENANCE.md @@ -0,0 +1,52 @@ +# third_party/kata — vendored kata-containers sources + +Source copied from [kata-containers](https://github.com/kata-containers/kata-containers) +and used by `cmd/ateom-microvm`. The upstream `LICENSE` in this directory covers +everything under it. + +- **Upstream:** github.com/kata-containers/kata-containers +- **Version:** tag `3.31.0` (matches the kata runtime assets the micro-VM demo fetches; + see `hack/microvm-assets/assemble.sh` `KATA_VER`) +- **License:** Apache-2.0 — `./LICENSE` is the upstream license verbatim (mirrored to + `LICENSES/third_party/kata/LICENSE` by `hack/update/licenses.sh`); the per-file + copyright headers (HyperHQ, Ant Group, Intel, Databricks) are retained verbatim in + each source file. + +## agentpb — kata-agent ttrpc protobufs + +A copy of the kata-agent protocol-buffer API, used to drive the kata-agent over ttrpc +when ateom boots the guest. + +- **Path:** `src/libs/protocols/protos/{agent,oci,types,csi}.proto` + +The `.proto` files are byte-identical to the 3.31.0 release except for a single line: +`option go_package` is repointed to this package's import path so the generated Go +lands in-tree. + +### Why a copy instead of a module dependency + +kata-containers is a large module; adding it to `go.mod` to use a handful of message +types would pull a heavy, unrelated dependency tree into the main module. We instead +vendor just these four `.proto` files and their generated Go. + +### Generated code + +The `*.pb.go` files were generated with `protoc-gen-go v1.36.11-devel` and +`protoc v4.25.3` (see each file's header). There are intentionally **no** generated +ttrpc service stubs: `internal/kata/agentclient.go` calls the agent by string method +name via `ttrpc.Client.Call(ctx, "grpc.AgentService", "", req, resp)`, so only +the message types are needed. + +### RPCs actually used + +ateom drives a small subset of `AgentService`: +`CreateSandbox`, `CreateContainer`, `StartContainer`, `UpdateInterface`, `UpdateRoutes`, +`AddARPNeighbors`, `ReadStdout`, `ReadStderr`. + +### Regenerating + +1. Check out the matching kata-containers tag and copy + `src/libs/protocols/protos/{agent,oci,types,csi}.proto` into `agentpb/`. +2. Set each `option go_package` to + `github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb;agentpb`. +3. Regenerate with `protoc --go_out=...` (protoc-gen-go), matching the versions above. diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.pb.go b/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.pb.go new file mode 100644 index 000000000..bb578f792 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.pb.go @@ -0,0 +1,4958 @@ +// +// Copyright 2017 HyperHQ Inc. +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11-devel +// protoc v4.25.3 +// source: agent.proto + +package agentpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + StringUser *StringUser `protobuf:"bytes,3,opt,name=string_user,json=stringUser,proto3" json:"string_user,omitempty"` + Devices []*Device `protobuf:"bytes,4,rep,name=devices,proto3" json:"devices,omitempty"` + Storages []*Storage `protobuf:"bytes,5,rep,name=storages,proto3" json:"storages,omitempty"` + OCI *Spec `protobuf:"bytes,6,opt,name=OCI,proto3" json:"OCI,omitempty"` + // This field is used to indicate if the container needs to join + // sandbox shared pid ns or create a new namespace. This field is + // meant to override the NEWPID config settings in the OCI spec. + // The agent would receive an OCI spec with PID namespace cleared + // out altogether and not just the pid ns path. + SandboxPidns bool `protobuf:"varint,7,opt,name=sandbox_pidns,json=sandboxPidns,proto3" json:"sandbox_pidns,omitempty"` + // This field is used to declare a set of shared mount points + // that support cross-container sharing of mount objects. + SharedMounts []*SharedMount `protobuf:"bytes,8,rep,name=shared_mounts,json=sharedMounts,proto3" json:"shared_mounts,omitempty"` + // These fields are the host-side vport numbers of passfd streams + // pre-created by runtime-rs, and used as identifiers for the agent + // to select the right streams for init process's stdin/stdout/stderr. + // Disable the feature by setting the associated port to 0. + StdinPort uint32 `protobuf:"varint,9,opt,name=stdin_port,json=stdinPort,proto3" json:"stdin_port,omitempty"` + StdoutPort uint32 `protobuf:"varint,10,opt,name=stdout_port,json=stdoutPort,proto3" json:"stdout_port,omitempty"` + StderrPort uint32 `protobuf:"varint,11,opt,name=stderr_port,json=stderrPort,proto3" json:"stderr_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateContainerRequest) Reset() { + *x = CreateContainerRequest{} + mi := &file_agent_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateContainerRequest) ProtoMessage() {} + +func (x *CreateContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateContainerRequest.ProtoReflect.Descriptor instead. +func (*CreateContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *CreateContainerRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *CreateContainerRequest) GetStringUser() *StringUser { + if x != nil { + return x.StringUser + } + return nil +} + +func (x *CreateContainerRequest) GetDevices() []*Device { + if x != nil { + return x.Devices + } + return nil +} + +func (x *CreateContainerRequest) GetStorages() []*Storage { + if x != nil { + return x.Storages + } + return nil +} + +func (x *CreateContainerRequest) GetOCI() *Spec { + if x != nil { + return x.OCI + } + return nil +} + +func (x *CreateContainerRequest) GetSandboxPidns() bool { + if x != nil { + return x.SandboxPidns + } + return false +} + +func (x *CreateContainerRequest) GetSharedMounts() []*SharedMount { + if x != nil { + return x.SharedMounts + } + return nil +} + +func (x *CreateContainerRequest) GetStdinPort() uint32 { + if x != nil { + return x.StdinPort + } + return 0 +} + +func (x *CreateContainerRequest) GetStdoutPort() uint32 { + if x != nil { + return x.StdoutPort + } + return 0 +} + +func (x *CreateContainerRequest) GetStderrPort() uint32 { + if x != nil { + return x.StderrPort + } + return 0 +} + +type StartContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartContainerRequest) Reset() { + *x = StartContainerRequest{} + mi := &file_agent_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartContainerRequest) ProtoMessage() {} + +func (x *StartContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StartContainerRequest.ProtoReflect.Descriptor instead. +func (*StartContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{1} +} + +func (x *StartContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type RemoveContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + // RemoveContainer will return an error if + // it could not kill some container processes + // after timeout seconds. + // Setting timeout to 0 means RemoveContainer will + // wait for ever. + Timeout uint32 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveContainerRequest) Reset() { + *x = RemoveContainerRequest{} + mi := &file_agent_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveContainerRequest) ProtoMessage() {} + +func (x *RemoveContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveContainerRequest.ProtoReflect.Descriptor instead. +func (*RemoveContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{2} +} + +func (x *RemoveContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *RemoveContainerRequest) GetTimeout() uint32 { + if x != nil { + return x.Timeout + } + return 0 +} + +type ExecProcessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + StringUser *StringUser `protobuf:"bytes,3,opt,name=string_user,json=stringUser,proto3" json:"string_user,omitempty"` + Process *Process `protobuf:"bytes,4,opt,name=process,proto3" json:"process,omitempty"` + // These fields are the host-side vport numbers of passfd streams + // pre-created by runtime-rs, and used as identifiers for the agent + // to select the right streams for process's stdin/stdout/stderr. + // Disable the feature by setting the associated port to 0. + StdinPort uint32 `protobuf:"varint,5,opt,name=stdin_port,json=stdinPort,proto3" json:"stdin_port,omitempty"` + StdoutPort uint32 `protobuf:"varint,6,opt,name=stdout_port,json=stdoutPort,proto3" json:"stdout_port,omitempty"` + StderrPort uint32 `protobuf:"varint,7,opt,name=stderr_port,json=stderrPort,proto3" json:"stderr_port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecProcessRequest) Reset() { + *x = ExecProcessRequest{} + mi := &file_agent_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecProcessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecProcessRequest) ProtoMessage() {} + +func (x *ExecProcessRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecProcessRequest.ProtoReflect.Descriptor instead. +func (*ExecProcessRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{3} +} + +func (x *ExecProcessRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *ExecProcessRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *ExecProcessRequest) GetStringUser() *StringUser { + if x != nil { + return x.StringUser + } + return nil +} + +func (x *ExecProcessRequest) GetProcess() *Process { + if x != nil { + return x.Process + } + return nil +} + +func (x *ExecProcessRequest) GetStdinPort() uint32 { + if x != nil { + return x.StdinPort + } + return 0 +} + +func (x *ExecProcessRequest) GetStdoutPort() uint32 { + if x != nil { + return x.StdoutPort + } + return 0 +} + +func (x *ExecProcessRequest) GetStderrPort() uint32 { + if x != nil { + return x.StderrPort + } + return 0 +} + +type SignalProcessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + // Special case for SignalProcess(): exec_id can be empty(""), + // which means to send the signal to all the processes including their descendants. + // Other APIs with exec_id should treat empty exec_id as an invalid request. + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + Signal uint32 `protobuf:"varint,3,opt,name=signal,proto3" json:"signal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalProcessRequest) Reset() { + *x = SignalProcessRequest{} + mi := &file_agent_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalProcessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalProcessRequest) ProtoMessage() {} + +func (x *SignalProcessRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignalProcessRequest.ProtoReflect.Descriptor instead. +func (*SignalProcessRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{4} +} + +func (x *SignalProcessRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *SignalProcessRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *SignalProcessRequest) GetSignal() uint32 { + if x != nil { + return x.Signal + } + return 0 +} + +type WaitProcessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WaitProcessRequest) Reset() { + *x = WaitProcessRequest{} + mi := &file_agent_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WaitProcessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WaitProcessRequest) ProtoMessage() {} + +func (x *WaitProcessRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WaitProcessRequest.ProtoReflect.Descriptor instead. +func (*WaitProcessRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{5} +} + +func (x *WaitProcessRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *WaitProcessRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +type WaitProcessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status int32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WaitProcessResponse) Reset() { + *x = WaitProcessResponse{} + mi := &file_agent_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WaitProcessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WaitProcessResponse) ProtoMessage() {} + +func (x *WaitProcessResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WaitProcessResponse.ProtoReflect.Descriptor instead. +func (*WaitProcessResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{6} +} + +func (x *WaitProcessResponse) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +type UpdateContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + Resources *LinuxResources `protobuf:"bytes,2,opt,name=resources,proto3" json:"resources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateContainerRequest) Reset() { + *x = UpdateContainerRequest{} + mi := &file_agent_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateContainerRequest) ProtoMessage() {} + +func (x *UpdateContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateContainerRequest.ProtoReflect.Descriptor instead. +func (*UpdateContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *UpdateContainerRequest) GetResources() *LinuxResources { + if x != nil { + return x.Resources + } + return nil +} + +type StatsContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatsContainerRequest) Reset() { + *x = StatsContainerRequest{} + mi := &file_agent_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatsContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsContainerRequest) ProtoMessage() {} + +func (x *StatsContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsContainerRequest.ProtoReflect.Descriptor instead. +func (*StatsContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{8} +} + +func (x *StatsContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type PauseContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PauseContainerRequest) Reset() { + *x = PauseContainerRequest{} + mi := &file_agent_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PauseContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PauseContainerRequest) ProtoMessage() {} + +func (x *PauseContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PauseContainerRequest.ProtoReflect.Descriptor instead. +func (*PauseContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{9} +} + +func (x *PauseContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type ResumeContainerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResumeContainerRequest) Reset() { + *x = ResumeContainerRequest{} + mi := &file_agent_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResumeContainerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResumeContainerRequest) ProtoMessage() {} + +func (x *ResumeContainerRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResumeContainerRequest.ProtoReflect.Descriptor instead. +func (*ResumeContainerRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{10} +} + +func (x *ResumeContainerRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type CpuUsage struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalUsage uint64 `protobuf:"varint,1,opt,name=total_usage,json=totalUsage,proto3" json:"total_usage,omitempty"` + PercpuUsage []uint64 `protobuf:"varint,2,rep,packed,name=percpu_usage,json=percpuUsage,proto3" json:"percpu_usage,omitempty"` + UsageInKernelmode uint64 `protobuf:"varint,3,opt,name=usage_in_kernelmode,json=usageInKernelmode,proto3" json:"usage_in_kernelmode,omitempty"` + UsageInUsermode uint64 `protobuf:"varint,4,opt,name=usage_in_usermode,json=usageInUsermode,proto3" json:"usage_in_usermode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CpuUsage) Reset() { + *x = CpuUsage{} + mi := &file_agent_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CpuUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CpuUsage) ProtoMessage() {} + +func (x *CpuUsage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CpuUsage.ProtoReflect.Descriptor instead. +func (*CpuUsage) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{11} +} + +func (x *CpuUsage) GetTotalUsage() uint64 { + if x != nil { + return x.TotalUsage + } + return 0 +} + +func (x *CpuUsage) GetPercpuUsage() []uint64 { + if x != nil { + return x.PercpuUsage + } + return nil +} + +func (x *CpuUsage) GetUsageInKernelmode() uint64 { + if x != nil { + return x.UsageInKernelmode + } + return 0 +} + +func (x *CpuUsage) GetUsageInUsermode() uint64 { + if x != nil { + return x.UsageInUsermode + } + return 0 +} + +type ThrottlingData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Periods uint64 `protobuf:"varint,1,opt,name=periods,proto3" json:"periods,omitempty"` + ThrottledPeriods uint64 `protobuf:"varint,2,opt,name=throttled_periods,json=throttledPeriods,proto3" json:"throttled_periods,omitempty"` + ThrottledTime uint64 `protobuf:"varint,3,opt,name=throttled_time,json=throttledTime,proto3" json:"throttled_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ThrottlingData) Reset() { + *x = ThrottlingData{} + mi := &file_agent_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ThrottlingData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThrottlingData) ProtoMessage() {} + +func (x *ThrottlingData) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThrottlingData.ProtoReflect.Descriptor instead. +func (*ThrottlingData) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{12} +} + +func (x *ThrottlingData) GetPeriods() uint64 { + if x != nil { + return x.Periods + } + return 0 +} + +func (x *ThrottlingData) GetThrottledPeriods() uint64 { + if x != nil { + return x.ThrottledPeriods + } + return 0 +} + +func (x *ThrottlingData) GetThrottledTime() uint64 { + if x != nil { + return x.ThrottledTime + } + return 0 +} + +type CpuStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuUsage *CpuUsage `protobuf:"bytes,1,opt,name=cpu_usage,json=cpuUsage,proto3" json:"cpu_usage,omitempty"` + ThrottlingData *ThrottlingData `protobuf:"bytes,2,opt,name=throttling_data,json=throttlingData,proto3" json:"throttling_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CpuStats) Reset() { + *x = CpuStats{} + mi := &file_agent_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CpuStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CpuStats) ProtoMessage() {} + +func (x *CpuStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CpuStats.ProtoReflect.Descriptor instead. +func (*CpuStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{13} +} + +func (x *CpuStats) GetCpuUsage() *CpuUsage { + if x != nil { + return x.CpuUsage + } + return nil +} + +func (x *CpuStats) GetThrottlingData() *ThrottlingData { + if x != nil { + return x.ThrottlingData + } + return nil +} + +type PidsStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Current uint64 `protobuf:"varint,1,opt,name=current,proto3" json:"current,omitempty"` + Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PidsStats) Reset() { + *x = PidsStats{} + mi := &file_agent_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PidsStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PidsStats) ProtoMessage() {} + +func (x *PidsStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PidsStats.ProtoReflect.Descriptor instead. +func (*PidsStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{14} +} + +func (x *PidsStats) GetCurrent() uint64 { + if x != nil { + return x.Current + } + return 0 +} + +func (x *PidsStats) GetLimit() uint64 { + if x != nil { + return x.Limit + } + return 0 +} + +type MemoryData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Usage uint64 `protobuf:"varint,1,opt,name=usage,proto3" json:"usage,omitempty"` + MaxUsage uint64 `protobuf:"varint,2,opt,name=max_usage,json=maxUsage,proto3" json:"max_usage,omitempty"` + Failcnt uint64 `protobuf:"varint,3,opt,name=failcnt,proto3" json:"failcnt,omitempty"` + Limit uint64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryData) Reset() { + *x = MemoryData{} + mi := &file_agent_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryData) ProtoMessage() {} + +func (x *MemoryData) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryData.ProtoReflect.Descriptor instead. +func (*MemoryData) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{15} +} + +func (x *MemoryData) GetUsage() uint64 { + if x != nil { + return x.Usage + } + return 0 +} + +func (x *MemoryData) GetMaxUsage() uint64 { + if x != nil { + return x.MaxUsage + } + return 0 +} + +func (x *MemoryData) GetFailcnt() uint64 { + if x != nil { + return x.Failcnt + } + return 0 +} + +func (x *MemoryData) GetLimit() uint64 { + if x != nil { + return x.Limit + } + return 0 +} + +type MemoryStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cache uint64 `protobuf:"varint,1,opt,name=cache,proto3" json:"cache,omitempty"` + Usage *MemoryData `protobuf:"bytes,2,opt,name=usage,proto3" json:"usage,omitempty"` + SwapUsage *MemoryData `protobuf:"bytes,3,opt,name=swap_usage,json=swapUsage,proto3" json:"swap_usage,omitempty"` + KernelUsage *MemoryData `protobuf:"bytes,4,opt,name=kernel_usage,json=kernelUsage,proto3" json:"kernel_usage,omitempty"` + UseHierarchy bool `protobuf:"varint,5,opt,name=use_hierarchy,json=useHierarchy,proto3" json:"use_hierarchy,omitempty"` + Stats map[string]uint64 `protobuf:"bytes,6,rep,name=stats,proto3" json:"stats,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryStats) Reset() { + *x = MemoryStats{} + mi := &file_agent_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryStats) ProtoMessage() {} + +func (x *MemoryStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoryStats.ProtoReflect.Descriptor instead. +func (*MemoryStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{16} +} + +func (x *MemoryStats) GetCache() uint64 { + if x != nil { + return x.Cache + } + return 0 +} + +func (x *MemoryStats) GetUsage() *MemoryData { + if x != nil { + return x.Usage + } + return nil +} + +func (x *MemoryStats) GetSwapUsage() *MemoryData { + if x != nil { + return x.SwapUsage + } + return nil +} + +func (x *MemoryStats) GetKernelUsage() *MemoryData { + if x != nil { + return x.KernelUsage + } + return nil +} + +func (x *MemoryStats) GetUseHierarchy() bool { + if x != nil { + return x.UseHierarchy + } + return false +} + +func (x *MemoryStats) GetStats() map[string]uint64 { + if x != nil { + return x.Stats + } + return nil +} + +type BlkioStatsEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Major uint64 `protobuf:"varint,1,opt,name=major,proto3" json:"major,omitempty"` + Minor uint64 `protobuf:"varint,2,opt,name=minor,proto3" json:"minor,omitempty"` + Op string `protobuf:"bytes,3,opt,name=op,proto3" json:"op,omitempty"` + Value uint64 `protobuf:"varint,4,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlkioStatsEntry) Reset() { + *x = BlkioStatsEntry{} + mi := &file_agent_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlkioStatsEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlkioStatsEntry) ProtoMessage() {} + +func (x *BlkioStatsEntry) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlkioStatsEntry.ProtoReflect.Descriptor instead. +func (*BlkioStatsEntry) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{17} +} + +func (x *BlkioStatsEntry) GetMajor() uint64 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *BlkioStatsEntry) GetMinor() uint64 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *BlkioStatsEntry) GetOp() string { + if x != nil { + return x.Op + } + return "" +} + +func (x *BlkioStatsEntry) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +type BlkioStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + IoServiceBytesRecursive []*BlkioStatsEntry `protobuf:"bytes,1,rep,name=io_service_bytes_recursive,json=ioServiceBytesRecursive,proto3" json:"io_service_bytes_recursive,omitempty"` // number of bytes transferred to and from the block device + IoServicedRecursive []*BlkioStatsEntry `protobuf:"bytes,2,rep,name=io_serviced_recursive,json=ioServicedRecursive,proto3" json:"io_serviced_recursive,omitempty"` + IoQueuedRecursive []*BlkioStatsEntry `protobuf:"bytes,3,rep,name=io_queued_recursive,json=ioQueuedRecursive,proto3" json:"io_queued_recursive,omitempty"` + IoServiceTimeRecursive []*BlkioStatsEntry `protobuf:"bytes,4,rep,name=io_service_time_recursive,json=ioServiceTimeRecursive,proto3" json:"io_service_time_recursive,omitempty"` + IoWaitTimeRecursive []*BlkioStatsEntry `protobuf:"bytes,5,rep,name=io_wait_time_recursive,json=ioWaitTimeRecursive,proto3" json:"io_wait_time_recursive,omitempty"` + IoMergedRecursive []*BlkioStatsEntry `protobuf:"bytes,6,rep,name=io_merged_recursive,json=ioMergedRecursive,proto3" json:"io_merged_recursive,omitempty"` + IoTimeRecursive []*BlkioStatsEntry `protobuf:"bytes,7,rep,name=io_time_recursive,json=ioTimeRecursive,proto3" json:"io_time_recursive,omitempty"` + SectorsRecursive []*BlkioStatsEntry `protobuf:"bytes,8,rep,name=sectors_recursive,json=sectorsRecursive,proto3" json:"sectors_recursive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlkioStats) Reset() { + *x = BlkioStats{} + mi := &file_agent_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlkioStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlkioStats) ProtoMessage() {} + +func (x *BlkioStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlkioStats.ProtoReflect.Descriptor instead. +func (*BlkioStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{18} +} + +func (x *BlkioStats) GetIoServiceBytesRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoServiceBytesRecursive + } + return nil +} + +func (x *BlkioStats) GetIoServicedRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoServicedRecursive + } + return nil +} + +func (x *BlkioStats) GetIoQueuedRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoQueuedRecursive + } + return nil +} + +func (x *BlkioStats) GetIoServiceTimeRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoServiceTimeRecursive + } + return nil +} + +func (x *BlkioStats) GetIoWaitTimeRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoWaitTimeRecursive + } + return nil +} + +func (x *BlkioStats) GetIoMergedRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoMergedRecursive + } + return nil +} + +func (x *BlkioStats) GetIoTimeRecursive() []*BlkioStatsEntry { + if x != nil { + return x.IoTimeRecursive + } + return nil +} + +func (x *BlkioStats) GetSectorsRecursive() []*BlkioStatsEntry { + if x != nil { + return x.SectorsRecursive + } + return nil +} + +type HugetlbStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Usage uint64 `protobuf:"varint,1,opt,name=usage,proto3" json:"usage,omitempty"` + MaxUsage uint64 `protobuf:"varint,2,opt,name=max_usage,json=maxUsage,proto3" json:"max_usage,omitempty"` + Failcnt uint64 `protobuf:"varint,3,opt,name=failcnt,proto3" json:"failcnt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HugetlbStats) Reset() { + *x = HugetlbStats{} + mi := &file_agent_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HugetlbStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HugetlbStats) ProtoMessage() {} + +func (x *HugetlbStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HugetlbStats.ProtoReflect.Descriptor instead. +func (*HugetlbStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{19} +} + +func (x *HugetlbStats) GetUsage() uint64 { + if x != nil { + return x.Usage + } + return 0 +} + +func (x *HugetlbStats) GetMaxUsage() uint64 { + if x != nil { + return x.MaxUsage + } + return 0 +} + +func (x *HugetlbStats) GetFailcnt() uint64 { + if x != nil { + return x.Failcnt + } + return 0 +} + +type CgroupStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + CpuStats *CpuStats `protobuf:"bytes,1,opt,name=cpu_stats,json=cpuStats,proto3" json:"cpu_stats,omitempty"` + MemoryStats *MemoryStats `protobuf:"bytes,2,opt,name=memory_stats,json=memoryStats,proto3" json:"memory_stats,omitempty"` + PidsStats *PidsStats `protobuf:"bytes,3,opt,name=pids_stats,json=pidsStats,proto3" json:"pids_stats,omitempty"` + BlkioStats *BlkioStats `protobuf:"bytes,4,opt,name=blkio_stats,json=blkioStats,proto3" json:"blkio_stats,omitempty"` + HugetlbStats map[string]*HugetlbStats `protobuf:"bytes,5,rep,name=hugetlb_stats,json=hugetlbStats,proto3" json:"hugetlb_stats,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // the map is in the format "size of hugepage: stats of the hugepage" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CgroupStats) Reset() { + *x = CgroupStats{} + mi := &file_agent_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CgroupStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CgroupStats) ProtoMessage() {} + +func (x *CgroupStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CgroupStats.ProtoReflect.Descriptor instead. +func (*CgroupStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{20} +} + +func (x *CgroupStats) GetCpuStats() *CpuStats { + if x != nil { + return x.CpuStats + } + return nil +} + +func (x *CgroupStats) GetMemoryStats() *MemoryStats { + if x != nil { + return x.MemoryStats + } + return nil +} + +func (x *CgroupStats) GetPidsStats() *PidsStats { + if x != nil { + return x.PidsStats + } + return nil +} + +func (x *CgroupStats) GetBlkioStats() *BlkioStats { + if x != nil { + return x.BlkioStats + } + return nil +} + +func (x *CgroupStats) GetHugetlbStats() map[string]*HugetlbStats { + if x != nil { + return x.HugetlbStats + } + return nil +} + +type NetworkStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + RxBytes uint64 `protobuf:"varint,2,opt,name=rx_bytes,json=rxBytes,proto3" json:"rx_bytes,omitempty"` + RxPackets uint64 `protobuf:"varint,3,opt,name=rx_packets,json=rxPackets,proto3" json:"rx_packets,omitempty"` + RxErrors uint64 `protobuf:"varint,4,opt,name=rx_errors,json=rxErrors,proto3" json:"rx_errors,omitempty"` + RxDropped uint64 `protobuf:"varint,5,opt,name=rx_dropped,json=rxDropped,proto3" json:"rx_dropped,omitempty"` + TxBytes uint64 `protobuf:"varint,6,opt,name=tx_bytes,json=txBytes,proto3" json:"tx_bytes,omitempty"` + TxPackets uint64 `protobuf:"varint,7,opt,name=tx_packets,json=txPackets,proto3" json:"tx_packets,omitempty"` + TxErrors uint64 `protobuf:"varint,8,opt,name=tx_errors,json=txErrors,proto3" json:"tx_errors,omitempty"` + TxDropped uint64 `protobuf:"varint,9,opt,name=tx_dropped,json=txDropped,proto3" json:"tx_dropped,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkStats) Reset() { + *x = NetworkStats{} + mi := &file_agent_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkStats) ProtoMessage() {} + +func (x *NetworkStats) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkStats.ProtoReflect.Descriptor instead. +func (*NetworkStats) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{21} +} + +func (x *NetworkStats) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NetworkStats) GetRxBytes() uint64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *NetworkStats) GetRxPackets() uint64 { + if x != nil { + return x.RxPackets + } + return 0 +} + +func (x *NetworkStats) GetRxErrors() uint64 { + if x != nil { + return x.RxErrors + } + return 0 +} + +func (x *NetworkStats) GetRxDropped() uint64 { + if x != nil { + return x.RxDropped + } + return 0 +} + +func (x *NetworkStats) GetTxBytes() uint64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *NetworkStats) GetTxPackets() uint64 { + if x != nil { + return x.TxPackets + } + return 0 +} + +func (x *NetworkStats) GetTxErrors() uint64 { + if x != nil { + return x.TxErrors + } + return 0 +} + +func (x *NetworkStats) GetTxDropped() uint64 { + if x != nil { + return x.TxDropped + } + return 0 +} + +type StatsContainerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + CgroupStats *CgroupStats `protobuf:"bytes,1,opt,name=cgroup_stats,json=cgroupStats,proto3" json:"cgroup_stats,omitempty"` + NetworkStats []*NetworkStats `protobuf:"bytes,2,rep,name=network_stats,json=networkStats,proto3" json:"network_stats,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatsContainerResponse) Reset() { + *x = StatsContainerResponse{} + mi := &file_agent_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatsContainerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsContainerResponse) ProtoMessage() {} + +func (x *StatsContainerResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsContainerResponse.ProtoReflect.Descriptor instead. +func (*StatsContainerResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{22} +} + +func (x *StatsContainerResponse) GetCgroupStats() *CgroupStats { + if x != nil { + return x.CgroupStats + } + return nil +} + +func (x *StatsContainerResponse) GetNetworkStats() []*NetworkStats { + if x != nil { + return x.NetworkStats + } + return nil +} + +type WriteStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteStreamRequest) Reset() { + *x = WriteStreamRequest{} + mi := &file_agent_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteStreamRequest) ProtoMessage() {} + +func (x *WriteStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteStreamRequest.ProtoReflect.Descriptor instead. +func (*WriteStreamRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{23} +} + +func (x *WriteStreamRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *WriteStreamRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *WriteStreamRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type WriteStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Len uint32 `protobuf:"varint,1,opt,name=len,proto3" json:"len,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteStreamResponse) Reset() { + *x = WriteStreamResponse{} + mi := &file_agent_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteStreamResponse) ProtoMessage() {} + +func (x *WriteStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WriteStreamResponse.ProtoReflect.Descriptor instead. +func (*WriteStreamResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{24} +} + +func (x *WriteStreamResponse) GetLen() uint32 { + if x != nil { + return x.Len + } + return 0 +} + +type ReadStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + Len uint32 `protobuf:"varint,3,opt,name=len,proto3" json:"len,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadStreamRequest) Reset() { + *x = ReadStreamRequest{} + mi := &file_agent_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadStreamRequest) ProtoMessage() {} + +func (x *ReadStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadStreamRequest.ProtoReflect.Descriptor instead. +func (*ReadStreamRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{25} +} + +func (x *ReadStreamRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *ReadStreamRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *ReadStreamRequest) GetLen() uint32 { + if x != nil { + return x.Len + } + return 0 +} + +type ReadStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadStreamResponse) Reset() { + *x = ReadStreamResponse{} + mi := &file_agent_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadStreamResponse) ProtoMessage() {} + +func (x *ReadStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadStreamResponse.ProtoReflect.Descriptor instead. +func (*ReadStreamResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{26} +} + +func (x *ReadStreamResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type CloseStdinRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStdinRequest) Reset() { + *x = CloseStdinRequest{} + mi := &file_agent_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStdinRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStdinRequest) ProtoMessage() {} + +func (x *CloseStdinRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStdinRequest.ProtoReflect.Descriptor instead. +func (*CloseStdinRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{27} +} + +func (x *CloseStdinRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *CloseStdinRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +type TtyWinResizeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ExecId string `protobuf:"bytes,2,opt,name=exec_id,json=execId,proto3" json:"exec_id,omitempty"` + Row uint32 `protobuf:"varint,3,opt,name=row,proto3" json:"row,omitempty"` + Column uint32 `protobuf:"varint,4,opt,name=column,proto3" json:"column,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TtyWinResizeRequest) Reset() { + *x = TtyWinResizeRequest{} + mi := &file_agent_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TtyWinResizeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TtyWinResizeRequest) ProtoMessage() {} + +func (x *TtyWinResizeRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TtyWinResizeRequest.ProtoReflect.Descriptor instead. +func (*TtyWinResizeRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{28} +} + +func (x *TtyWinResizeRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *TtyWinResizeRequest) GetExecId() string { + if x != nil { + return x.ExecId + } + return "" +} + +func (x *TtyWinResizeRequest) GetRow() uint32 { + if x != nil { + return x.Row + } + return 0 +} + +func (x *TtyWinResizeRequest) GetColumn() uint32 { + if x != nil { + return x.Column + } + return 0 +} + +type KernelModule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This field is the name of the kernel module. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // This field are the parameters for the kernel module which are + // whitespace-delimited key=value pairs passed to modprobe(8). + Parameters []string `protobuf:"bytes,2,rep,name=parameters,proto3" json:"parameters,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KernelModule) Reset() { + *x = KernelModule{} + mi := &file_agent_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KernelModule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KernelModule) ProtoMessage() {} + +func (x *KernelModule) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KernelModule.ProtoReflect.Descriptor instead. +func (*KernelModule) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{29} +} + +func (x *KernelModule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *KernelModule) GetParameters() []string { + if x != nil { + return x.Parameters + } + return nil +} + +type CreateSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + Dns []string `protobuf:"bytes,2,rep,name=dns,proto3" json:"dns,omitempty"` + Storages []*Storage `protobuf:"bytes,3,rep,name=storages,proto3" json:"storages,omitempty"` + // This field means that a pause process needs to be created by the + // agent. This pid namespace of the pause process will be treated as + // a shared pid namespace. All containers created will join this shared + // pid namespace. + SandboxPidns bool `protobuf:"varint,4,opt,name=sandbox_pidns,json=sandboxPidns,proto3" json:"sandbox_pidns,omitempty"` + // SandboxId identifies which sandbox is using the agent. We allow only + // one sandbox per agent and implicitly require that CreateSandbox is + // called before other sandbox/network calls. + SandboxId string `protobuf:"bytes,5,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // This field, if non-empty, designates an absolute path to a directory + // that the agent will search for OCI hooks to run within the guest. + GuestHookPath string `protobuf:"bytes,6,opt,name=guest_hook_path,json=guestHookPath,proto3" json:"guest_hook_path,omitempty"` + // This field is the list of kernel modules to be loaded in the guest kernel. + KernelModules []*KernelModule `protobuf:"bytes,7,rep,name=kernel_modules,json=kernelModules,proto3" json:"kernel_modules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSandboxRequest) Reset() { + *x = CreateSandboxRequest{} + mi := &file_agent_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSandboxRequest) ProtoMessage() {} + +func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead. +func (*CreateSandboxRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{30} +} + +func (x *CreateSandboxRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *CreateSandboxRequest) GetDns() []string { + if x != nil { + return x.Dns + } + return nil +} + +func (x *CreateSandboxRequest) GetStorages() []*Storage { + if x != nil { + return x.Storages + } + return nil +} + +func (x *CreateSandboxRequest) GetSandboxPidns() bool { + if x != nil { + return x.SandboxPidns + } + return false +} + +func (x *CreateSandboxRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *CreateSandboxRequest) GetGuestHookPath() string { + if x != nil { + return x.GuestHookPath + } + return "" +} + +func (x *CreateSandboxRequest) GetKernelModules() []*KernelModule { + if x != nil { + return x.KernelModules + } + return nil +} + +type DestroySandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DestroySandboxRequest) Reset() { + *x = DestroySandboxRequest{} + mi := &file_agent_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DestroySandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DestroySandboxRequest) ProtoMessage() {} + +func (x *DestroySandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DestroySandboxRequest.ProtoReflect.Descriptor instead. +func (*DestroySandboxRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{31} +} + +type RemoveStaleVirtiofsShareMountsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveStaleVirtiofsShareMountsRequest) Reset() { + *x = RemoveStaleVirtiofsShareMountsRequest{} + mi := &file_agent_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveStaleVirtiofsShareMountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveStaleVirtiofsShareMountsRequest) ProtoMessage() {} + +func (x *RemoveStaleVirtiofsShareMountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveStaleVirtiofsShareMountsRequest.ProtoReflect.Descriptor instead. +func (*RemoveStaleVirtiofsShareMountsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{32} +} + +type Interfaces struct { + state protoimpl.MessageState `protogen:"open.v1"` + Interfaces []*Interface `protobuf:"bytes,1,rep,name=Interfaces,proto3" json:"Interfaces,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Interfaces) Reset() { + *x = Interfaces{} + mi := &file_agent_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Interfaces) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Interfaces) ProtoMessage() {} + +func (x *Interfaces) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Interfaces.ProtoReflect.Descriptor instead. +func (*Interfaces) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{33} +} + +func (x *Interfaces) GetInterfaces() []*Interface { + if x != nil { + return x.Interfaces + } + return nil +} + +type Routes struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*Route `protobuf:"bytes,1,rep,name=Routes,proto3" json:"Routes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Routes) Reset() { + *x = Routes{} + mi := &file_agent_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Routes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Routes) ProtoMessage() {} + +func (x *Routes) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Routes.ProtoReflect.Descriptor instead. +func (*Routes) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{34} +} + +func (x *Routes) GetRoutes() []*Route { + if x != nil { + return x.Routes + } + return nil +} + +type UpdateInterfaceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Interface *Interface `protobuf:"bytes,1,opt,name=interface,proto3" json:"interface,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateInterfaceRequest) Reset() { + *x = UpdateInterfaceRequest{} + mi := &file_agent_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateInterfaceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateInterfaceRequest) ProtoMessage() {} + +func (x *UpdateInterfaceRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateInterfaceRequest.ProtoReflect.Descriptor instead. +func (*UpdateInterfaceRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{35} +} + +func (x *UpdateInterfaceRequest) GetInterface() *Interface { + if x != nil { + return x.Interface + } + return nil +} + +type UpdateRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes *Routes `protobuf:"bytes,1,opt,name=routes,proto3" json:"routes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRoutesRequest) Reset() { + *x = UpdateRoutesRequest{} + mi := &file_agent_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRoutesRequest) ProtoMessage() {} + +func (x *UpdateRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRoutesRequest.ProtoReflect.Descriptor instead. +func (*UpdateRoutesRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{36} +} + +func (x *UpdateRoutesRequest) GetRoutes() *Routes { + if x != nil { + return x.Routes + } + return nil +} + +type UpdateEphemeralMountsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Storages []*Storage `protobuf:"bytes,1,rep,name=storages,proto3" json:"storages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateEphemeralMountsRequest) Reset() { + *x = UpdateEphemeralMountsRequest{} + mi := &file_agent_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateEphemeralMountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateEphemeralMountsRequest) ProtoMessage() {} + +func (x *UpdateEphemeralMountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateEphemeralMountsRequest.ProtoReflect.Descriptor instead. +func (*UpdateEphemeralMountsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{37} +} + +func (x *UpdateEphemeralMountsRequest) GetStorages() []*Storage { + if x != nil { + return x.Storages + } + return nil +} + +type ListInterfacesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInterfacesRequest) Reset() { + *x = ListInterfacesRequest{} + mi := &file_agent_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInterfacesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInterfacesRequest) ProtoMessage() {} + +func (x *ListInterfacesRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInterfacesRequest.ProtoReflect.Descriptor instead. +func (*ListInterfacesRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{38} +} + +type ListRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRoutesRequest) Reset() { + *x = ListRoutesRequest{} + mi := &file_agent_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRoutesRequest) ProtoMessage() {} + +func (x *ListRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRoutesRequest.ProtoReflect.Descriptor instead. +func (*ListRoutesRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{39} +} + +type ARPNeighbors struct { + state protoimpl.MessageState `protogen:"open.v1"` + ARPNeighbors []*ARPNeighbor `protobuf:"bytes,1,rep,name=ARPNeighbors,proto3" json:"ARPNeighbors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ARPNeighbors) Reset() { + *x = ARPNeighbors{} + mi := &file_agent_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ARPNeighbors) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ARPNeighbors) ProtoMessage() {} + +func (x *ARPNeighbors) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ARPNeighbors.ProtoReflect.Descriptor instead. +func (*ARPNeighbors) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{40} +} + +func (x *ARPNeighbors) GetARPNeighbors() []*ARPNeighbor { + if x != nil { + return x.ARPNeighbors + } + return nil +} + +type AddARPNeighborsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Neighbors *ARPNeighbors `protobuf:"bytes,1,opt,name=neighbors,proto3" json:"neighbors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddARPNeighborsRequest) Reset() { + *x = AddARPNeighborsRequest{} + mi := &file_agent_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddARPNeighborsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddARPNeighborsRequest) ProtoMessage() {} + +func (x *AddARPNeighborsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddARPNeighborsRequest.ProtoReflect.Descriptor instead. +func (*AddARPNeighborsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{41} +} + +func (x *AddARPNeighborsRequest) GetNeighbors() *ARPNeighbors { + if x != nil { + return x.Neighbors + } + return nil +} + +type GetIPTablesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsIpv6 bool `protobuf:"varint,1,opt,name=is_ipv6,json=isIpv6,proto3" json:"is_ipv6,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIPTablesRequest) Reset() { + *x = GetIPTablesRequest{} + mi := &file_agent_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIPTablesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIPTablesRequest) ProtoMessage() {} + +func (x *GetIPTablesRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIPTablesRequest.ProtoReflect.Descriptor instead. +func (*GetIPTablesRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{42} +} + +func (x *GetIPTablesRequest) GetIsIpv6() bool { + if x != nil { + return x.IsIpv6 + } + return false +} + +type GetIPTablesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // raw stdout from iptables-save or ip6tables-save + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIPTablesResponse) Reset() { + *x = GetIPTablesResponse{} + mi := &file_agent_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIPTablesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIPTablesResponse) ProtoMessage() {} + +func (x *GetIPTablesResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIPTablesResponse.ProtoReflect.Descriptor instead. +func (*GetIPTablesResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{43} +} + +func (x *GetIPTablesResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type SetIPTablesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + IsIpv6 bool `protobuf:"varint,1,opt,name=is_ipv6,json=isIpv6,proto3" json:"is_ipv6,omitempty"` + // iptables, in raw format expected to be passed to stdin + // of iptables-save or ip6tables-save + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetIPTablesRequest) Reset() { + *x = SetIPTablesRequest{} + mi := &file_agent_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetIPTablesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetIPTablesRequest) ProtoMessage() {} + +func (x *SetIPTablesRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetIPTablesRequest.ProtoReflect.Descriptor instead. +func (*SetIPTablesRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{44} +} + +func (x *SetIPTablesRequest) GetIsIpv6() bool { + if x != nil { + return x.IsIpv6 + } + return false +} + +func (x *SetIPTablesRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type SetIPTablesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // raw stdout from iptables-restore or ip6tables-restore + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetIPTablesResponse) Reset() { + *x = SetIPTablesResponse{} + mi := &file_agent_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetIPTablesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetIPTablesResponse) ProtoMessage() {} + +func (x *SetIPTablesResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetIPTablesResponse.ProtoReflect.Descriptor instead. +func (*SetIPTablesResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{45} +} + +func (x *SetIPTablesResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type OnlineCPUMemRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Wait specifies if the caller waits for the agent to online all resources. + // If true the agent returns once all resources have been connected, otherwise all + // resources are connected asynchronously and the agent returns immediately. + Wait bool `protobuf:"varint,1,opt,name=wait,proto3" json:"wait,omitempty"` + // NbCpus specifies the number of CPUs that should be onlined in the guest. + // Special value 0 means agent will skip this check. + NbCpus uint32 `protobuf:"varint,2,opt,name=nb_cpus,json=nbCpus,proto3" json:"nb_cpus,omitempty"` + // CpuOnly specifies whether only online CPU or not. + CpuOnly bool `protobuf:"varint,3,opt,name=cpu_only,json=cpuOnly,proto3" json:"cpu_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OnlineCPUMemRequest) Reset() { + *x = OnlineCPUMemRequest{} + mi := &file_agent_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OnlineCPUMemRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OnlineCPUMemRequest) ProtoMessage() {} + +func (x *OnlineCPUMemRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OnlineCPUMemRequest.ProtoReflect.Descriptor instead. +func (*OnlineCPUMemRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{46} +} + +func (x *OnlineCPUMemRequest) GetWait() bool { + if x != nil { + return x.Wait + } + return false +} + +func (x *OnlineCPUMemRequest) GetNbCpus() uint32 { + if x != nil { + return x.NbCpus + } + return 0 +} + +func (x *OnlineCPUMemRequest) GetCpuOnly() bool { + if x != nil { + return x.CpuOnly + } + return false +} + +type ReseedRandomDevRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Data specifies the random data used to reseed the guest crng. + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReseedRandomDevRequest) Reset() { + *x = ReseedRandomDevRequest{} + mi := &file_agent_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReseedRandomDevRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReseedRandomDevRequest) ProtoMessage() {} + +func (x *ReseedRandomDevRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReseedRandomDevRequest.ProtoReflect.Descriptor instead. +func (*ReseedRandomDevRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{47} +} + +func (x *ReseedRandomDevRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// AgentDetails provides information to the client about the running agent. +type AgentDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Semantic version of agent (see https://semver.org). + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + // Set if the agent is running as PID 1. + InitDaemon bool `protobuf:"varint,2,opt,name=init_daemon,json=initDaemon,proto3" json:"init_daemon,omitempty"` + // List of available device handlers. + DeviceHandlers []string `protobuf:"bytes,3,rep,name=device_handlers,json=deviceHandlers,proto3" json:"device_handlers,omitempty"` + // List of available storage handlers. + StorageHandlers []string `protobuf:"bytes,4,rep,name=storage_handlers,json=storageHandlers,proto3" json:"storage_handlers,omitempty"` + // Set only if the agent is built with seccomp support and the guest + // environment supports seccomp. + SupportsSeccomp bool `protobuf:"varint,5,opt,name=supports_seccomp,json=supportsSeccomp,proto3" json:"supports_seccomp,omitempty"` + // List of additional features enabled at agent build time. + ExtraFeatures []string `protobuf:"bytes,6,rep,name=extra_features,json=extraFeatures,proto3" json:"extra_features,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentDetails) Reset() { + *x = AgentDetails{} + mi := &file_agent_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentDetails) ProtoMessage() {} + +func (x *AgentDetails) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentDetails.ProtoReflect.Descriptor instead. +func (*AgentDetails) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{48} +} + +func (x *AgentDetails) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *AgentDetails) GetInitDaemon() bool { + if x != nil { + return x.InitDaemon + } + return false +} + +func (x *AgentDetails) GetDeviceHandlers() []string { + if x != nil { + return x.DeviceHandlers + } + return nil +} + +func (x *AgentDetails) GetStorageHandlers() []string { + if x != nil { + return x.StorageHandlers + } + return nil +} + +func (x *AgentDetails) GetSupportsSeccomp() bool { + if x != nil { + return x.SupportsSeccomp + } + return false +} + +func (x *AgentDetails) GetExtraFeatures() []string { + if x != nil { + return x.ExtraFeatures + } + return nil +} + +type GuestDetailsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // MemBlockSize asks server to return the system memory block size that can be used + // for memory hotplug alignment. Typically the server returns what's in + // /sys/devices/system/memory/block_size_bytes. + MemBlockSize bool `protobuf:"varint,1,opt,name=mem_block_size,json=memBlockSize,proto3" json:"mem_block_size,omitempty"` + // MemoryHotplugProbe asks server to return whether guest kernel supports memory hotplug + // via probeinterface. Typically the server will check if the path + // /sys/devices/system/memory/probe exists. + MemHotplugProbe bool `protobuf:"varint,2,opt,name=mem_hotplug_probe,json=memHotplugProbe,proto3" json:"mem_hotplug_probe,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GuestDetailsRequest) Reset() { + *x = GuestDetailsRequest{} + mi := &file_agent_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GuestDetailsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GuestDetailsRequest) ProtoMessage() {} + +func (x *GuestDetailsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GuestDetailsRequest.ProtoReflect.Descriptor instead. +func (*GuestDetailsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{49} +} + +func (x *GuestDetailsRequest) GetMemBlockSize() bool { + if x != nil { + return x.MemBlockSize + } + return false +} + +func (x *GuestDetailsRequest) GetMemHotplugProbe() bool { + if x != nil { + return x.MemHotplugProbe + } + return false +} + +type GuestDetailsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // MemBlockSizeBytes returns the system memory block size in bytes. + MemBlockSizeBytes uint64 `protobuf:"varint,1,opt,name=mem_block_size_bytes,json=memBlockSizeBytes,proto3" json:"mem_block_size_bytes,omitempty"` + AgentDetails *AgentDetails `protobuf:"bytes,2,opt,name=agent_details,json=agentDetails,proto3" json:"agent_details,omitempty"` + SupportMemHotplugProbe bool `protobuf:"varint,3,opt,name=support_mem_hotplug_probe,json=supportMemHotplugProbe,proto3" json:"support_mem_hotplug_probe,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GuestDetailsResponse) Reset() { + *x = GuestDetailsResponse{} + mi := &file_agent_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GuestDetailsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GuestDetailsResponse) ProtoMessage() {} + +func (x *GuestDetailsResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GuestDetailsResponse.ProtoReflect.Descriptor instead. +func (*GuestDetailsResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{50} +} + +func (x *GuestDetailsResponse) GetMemBlockSizeBytes() uint64 { + if x != nil { + return x.MemBlockSizeBytes + } + return 0 +} + +func (x *GuestDetailsResponse) GetAgentDetails() *AgentDetails { + if x != nil { + return x.AgentDetails + } + return nil +} + +func (x *GuestDetailsResponse) GetSupportMemHotplugProbe() bool { + if x != nil { + return x.SupportMemHotplugProbe + } + return false +} + +type MemHotplugByProbeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // server needs to send the value of memHotplugProbeAddr into file /sys/devices/system/memory/probe, + // in order to notify the guest kernel about hot-add memory event + MemHotplugProbeAddr []uint64 `protobuf:"varint,1,rep,packed,name=memHotplugProbeAddr,proto3" json:"memHotplugProbeAddr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemHotplugByProbeRequest) Reset() { + *x = MemHotplugByProbeRequest{} + mi := &file_agent_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemHotplugByProbeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemHotplugByProbeRequest) ProtoMessage() {} + +func (x *MemHotplugByProbeRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemHotplugByProbeRequest.ProtoReflect.Descriptor instead. +func (*MemHotplugByProbeRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{51} +} + +func (x *MemHotplugByProbeRequest) GetMemHotplugProbeAddr() []uint64 { + if x != nil { + return x.MemHotplugProbeAddr + } + return nil +} + +type SetGuestDateTimeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sec the second since the Epoch. + Sec int64 `protobuf:"varint,1,opt,name=Sec,proto3" json:"Sec,omitempty"` + // Usec the microseconds portion of time since the Epoch. + Usec int64 `protobuf:"varint,2,opt,name=Usec,proto3" json:"Usec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetGuestDateTimeRequest) Reset() { + *x = SetGuestDateTimeRequest{} + mi := &file_agent_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetGuestDateTimeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetGuestDateTimeRequest) ProtoMessage() {} + +func (x *SetGuestDateTimeRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetGuestDateTimeRequest.ProtoReflect.Descriptor instead. +func (*SetGuestDateTimeRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{52} +} + +func (x *SetGuestDateTimeRequest) GetSec() int64 { + if x != nil { + return x.Sec + } + return 0 +} + +func (x *SetGuestDateTimeRequest) GetUsec() int64 { + if x != nil { + return x.Usec + } + return 0 +} + +// FSGroup consists of the group id and group ownership change policy +// that a volume should have its ownership changed to. +type FSGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + // GroupID is the ID that the group ownership of the + // files in the mounted volume will need to be changed to. + GroupId uint32 `protobuf:"varint,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + // GroupChangePolicy specifies the policy for applying group id + // ownership change on a mounted volume. + GroupChangePolicy FSGroupChangePolicy `protobuf:"varint,3,opt,name=group_change_policy,json=groupChangePolicy,proto3,enum=types.FSGroupChangePolicy" json:"group_change_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FSGroup) Reset() { + *x = FSGroup{} + mi := &file_agent_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FSGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FSGroup) ProtoMessage() {} + +func (x *FSGroup) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FSGroup.ProtoReflect.Descriptor instead. +func (*FSGroup) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{53} +} + +func (x *FSGroup) GetGroupId() uint32 { + if x != nil { + return x.GroupId + } + return 0 +} + +func (x *FSGroup) GetGroupChangePolicy() FSGroupChangePolicy { + if x != nil { + return x.GroupChangePolicy + } + return FSGroupChangePolicy_Always +} + +// SharedMount declares a set of shared mount points that support +// cross-container sharing of mount objects. +type SharedMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name is used to identify a pair of shared mount points. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Src_ctr is used to specify the name of the source container. + SrcCtr string `protobuf:"bytes,2,opt,name=src_ctr,json=srcCtr,proto3" json:"src_ctr,omitempty"` + // Src_path is used to specify the path of the mount point. If the path doesn't + // exist in the rootfs, it will be created. + SrcPath string `protobuf:"bytes,3,opt,name=src_path,json=srcPath,proto3" json:"src_path,omitempty"` + // Dst_ctr is used to specify the name of the destination container. + DstCtr string `protobuf:"bytes,4,opt,name=dst_ctr,json=dstCtr,proto3" json:"dst_ctr,omitempty"` + // Dst_path is used to specify the path of the mount point. If the path doesn't + // exist in the rootfs, it will be created. + DstPath string `protobuf:"bytes,5,opt,name=dst_path,json=dstPath,proto3" json:"dst_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SharedMount) Reset() { + *x = SharedMount{} + mi := &file_agent_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SharedMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedMount) ProtoMessage() {} + +func (x *SharedMount) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SharedMount.ProtoReflect.Descriptor instead. +func (*SharedMount) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{54} +} + +func (x *SharedMount) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SharedMount) GetSrcCtr() string { + if x != nil { + return x.SrcCtr + } + return "" +} + +func (x *SharedMount) GetSrcPath() string { + if x != nil { + return x.SrcPath + } + return "" +} + +func (x *SharedMount) GetDstCtr() string { + if x != nil { + return x.DstCtr + } + return "" +} + +func (x *SharedMount) GetDstPath() string { + if x != nil { + return x.DstPath + } + return "" +} + +// Storage represents both the rootfs of the container, and any volume that +// could have been defined through the Mount list of the OCI specification. +type Storage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Driver is used to define the way the storage is passed through the + // virtual machine. It can be "blk", or something else, but for + // all cases, this will define if some extra steps are required before + // this storage gets mounted into the container. + Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"` + // DriverOptions allows the caller to define a list of options such + // as block sizes, numbers of luns, ... which are very specific to + // every device and cannot be generalized through extra fields. + DriverOptions []string `protobuf:"bytes,2,rep,name=driver_options,json=driverOptions,proto3" json:"driver_options,omitempty"` + // Source can be anything representing the source of the storage. This + // will be handled by the proper handler based on the Driver used. + // For instance, it can be a very simple path if the caller knows the + // name of device inside the VM, or it can be some sort of identifier + // to let the agent find the device inside the VM. + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + // Fstype represents the filesystem that needs to be used to mount the + // storage inside the VM. For instance, it could be "xfs" for block + // device, or "tmpfs" for shared /dev/shm. + Fstype string `protobuf:"bytes,4,opt,name=fstype,proto3" json:"fstype,omitempty"` + // Options describes the additional options that might be needed to + // mount properly the storage filesystem. + Options []string `protobuf:"bytes,5,rep,name=options,proto3" json:"options,omitempty"` + // MountPoint refers to the path where the storage should be mounted + // inside the VM. + MountPoint string `protobuf:"bytes,6,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` + // FSGroup consists of the group ID and group ownership change policy + // that the mounted volume must have its group ID changed to when specified. + FsGroup *FSGroup `protobuf:"bytes,7,opt,name=fs_group,json=fsGroup,proto3" json:"fs_group,omitempty"` + // Shared indicates this storage is shared across multiple containers + // (e.g., block-based emptyDirs). When true, the agent should not clean up + // the storage when a container using it exits, as other containers + // may still need it. Cleanup will happen when the sandbox is destroyed. + Shared bool `protobuf:"varint,8,opt,name=shared,proto3" json:"shared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Storage) Reset() { + *x = Storage{} + mi := &file_agent_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Storage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Storage) ProtoMessage() {} + +func (x *Storage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Storage.ProtoReflect.Descriptor instead. +func (*Storage) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{55} +} + +func (x *Storage) GetDriver() string { + if x != nil { + return x.Driver + } + return "" +} + +func (x *Storage) GetDriverOptions() []string { + if x != nil { + return x.DriverOptions + } + return nil +} + +func (x *Storage) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Storage) GetFstype() string { + if x != nil { + return x.Fstype + } + return "" +} + +func (x *Storage) GetOptions() []string { + if x != nil { + return x.Options + } + return nil +} + +func (x *Storage) GetMountPoint() string { + if x != nil { + return x.MountPoint + } + return "" +} + +func (x *Storage) GetFsGroup() *FSGroup { + if x != nil { + return x.FsGroup + } + return nil +} + +func (x *Storage) GetShared() bool { + if x != nil { + return x.Shared + } + return false +} + +// Device represents only the devices that could have been defined through the +// Linux Device list of the OCI specification. +type Device struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Id can be used to identify the device inside the VM. Some devices + // might not need it to be identified on the VM, and will rely on the + // provided VmPath instead. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Type defines the type of device described. This can be "blk", + // "scsi", "vfio", ... + // Particularly, this should be used to trigger the use of the + // appropriate device handler. + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // VmPath can be used by the caller to provide directly the path of + // the device as it will appear inside the VM. For some devices, the + // device id or the list of options passed might not be enough to find + // the device. In those cases, the caller should predict and provide + // this vm_path. + VmPath string `protobuf:"bytes,3,opt,name=vm_path,json=vmPath,proto3" json:"vm_path,omitempty"` + // ContainerPath defines the path where the device should be found inside + // the container. This path should match the path of the device from + // the device list listed inside the OCI spec. This is used in order + // to identify the right device in the spec and update it with the + // right options such as major/minor numbers as they appear inside + // the VM for instance. Note that an empty ctr_path should be used + // to make sure the device handler inside the agent is called, but + // no spec update needs to be performed. This has to happen for the + // case of rootfs, when a device has to be waited for after it has + // been hotplugged. An equivalent Storage entry should be defined if + // any mount needs to be performed afterwards. + ContainerPath string `protobuf:"bytes,4,opt,name=container_path,json=containerPath,proto3" json:"container_path,omitempty"` + // Options allows the caller to define a list of options such as block + // sizes, numbers of luns, ... which are very specific to every device + // and cannot be generalized through extra fields. + Options []string `protobuf:"bytes,5,rep,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Device) Reset() { + *x = Device{} + mi := &file_agent_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Device) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Device) ProtoMessage() {} + +func (x *Device) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Device.ProtoReflect.Descriptor instead. +func (*Device) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{56} +} + +func (x *Device) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Device) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Device) GetVmPath() string { + if x != nil { + return x.VmPath + } + return "" +} + +func (x *Device) GetContainerPath() string { + if x != nil { + return x.ContainerPath + } + return "" +} + +func (x *Device) GetOptions() []string { + if x != nil { + return x.Options + } + return nil +} + +type StringUser struct { + state protoimpl.MessageState `protogen:"open.v1"` + Uid string `protobuf:"bytes,1,opt,name=uid,proto3" json:"uid,omitempty"` + Gid string `protobuf:"bytes,2,opt,name=gid,proto3" json:"gid,omitempty"` + AdditionalGids []string `protobuf:"bytes,3,rep,name=additionalGids,proto3" json:"additionalGids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StringUser) Reset() { + *x = StringUser{} + mi := &file_agent_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringUser) ProtoMessage() {} + +func (x *StringUser) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringUser.ProtoReflect.Descriptor instead. +func (*StringUser) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{57} +} + +func (x *StringUser) GetUid() string { + if x != nil { + return x.Uid + } + return "" +} + +func (x *StringUser) GetGid() string { + if x != nil { + return x.Gid + } + return "" +} + +func (x *StringUser) GetAdditionalGids() []string { + if x != nil { + return x.AdditionalGids + } + return nil +} + +type CopyFileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Path is the destination file in the guest. It must be absolute, + // canonical and below /run. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // FileSize is the expected file size, for security reasons write operations + // are made in a temporary file, once it has the expected size, it's moved + // to the destination path. + FileSize int64 `protobuf:"varint,2,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"` + // FileMode is the file mode. + FileMode uint32 `protobuf:"varint,3,opt,name=file_mode,json=fileMode,proto3" json:"file_mode,omitempty"` + // DirMode is the mode for the parent directories of destination path. + DirMode uint32 `protobuf:"varint,4,opt,name=dir_mode,json=dirMode,proto3" json:"dir_mode,omitempty"` + // Uid is the numeric user id. + Uid int32 `protobuf:"varint,5,opt,name=uid,proto3" json:"uid,omitempty"` + // Gid is the numeric group id. + Gid int32 `protobuf:"varint,6,opt,name=gid,proto3" json:"gid,omitempty"` + // Offset for the next write operation. + Offset int64 `protobuf:"varint,7,opt,name=offset,proto3" json:"offset,omitempty"` + // Data to write in the destination file. + Data []byte `protobuf:"bytes,8,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CopyFileRequest) Reset() { + *x = CopyFileRequest{} + mi := &file_agent_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CopyFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CopyFileRequest) ProtoMessage() {} + +func (x *CopyFileRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CopyFileRequest.ProtoReflect.Descriptor instead. +func (*CopyFileRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{58} +} + +func (x *CopyFileRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *CopyFileRequest) GetFileSize() int64 { + if x != nil { + return x.FileSize + } + return 0 +} + +func (x *CopyFileRequest) GetFileMode() uint32 { + if x != nil { + return x.FileMode + } + return 0 +} + +func (x *CopyFileRequest) GetDirMode() uint32 { + if x != nil { + return x.DirMode + } + return 0 +} + +func (x *CopyFileRequest) GetUid() int32 { + if x != nil { + return x.Uid + } + return 0 +} + +func (x *CopyFileRequest) GetGid() int32 { + if x != nil { + return x.Gid + } + return 0 +} + +func (x *CopyFileRequest) GetOffset() int64 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *CopyFileRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type GetOOMEventRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOOMEventRequest) Reset() { + *x = GetOOMEventRequest{} + mi := &file_agent_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOOMEventRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOOMEventRequest) ProtoMessage() {} + +func (x *GetOOMEventRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOOMEventRequest.ProtoReflect.Descriptor instead. +func (*GetOOMEventRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{59} +} + +type OOMEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OOMEvent) Reset() { + *x = OOMEvent{} + mi := &file_agent_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OOMEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OOMEvent) ProtoMessage() {} + +func (x *OOMEvent) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OOMEvent.ProtoReflect.Descriptor instead. +func (*OOMEvent) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{60} +} + +func (x *OOMEvent) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type AddSwapRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PCIPath []uint32 `protobuf:"varint,1,rep,packed,name=PCIPath,proto3" json:"PCIPath,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddSwapRequest) Reset() { + *x = AddSwapRequest{} + mi := &file_agent_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddSwapRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddSwapRequest) ProtoMessage() {} + +func (x *AddSwapRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddSwapRequest.ProtoReflect.Descriptor instead. +func (*AddSwapRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{61} +} + +func (x *AddSwapRequest) GetPCIPath() []uint32 { + if x != nil { + return x.PCIPath + } + return nil +} + +type AddSwapPathRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddSwapPathRequest) Reset() { + *x = AddSwapPathRequest{} + mi := &file_agent_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddSwapPathRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddSwapPathRequest) ProtoMessage() {} + +func (x *AddSwapPathRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddSwapPathRequest.ProtoReflect.Descriptor instead. +func (*AddSwapPathRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{62} +} + +func (x *AddSwapPathRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type GetMetricsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetricsRequest) Reset() { + *x = GetMetricsRequest{} + mi := &file_agent_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetricsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetricsRequest) ProtoMessage() {} + +func (x *GetMetricsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetricsRequest.ProtoReflect.Descriptor instead. +func (*GetMetricsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{63} +} + +type Metrics struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metrics string `protobuf:"bytes,1,opt,name=metrics,proto3" json:"metrics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Metrics) Reset() { + *x = Metrics{} + mi := &file_agent_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Metrics) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metrics) ProtoMessage() {} + +func (x *Metrics) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metrics.ProtoReflect.Descriptor instead. +func (*Metrics) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{64} +} + +func (x *Metrics) GetMetrics() string { + if x != nil { + return x.Metrics + } + return "" +} + +type VolumeStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volume path on the guest outside the container + VolumeGuestPath string `protobuf:"bytes,1,opt,name=volume_guest_path,json=volumeGuestPath,proto3" json:"volume_guest_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeStatsRequest) Reset() { + *x = VolumeStatsRequest{} + mi := &file_agent_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeStatsRequest) ProtoMessage() {} + +func (x *VolumeStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeStatsRequest.ProtoReflect.Descriptor instead. +func (*VolumeStatsRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{65} +} + +func (x *VolumeStatsRequest) GetVolumeGuestPath() string { + if x != nil { + return x.VolumeGuestPath + } + return "" +} + +type ResizeVolumeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Full VM guest path of the volume (outside the container) + VolumeGuestPath string `protobuf:"bytes,1,opt,name=volume_guest_path,json=volumeGuestPath,proto3" json:"volume_guest_path,omitempty"` + Size uint64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResizeVolumeRequest) Reset() { + *x = ResizeVolumeRequest{} + mi := &file_agent_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResizeVolumeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResizeVolumeRequest) ProtoMessage() {} + +func (x *ResizeVolumeRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResizeVolumeRequest.ProtoReflect.Descriptor instead. +func (*ResizeVolumeRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{66} +} + +func (x *ResizeVolumeRequest) GetVolumeGuestPath() string { + if x != nil { + return x.VolumeGuestPath + } + return "" +} + +func (x *ResizeVolumeRequest) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +type SetPolicyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetPolicyRequest) Reset() { + *x = SetPolicyRequest{} + mi := &file_agent_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetPolicyRequest) ProtoMessage() {} + +func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead. +func (*SetPolicyRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{67} +} + +func (x *SetPolicyRequest) GetPolicy() string { + if x != nil { + return x.Policy + } + return "" +} + +type GetDiagnosticDataRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + LogType string `protobuf:"bytes,1,opt,name=log_type,json=logType,proto3" json:"log_type,omitempty"` + ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDiagnosticDataRequest) Reset() { + *x = GetDiagnosticDataRequest{} + mi := &file_agent_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDiagnosticDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDiagnosticDataRequest) ProtoMessage() {} + +func (x *GetDiagnosticDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDiagnosticDataRequest.ProtoReflect.Descriptor instead. +func (*GetDiagnosticDataRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{68} +} + +func (x *GetDiagnosticDataRequest) GetLogType() string { + if x != nil { + return x.LogType + } + return "" +} + +func (x *GetDiagnosticDataRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type GetDiagnosticDataResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDiagnosticDataResponse) Reset() { + *x = GetDiagnosticDataResponse{} + mi := &file_agent_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDiagnosticDataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDiagnosticDataResponse) ProtoMessage() {} + +func (x *GetDiagnosticDataResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDiagnosticDataResponse.ProtoReflect.Descriptor instead. +func (*GetDiagnosticDataResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{69} +} + +func (x *GetDiagnosticDataResponse) GetData() string { + if x != nil { + return x.Data + } + return "" +} + +type MemAgentMemcgConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Disabled *bool `protobuf:"varint,1,opt,name=disabled,proto3,oneof" json:"disabled,omitempty"` + Swap *bool `protobuf:"varint,2,opt,name=swap,proto3,oneof" json:"swap,omitempty"` + SwappinessMax *uint32 `protobuf:"varint,3,opt,name=swappiness_max,json=swappinessMax,proto3,oneof" json:"swappiness_max,omitempty"` + PeriodSecs *uint64 `protobuf:"varint,4,opt,name=period_secs,json=periodSecs,proto3,oneof" json:"period_secs,omitempty"` + PeriodPsiPercentLimit *uint32 `protobuf:"varint,5,opt,name=period_psi_percent_limit,json=periodPsiPercentLimit,proto3,oneof" json:"period_psi_percent_limit,omitempty"` + EvictionPsiPercentLimit *uint32 `protobuf:"varint,6,opt,name=eviction_psi_percent_limit,json=evictionPsiPercentLimit,proto3,oneof" json:"eviction_psi_percent_limit,omitempty"` + EvictionRunAgingCountMin *uint64 `protobuf:"varint,7,opt,name=eviction_run_aging_count_min,json=evictionRunAgingCountMin,proto3,oneof" json:"eviction_run_aging_count_min,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemAgentMemcgConfig) Reset() { + *x = MemAgentMemcgConfig{} + mi := &file_agent_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemAgentMemcgConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemAgentMemcgConfig) ProtoMessage() {} + +func (x *MemAgentMemcgConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemAgentMemcgConfig.ProtoReflect.Descriptor instead. +func (*MemAgentMemcgConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{70} +} + +func (x *MemAgentMemcgConfig) GetDisabled() bool { + if x != nil && x.Disabled != nil { + return *x.Disabled + } + return false +} + +func (x *MemAgentMemcgConfig) GetSwap() bool { + if x != nil && x.Swap != nil { + return *x.Swap + } + return false +} + +func (x *MemAgentMemcgConfig) GetSwappinessMax() uint32 { + if x != nil && x.SwappinessMax != nil { + return *x.SwappinessMax + } + return 0 +} + +func (x *MemAgentMemcgConfig) GetPeriodSecs() uint64 { + if x != nil && x.PeriodSecs != nil { + return *x.PeriodSecs + } + return 0 +} + +func (x *MemAgentMemcgConfig) GetPeriodPsiPercentLimit() uint32 { + if x != nil && x.PeriodPsiPercentLimit != nil { + return *x.PeriodPsiPercentLimit + } + return 0 +} + +func (x *MemAgentMemcgConfig) GetEvictionPsiPercentLimit() uint32 { + if x != nil && x.EvictionPsiPercentLimit != nil { + return *x.EvictionPsiPercentLimit + } + return 0 +} + +func (x *MemAgentMemcgConfig) GetEvictionRunAgingCountMin() uint64 { + if x != nil && x.EvictionRunAgingCountMin != nil { + return *x.EvictionRunAgingCountMin + } + return 0 +} + +type MemAgentCompactConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Disabled *bool `protobuf:"varint,1,opt,name=disabled,proto3,oneof" json:"disabled,omitempty"` + PeriodSecs *uint64 `protobuf:"varint,2,opt,name=period_secs,json=periodSecs,proto3,oneof" json:"period_secs,omitempty"` + PeriodPsiPercentLimit *uint32 `protobuf:"varint,3,opt,name=period_psi_percent_limit,json=periodPsiPercentLimit,proto3,oneof" json:"period_psi_percent_limit,omitempty"` + CompactPsiPercentLimit *uint32 `protobuf:"varint,4,opt,name=compact_psi_percent_limit,json=compactPsiPercentLimit,proto3,oneof" json:"compact_psi_percent_limit,omitempty"` + CompactSecMax *int64 `protobuf:"varint,5,opt,name=compact_sec_max,json=compactSecMax,proto3,oneof" json:"compact_sec_max,omitempty"` + CompactOrder *uint32 `protobuf:"varint,6,opt,name=compact_order,json=compactOrder,proto3,oneof" json:"compact_order,omitempty"` + CompactThreshold *uint64 `protobuf:"varint,7,opt,name=compact_threshold,json=compactThreshold,proto3,oneof" json:"compact_threshold,omitempty"` + CompactForceTimes *uint64 `protobuf:"varint,8,opt,name=compact_force_times,json=compactForceTimes,proto3,oneof" json:"compact_force_times,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemAgentCompactConfig) Reset() { + *x = MemAgentCompactConfig{} + mi := &file_agent_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemAgentCompactConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemAgentCompactConfig) ProtoMessage() {} + +func (x *MemAgentCompactConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemAgentCompactConfig.ProtoReflect.Descriptor instead. +func (*MemAgentCompactConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{71} +} + +func (x *MemAgentCompactConfig) GetDisabled() bool { + if x != nil && x.Disabled != nil { + return *x.Disabled + } + return false +} + +func (x *MemAgentCompactConfig) GetPeriodSecs() uint64 { + if x != nil && x.PeriodSecs != nil { + return *x.PeriodSecs + } + return 0 +} + +func (x *MemAgentCompactConfig) GetPeriodPsiPercentLimit() uint32 { + if x != nil && x.PeriodPsiPercentLimit != nil { + return *x.PeriodPsiPercentLimit + } + return 0 +} + +func (x *MemAgentCompactConfig) GetCompactPsiPercentLimit() uint32 { + if x != nil && x.CompactPsiPercentLimit != nil { + return *x.CompactPsiPercentLimit + } + return 0 +} + +func (x *MemAgentCompactConfig) GetCompactSecMax() int64 { + if x != nil && x.CompactSecMax != nil { + return *x.CompactSecMax + } + return 0 +} + +func (x *MemAgentCompactConfig) GetCompactOrder() uint32 { + if x != nil && x.CompactOrder != nil { + return *x.CompactOrder + } + return 0 +} + +func (x *MemAgentCompactConfig) GetCompactThreshold() uint64 { + if x != nil && x.CompactThreshold != nil { + return *x.CompactThreshold + } + return 0 +} + +func (x *MemAgentCompactConfig) GetCompactForceTimes() uint64 { + if x != nil && x.CompactForceTimes != nil { + return *x.CompactForceTimes + } + return 0 +} + +var File_agent_proto protoreflect.FileDescriptor + +const file_agent_proto_rawDesc = "" + + "\n" + + "\vagent.proto\x12\x04grpc\x1a\toci.proto\x1a\tcsi.proto\x1a\vtypes.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xb6\x03\n" + + "\x16CreateContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x121\n" + + "\vstring_user\x18\x03 \x01(\v2\x10.grpc.StringUserR\n" + + "stringUser\x12&\n" + + "\adevices\x18\x04 \x03(\v2\f.grpc.DeviceR\adevices\x12)\n" + + "\bstorages\x18\x05 \x03(\v2\r.grpc.StorageR\bstorages\x12\x1c\n" + + "\x03OCI\x18\x06 \x01(\v2\n" + + ".grpc.SpecR\x03OCI\x12#\n" + + "\rsandbox_pidns\x18\a \x01(\bR\fsandboxPidns\x126\n" + + "\rshared_mounts\x18\b \x03(\v2\x11.grpc.SharedMountR\fsharedMounts\x12\x1d\n" + + "\n" + + "stdin_port\x18\t \x01(\rR\tstdinPort\x12\x1f\n" + + "\vstdout_port\x18\n" + + " \x01(\rR\n" + + "stdoutPort\x12\x1f\n" + + "\vstderr_port\x18\v \x01(\rR\n" + + "stderrPort\":\n" + + "\x15StartContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\"U\n" + + "\x16RemoveContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x18\n" + + "\atimeout\x18\x02 \x01(\rR\atimeout\"\x8d\x02\n" + + "\x12ExecProcessRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x121\n" + + "\vstring_user\x18\x03 \x01(\v2\x10.grpc.StringUserR\n" + + "stringUser\x12'\n" + + "\aprocess\x18\x04 \x01(\v2\r.grpc.ProcessR\aprocess\x12\x1d\n" + + "\n" + + "stdin_port\x18\x05 \x01(\rR\tstdinPort\x12\x1f\n" + + "\vstdout_port\x18\x06 \x01(\rR\n" + + "stdoutPort\x12\x1f\n" + + "\vstderr_port\x18\a \x01(\rR\n" + + "stderrPort\"j\n" + + "\x14SignalProcessRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x12\x16\n" + + "\x06signal\x18\x03 \x01(\rR\x06signal\"P\n" + + "\x12WaitProcessRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\"-\n" + + "\x13WaitProcessResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\x05R\x06status\"o\n" + + "\x16UpdateContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x122\n" + + "\tresources\x18\x02 \x01(\v2\x14.grpc.LinuxResourcesR\tresources\":\n" + + "\x15StatsContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\":\n" + + "\x15PauseContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\";\n" + + "\x16ResumeContainerRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\"\xaa\x01\n" + + "\bCpuUsage\x12\x1f\n" + + "\vtotal_usage\x18\x01 \x01(\x04R\n" + + "totalUsage\x12!\n" + + "\fpercpu_usage\x18\x02 \x03(\x04R\vpercpuUsage\x12.\n" + + "\x13usage_in_kernelmode\x18\x03 \x01(\x04R\x11usageInKernelmode\x12*\n" + + "\x11usage_in_usermode\x18\x04 \x01(\x04R\x0fusageInUsermode\"~\n" + + "\x0eThrottlingData\x12\x18\n" + + "\aperiods\x18\x01 \x01(\x04R\aperiods\x12+\n" + + "\x11throttled_periods\x18\x02 \x01(\x04R\x10throttledPeriods\x12%\n" + + "\x0ethrottled_time\x18\x03 \x01(\x04R\rthrottledTime\"v\n" + + "\bCpuStats\x12+\n" + + "\tcpu_usage\x18\x01 \x01(\v2\x0e.grpc.CpuUsageR\bcpuUsage\x12=\n" + + "\x0fthrottling_data\x18\x02 \x01(\v2\x14.grpc.ThrottlingDataR\x0ethrottlingData\";\n" + + "\tPidsStats\x12\x18\n" + + "\acurrent\x18\x01 \x01(\x04R\acurrent\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x04R\x05limit\"o\n" + + "\n" + + "MemoryData\x12\x14\n" + + "\x05usage\x18\x01 \x01(\x04R\x05usage\x12\x1b\n" + + "\tmax_usage\x18\x02 \x01(\x04R\bmaxUsage\x12\x18\n" + + "\afailcnt\x18\x03 \x01(\x04R\afailcnt\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x04R\x05limit\"\xc4\x02\n" + + "\vMemoryStats\x12\x14\n" + + "\x05cache\x18\x01 \x01(\x04R\x05cache\x12&\n" + + "\x05usage\x18\x02 \x01(\v2\x10.grpc.MemoryDataR\x05usage\x12/\n" + + "\n" + + "swap_usage\x18\x03 \x01(\v2\x10.grpc.MemoryDataR\tswapUsage\x123\n" + + "\fkernel_usage\x18\x04 \x01(\v2\x10.grpc.MemoryDataR\vkernelUsage\x12#\n" + + "\ruse_hierarchy\x18\x05 \x01(\bR\fuseHierarchy\x122\n" + + "\x05stats\x18\x06 \x03(\v2\x1c.grpc.MemoryStats.StatsEntryR\x05stats\x1a8\n" + + "\n" + + "StatsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x04R\x05value:\x028\x01\"c\n" + + "\x0fBlkioStatsEntry\x12\x14\n" + + "\x05major\x18\x01 \x01(\x04R\x05major\x12\x14\n" + + "\x05minor\x18\x02 \x01(\x04R\x05minor\x12\x0e\n" + + "\x02op\x18\x03 \x01(\tR\x02op\x12\x14\n" + + "\x05value\x18\x04 \x01(\x04R\x05value\"\xde\x04\n" + + "\n" + + "BlkioStats\x12R\n" + + "\x1aio_service_bytes_recursive\x18\x01 \x03(\v2\x15.grpc.BlkioStatsEntryR\x17ioServiceBytesRecursive\x12I\n" + + "\x15io_serviced_recursive\x18\x02 \x03(\v2\x15.grpc.BlkioStatsEntryR\x13ioServicedRecursive\x12E\n" + + "\x13io_queued_recursive\x18\x03 \x03(\v2\x15.grpc.BlkioStatsEntryR\x11ioQueuedRecursive\x12P\n" + + "\x19io_service_time_recursive\x18\x04 \x03(\v2\x15.grpc.BlkioStatsEntryR\x16ioServiceTimeRecursive\x12J\n" + + "\x16io_wait_time_recursive\x18\x05 \x03(\v2\x15.grpc.BlkioStatsEntryR\x13ioWaitTimeRecursive\x12E\n" + + "\x13io_merged_recursive\x18\x06 \x03(\v2\x15.grpc.BlkioStatsEntryR\x11ioMergedRecursive\x12A\n" + + "\x11io_time_recursive\x18\a \x03(\v2\x15.grpc.BlkioStatsEntryR\x0fioTimeRecursive\x12B\n" + + "\x11sectors_recursive\x18\b \x03(\v2\x15.grpc.BlkioStatsEntryR\x10sectorsRecursive\"[\n" + + "\fHugetlbStats\x12\x14\n" + + "\x05usage\x18\x01 \x01(\x04R\x05usage\x12\x1b\n" + + "\tmax_usage\x18\x02 \x01(\x04R\bmaxUsage\x12\x18\n" + + "\afailcnt\x18\x03 \x01(\x04R\afailcnt\"\xf2\x02\n" + + "\vCgroupStats\x12+\n" + + "\tcpu_stats\x18\x01 \x01(\v2\x0e.grpc.CpuStatsR\bcpuStats\x124\n" + + "\fmemory_stats\x18\x02 \x01(\v2\x11.grpc.MemoryStatsR\vmemoryStats\x12.\n" + + "\n" + + "pids_stats\x18\x03 \x01(\v2\x0f.grpc.PidsStatsR\tpidsStats\x121\n" + + "\vblkio_stats\x18\x04 \x01(\v2\x10.grpc.BlkioStatsR\n" + + "blkioStats\x12H\n" + + "\rhugetlb_stats\x18\x05 \x03(\v2#.grpc.CgroupStats.HugetlbStatsEntryR\fhugetlbStats\x1aS\n" + + "\x11HugetlbStatsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12(\n" + + "\x05value\x18\x02 \x01(\v2\x12.grpc.HugetlbStatsR\x05value:\x028\x01\"\x8e\x02\n" + + "\fNetworkStats\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + + "\brx_bytes\x18\x02 \x01(\x04R\arxBytes\x12\x1d\n" + + "\n" + + "rx_packets\x18\x03 \x01(\x04R\trxPackets\x12\x1b\n" + + "\trx_errors\x18\x04 \x01(\x04R\brxErrors\x12\x1d\n" + + "\n" + + "rx_dropped\x18\x05 \x01(\x04R\trxDropped\x12\x19\n" + + "\btx_bytes\x18\x06 \x01(\x04R\atxBytes\x12\x1d\n" + + "\n" + + "tx_packets\x18\a \x01(\x04R\ttxPackets\x12\x1b\n" + + "\ttx_errors\x18\b \x01(\x04R\btxErrors\x12\x1d\n" + + "\n" + + "tx_dropped\x18\t \x01(\x04R\ttxDropped\"\x87\x01\n" + + "\x16StatsContainerResponse\x124\n" + + "\fcgroup_stats\x18\x01 \x01(\v2\x11.grpc.CgroupStatsR\vcgroupStats\x127\n" + + "\rnetwork_stats\x18\x02 \x03(\v2\x12.grpc.NetworkStatsR\fnetworkStats\"d\n" + + "\x12WriteStreamRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\"'\n" + + "\x13WriteStreamResponse\x12\x10\n" + + "\x03len\x18\x01 \x01(\rR\x03len\"a\n" + + "\x11ReadStreamRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x12\x10\n" + + "\x03len\x18\x03 \x01(\rR\x03len\"(\n" + + "\x12ReadStreamResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"O\n" + + "\x11CloseStdinRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\"{\n" + + "\x13TtyWinResizeRequest\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\x12\x17\n" + + "\aexec_id\x18\x02 \x01(\tR\x06execId\x12\x10\n" + + "\x03row\x18\x03 \x01(\rR\x03row\x12\x16\n" + + "\x06column\x18\x04 \x01(\rR\x06column\"B\n" + + "\fKernelModule\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + + "\n" + + "parameters\x18\x02 \x03(\tR\n" + + "parameters\"\x96\x02\n" + + "\x14CreateSandboxRequest\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x10\n" + + "\x03dns\x18\x02 \x03(\tR\x03dns\x12)\n" + + "\bstorages\x18\x03 \x03(\v2\r.grpc.StorageR\bstorages\x12#\n" + + "\rsandbox_pidns\x18\x04 \x01(\bR\fsandboxPidns\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x05 \x01(\tR\tsandboxId\x12&\n" + + "\x0fguest_hook_path\x18\x06 \x01(\tR\rguestHookPath\x129\n" + + "\x0ekernel_modules\x18\a \x03(\v2\x12.grpc.KernelModuleR\rkernelModules\"\x17\n" + + "\x15DestroySandboxRequest\"'\n" + + "%RemoveStaleVirtiofsShareMountsRequest\">\n" + + "\n" + + "Interfaces\x120\n" + + "\n" + + "Interfaces\x18\x01 \x03(\v2\x10.types.InterfaceR\n" + + "Interfaces\".\n" + + "\x06Routes\x12$\n" + + "\x06Routes\x18\x01 \x03(\v2\f.types.RouteR\x06Routes\"H\n" + + "\x16UpdateInterfaceRequest\x12.\n" + + "\tinterface\x18\x01 \x01(\v2\x10.types.InterfaceR\tinterface\";\n" + + "\x13UpdateRoutesRequest\x12$\n" + + "\x06routes\x18\x01 \x01(\v2\f.grpc.RoutesR\x06routes\"I\n" + + "\x1cUpdateEphemeralMountsRequest\x12)\n" + + "\bstorages\x18\x01 \x03(\v2\r.grpc.StorageR\bstorages\"\x17\n" + + "\x15ListInterfacesRequest\"\x13\n" + + "\x11ListRoutesRequest\"F\n" + + "\fARPNeighbors\x126\n" + + "\fARPNeighbors\x18\x01 \x03(\v2\x12.types.ARPNeighborR\fARPNeighbors\"J\n" + + "\x16AddARPNeighborsRequest\x120\n" + + "\tneighbors\x18\x01 \x01(\v2\x12.grpc.ARPNeighborsR\tneighbors\"-\n" + + "\x12GetIPTablesRequest\x12\x17\n" + + "\ais_ipv6\x18\x01 \x01(\bR\x06isIpv6\")\n" + + "\x13GetIPTablesResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"A\n" + + "\x12SetIPTablesRequest\x12\x17\n" + + "\ais_ipv6\x18\x01 \x01(\bR\x06isIpv6\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\")\n" + + "\x13SetIPTablesResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"]\n" + + "\x13OnlineCPUMemRequest\x12\x12\n" + + "\x04wait\x18\x01 \x01(\bR\x04wait\x12\x17\n" + + "\anb_cpus\x18\x02 \x01(\rR\x06nbCpus\x12\x19\n" + + "\bcpu_only\x18\x03 \x01(\bR\acpuOnly\",\n" + + "\x16ReseedRandomDevRequest\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\"\xef\x01\n" + + "\fAgentDetails\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x1f\n" + + "\vinit_daemon\x18\x02 \x01(\bR\n" + + "initDaemon\x12'\n" + + "\x0fdevice_handlers\x18\x03 \x03(\tR\x0edeviceHandlers\x12)\n" + + "\x10storage_handlers\x18\x04 \x03(\tR\x0fstorageHandlers\x12)\n" + + "\x10supports_seccomp\x18\x05 \x01(\bR\x0fsupportsSeccomp\x12%\n" + + "\x0eextra_features\x18\x06 \x03(\tR\rextraFeatures\"g\n" + + "\x13GuestDetailsRequest\x12$\n" + + "\x0emem_block_size\x18\x01 \x01(\bR\fmemBlockSize\x12*\n" + + "\x11mem_hotplug_probe\x18\x02 \x01(\bR\x0fmemHotplugProbe\"\xbb\x01\n" + + "\x14GuestDetailsResponse\x12/\n" + + "\x14mem_block_size_bytes\x18\x01 \x01(\x04R\x11memBlockSizeBytes\x127\n" + + "\ragent_details\x18\x02 \x01(\v2\x12.grpc.AgentDetailsR\fagentDetails\x129\n" + + "\x19support_mem_hotplug_probe\x18\x03 \x01(\bR\x16supportMemHotplugProbe\"L\n" + + "\x18MemHotplugByProbeRequest\x120\n" + + "\x13memHotplugProbeAddr\x18\x01 \x03(\x04R\x13memHotplugProbeAddr\"?\n" + + "\x17SetGuestDateTimeRequest\x12\x10\n" + + "\x03Sec\x18\x01 \x01(\x03R\x03Sec\x12\x12\n" + + "\x04Usec\x18\x02 \x01(\x03R\x04Usec\"p\n" + + "\aFSGroup\x12\x19\n" + + "\bgroup_id\x18\x02 \x01(\rR\agroupId\x12J\n" + + "\x13group_change_policy\x18\x03 \x01(\x0e2\x1a.types.FSGroupChangePolicyR\x11groupChangePolicy\"\x89\x01\n" + + "\vSharedMount\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x17\n" + + "\asrc_ctr\x18\x02 \x01(\tR\x06srcCtr\x12\x19\n" + + "\bsrc_path\x18\x03 \x01(\tR\asrcPath\x12\x17\n" + + "\adst_ctr\x18\x04 \x01(\tR\x06dstCtr\x12\x19\n" + + "\bdst_path\x18\x05 \x01(\tR\adstPath\"\xf5\x01\n" + + "\aStorage\x12\x16\n" + + "\x06driver\x18\x01 \x01(\tR\x06driver\x12%\n" + + "\x0edriver_options\x18\x02 \x03(\tR\rdriverOptions\x12\x16\n" + + "\x06source\x18\x03 \x01(\tR\x06source\x12\x16\n" + + "\x06fstype\x18\x04 \x01(\tR\x06fstype\x12\x18\n" + + "\aoptions\x18\x05 \x03(\tR\aoptions\x12\x1f\n" + + "\vmount_point\x18\x06 \x01(\tR\n" + + "mountPoint\x12(\n" + + "\bfs_group\x18\a \x01(\v2\r.grpc.FSGroupR\afsGroup\x12\x16\n" + + "\x06shared\x18\b \x01(\bR\x06shared\"\x86\x01\n" + + "\x06Device\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x17\n" + + "\avm_path\x18\x03 \x01(\tR\x06vmPath\x12%\n" + + "\x0econtainer_path\x18\x04 \x01(\tR\rcontainerPath\x12\x18\n" + + "\aoptions\x18\x05 \x03(\tR\aoptions\"X\n" + + "\n" + + "StringUser\x12\x10\n" + + "\x03uid\x18\x01 \x01(\tR\x03uid\x12\x10\n" + + "\x03gid\x18\x02 \x01(\tR\x03gid\x12&\n" + + "\x0eadditionalGids\x18\x03 \x03(\tR\x0eadditionalGids\"\xca\x01\n" + + "\x0fCopyFileRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x1b\n" + + "\tfile_size\x18\x02 \x01(\x03R\bfileSize\x12\x1b\n" + + "\tfile_mode\x18\x03 \x01(\rR\bfileMode\x12\x19\n" + + "\bdir_mode\x18\x04 \x01(\rR\adirMode\x12\x10\n" + + "\x03uid\x18\x05 \x01(\x05R\x03uid\x12\x10\n" + + "\x03gid\x18\x06 \x01(\x05R\x03gid\x12\x16\n" + + "\x06offset\x18\a \x01(\x03R\x06offset\x12\x12\n" + + "\x04data\x18\b \x01(\fR\x04data\"\x14\n" + + "\x12GetOOMEventRequest\"-\n" + + "\bOOMEvent\x12!\n" + + "\fcontainer_id\x18\x01 \x01(\tR\vcontainerId\"*\n" + + "\x0eAddSwapRequest\x12\x18\n" + + "\aPCIPath\x18\x01 \x03(\rR\aPCIPath\"(\n" + + "\x12AddSwapPathRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\"\x13\n" + + "\x11GetMetricsRequest\"#\n" + + "\aMetrics\x12\x18\n" + + "\ametrics\x18\x01 \x01(\tR\ametrics\"@\n" + + "\x12VolumeStatsRequest\x12*\n" + + "\x11volume_guest_path\x18\x01 \x01(\tR\x0fvolumeGuestPath\"U\n" + + "\x13ResizeVolumeRequest\x12*\n" + + "\x11volume_guest_path\x18\x01 \x01(\tR\x0fvolumeGuestPath\x12\x12\n" + + "\x04size\x18\x02 \x01(\x04R\x04size\"*\n" + + "\x10SetPolicyRequest\x12\x16\n" + + "\x06policy\x18\x01 \x01(\tR\x06policy\"X\n" + + "\x18GetDiagnosticDataRequest\x12\x19\n" + + "\blog_type\x18\x01 \x01(\tR\alogType\x12!\n" + + "\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\"/\n" + + "\x19GetDiagnosticDataResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\tR\x04data\"\xfc\x03\n" + + "\x13MemAgentMemcgConfig\x12\x1f\n" + + "\bdisabled\x18\x01 \x01(\bH\x00R\bdisabled\x88\x01\x01\x12\x17\n" + + "\x04swap\x18\x02 \x01(\bH\x01R\x04swap\x88\x01\x01\x12*\n" + + "\x0eswappiness_max\x18\x03 \x01(\rH\x02R\rswappinessMax\x88\x01\x01\x12$\n" + + "\vperiod_secs\x18\x04 \x01(\x04H\x03R\n" + + "periodSecs\x88\x01\x01\x12<\n" + + "\x18period_psi_percent_limit\x18\x05 \x01(\rH\x04R\x15periodPsiPercentLimit\x88\x01\x01\x12@\n" + + "\x1aeviction_psi_percent_limit\x18\x06 \x01(\rH\x05R\x17evictionPsiPercentLimit\x88\x01\x01\x12C\n" + + "\x1ceviction_run_aging_count_min\x18\a \x01(\x04H\x06R\x18evictionRunAgingCountMin\x88\x01\x01B\v\n" + + "\t_disabledB\a\n" + + "\x05_swapB\x11\n" + + "\x0f_swappiness_maxB\x0e\n" + + "\f_period_secsB\x1b\n" + + "\x19_period_psi_percent_limitB\x1d\n" + + "\x1b_eviction_psi_percent_limitB\x1f\n" + + "\x1d_eviction_run_aging_count_min\"\xc6\x04\n" + + "\x15MemAgentCompactConfig\x12\x1f\n" + + "\bdisabled\x18\x01 \x01(\bH\x00R\bdisabled\x88\x01\x01\x12$\n" + + "\vperiod_secs\x18\x02 \x01(\x04H\x01R\n" + + "periodSecs\x88\x01\x01\x12<\n" + + "\x18period_psi_percent_limit\x18\x03 \x01(\rH\x02R\x15periodPsiPercentLimit\x88\x01\x01\x12>\n" + + "\x19compact_psi_percent_limit\x18\x04 \x01(\rH\x03R\x16compactPsiPercentLimit\x88\x01\x01\x12+\n" + + "\x0fcompact_sec_max\x18\x05 \x01(\x03H\x04R\rcompactSecMax\x88\x01\x01\x12(\n" + + "\rcompact_order\x18\x06 \x01(\rH\x05R\fcompactOrder\x88\x01\x01\x120\n" + + "\x11compact_threshold\x18\a \x01(\x04H\x06R\x10compactThreshold\x88\x01\x01\x123\n" + + "\x13compact_force_times\x18\b \x01(\x04H\aR\x11compactForceTimes\x88\x01\x01B\v\n" + + "\t_disabledB\x0e\n" + + "\f_period_secsB\x1b\n" + + "\x19_period_psi_percent_limitB\x1c\n" + + "\x1a_compact_psi_percent_limitB\x12\n" + + "\x10_compact_sec_maxB\x10\n" + + "\x0e_compact_orderB\x14\n" + + "\x12_compact_thresholdB\x16\n" + + "\x14_compact_force_times2\xea\x16\n" + + "\fAgentService\x12G\n" + + "\x0fCreateContainer\x12\x1c.grpc.CreateContainerRequest\x1a\x16.google.protobuf.Empty\x12E\n" + + "\x0eStartContainer\x12\x1b.grpc.StartContainerRequest\x1a\x16.google.protobuf.Empty\x12G\n" + + "\x0fRemoveContainer\x12\x1c.grpc.RemoveContainerRequest\x1a\x16.google.protobuf.Empty\x12?\n" + + "\vExecProcess\x12\x18.grpc.ExecProcessRequest\x1a\x16.google.protobuf.Empty\x12C\n" + + "\rSignalProcess\x12\x1a.grpc.SignalProcessRequest\x1a\x16.google.protobuf.Empty\x12B\n" + + "\vWaitProcess\x12\x18.grpc.WaitProcessRequest\x1a\x19.grpc.WaitProcessResponse\x12G\n" + + "\x0fUpdateContainer\x12\x1c.grpc.UpdateContainerRequest\x1a\x16.google.protobuf.Empty\x12S\n" + + "\x15UpdateEphemeralMounts\x12\".grpc.UpdateEphemeralMountsRequest\x1a\x16.google.protobuf.Empty\x12K\n" + + "\x0eStatsContainer\x12\x1b.grpc.StatsContainerRequest\x1a\x1c.grpc.StatsContainerResponse\x12E\n" + + "\x0ePauseContainer\x12\x1b.grpc.PauseContainerRequest\x1a\x16.google.protobuf.Empty\x12G\n" + + "\x0fResumeContainer\x12\x1c.grpc.ResumeContainerRequest\x1a\x16.google.protobuf.Empty\x12e\n" + + "\x1eRemoveStaleVirtiofsShareMounts\x12+.grpc.RemoveStaleVirtiofsShareMountsRequest\x1a\x16.google.protobuf.Empty\x12T\n" + + "\x11GetDiagnosticData\x12\x1e.grpc.GetDiagnosticDataRequest\x1a\x1f.grpc.GetDiagnosticDataResponse\x12A\n" + + "\n" + + "WriteStdin\x12\x18.grpc.WriteStreamRequest\x1a\x19.grpc.WriteStreamResponse\x12?\n" + + "\n" + + "ReadStdout\x12\x17.grpc.ReadStreamRequest\x1a\x18.grpc.ReadStreamResponse\x12?\n" + + "\n" + + "ReadStderr\x12\x17.grpc.ReadStreamRequest\x1a\x18.grpc.ReadStreamResponse\x12=\n" + + "\n" + + "CloseStdin\x12\x17.grpc.CloseStdinRequest\x1a\x16.google.protobuf.Empty\x12A\n" + + "\fTtyWinResize\x12\x19.grpc.TtyWinResizeRequest\x1a\x16.google.protobuf.Empty\x12A\n" + + "\x0fUpdateInterface\x12\x1c.grpc.UpdateInterfaceRequest\x1a\x10.types.Interface\x127\n" + + "\fUpdateRoutes\x12\x19.grpc.UpdateRoutesRequest\x1a\f.grpc.Routes\x12?\n" + + "\x0eListInterfaces\x12\x1b.grpc.ListInterfacesRequest\x1a\x10.grpc.Interfaces\x123\n" + + "\n" + + "ListRoutes\x12\x17.grpc.ListRoutesRequest\x1a\f.grpc.Routes\x12G\n" + + "\x0fAddARPNeighbors\x12\x1c.grpc.AddARPNeighborsRequest\x1a\x16.google.protobuf.Empty\x12B\n" + + "\vGetIPTables\x12\x18.grpc.GetIPTablesRequest\x1a\x19.grpc.GetIPTablesResponse\x12B\n" + + "\vSetIPTables\x12\x18.grpc.SetIPTablesRequest\x1a\x19.grpc.SetIPTablesResponse\x124\n" + + "\n" + + "GetMetrics\x12\x17.grpc.GetMetricsRequest\x1a\r.grpc.Metrics\x12E\n" + + "\x10MemAgentMemcgSet\x12\x19.grpc.MemAgentMemcgConfig\x1a\x16.google.protobuf.Empty\x12I\n" + + "\x12MemAgentCompactSet\x12\x1b.grpc.MemAgentCompactConfig\x1a\x16.google.protobuf.Empty\x12C\n" + + "\rCreateSandbox\x12\x1a.grpc.CreateSandboxRequest\x1a\x16.google.protobuf.Empty\x12E\n" + + "\x0eDestroySandbox\x12\x1b.grpc.DestroySandboxRequest\x1a\x16.google.protobuf.Empty\x12A\n" + + "\fOnlineCPUMem\x12\x19.grpc.OnlineCPUMemRequest\x1a\x16.google.protobuf.Empty\x12G\n" + + "\x0fReseedRandomDev\x12\x1c.grpc.ReseedRandomDevRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x0fGetGuestDetails\x12\x19.grpc.GuestDetailsRequest\x1a\x1a.grpc.GuestDetailsResponse\x12K\n" + + "\x11MemHotplugByProbe\x12\x1e.grpc.MemHotplugByProbeRequest\x1a\x16.google.protobuf.Empty\x12I\n" + + "\x10SetGuestDateTime\x12\x1d.grpc.SetGuestDateTimeRequest\x1a\x16.google.protobuf.Empty\x129\n" + + "\bCopyFile\x12\x15.grpc.CopyFileRequest\x1a\x16.google.protobuf.Empty\x127\n" + + "\vGetOOMEvent\x12\x18.grpc.GetOOMEventRequest\x1a\x0e.grpc.OOMEvent\x127\n" + + "\aAddSwap\x12\x14.grpc.AddSwapRequest\x1a\x16.google.protobuf.Empty\x12?\n" + + "\vAddSwapPath\x12\x18.grpc.AddSwapPathRequest\x1a\x16.google.protobuf.Empty\x12E\n" + + "\x0eGetVolumeStats\x12\x18.grpc.VolumeStatsRequest\x1a\x19.grpc.VolumeStatsResponse\x12A\n" + + "\fResizeVolume\x12\x19.grpc.ResizeVolumeRequest\x1a\x16.google.protobuf.Empty\x12;\n" + + "\tSetPolicy\x12\x16.grpc.SetPolicyRequest\x1a\x16.google.protobuf.EmptyBVZTgithub.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata/agentpb;agentpbb\x06proto3" + +var ( + file_agent_proto_rawDescOnce sync.Once + file_agent_proto_rawDescData []byte +) + +func file_agent_proto_rawDescGZIP() []byte { + file_agent_proto_rawDescOnce.Do(func() { + file_agent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc))) + }) + return file_agent_proto_rawDescData +} + +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 74) +var file_agent_proto_goTypes = []any{ + (*CreateContainerRequest)(nil), // 0: grpc.CreateContainerRequest + (*StartContainerRequest)(nil), // 1: grpc.StartContainerRequest + (*RemoveContainerRequest)(nil), // 2: grpc.RemoveContainerRequest + (*ExecProcessRequest)(nil), // 3: grpc.ExecProcessRequest + (*SignalProcessRequest)(nil), // 4: grpc.SignalProcessRequest + (*WaitProcessRequest)(nil), // 5: grpc.WaitProcessRequest + (*WaitProcessResponse)(nil), // 6: grpc.WaitProcessResponse + (*UpdateContainerRequest)(nil), // 7: grpc.UpdateContainerRequest + (*StatsContainerRequest)(nil), // 8: grpc.StatsContainerRequest + (*PauseContainerRequest)(nil), // 9: grpc.PauseContainerRequest + (*ResumeContainerRequest)(nil), // 10: grpc.ResumeContainerRequest + (*CpuUsage)(nil), // 11: grpc.CpuUsage + (*ThrottlingData)(nil), // 12: grpc.ThrottlingData + (*CpuStats)(nil), // 13: grpc.CpuStats + (*PidsStats)(nil), // 14: grpc.PidsStats + (*MemoryData)(nil), // 15: grpc.MemoryData + (*MemoryStats)(nil), // 16: grpc.MemoryStats + (*BlkioStatsEntry)(nil), // 17: grpc.BlkioStatsEntry + (*BlkioStats)(nil), // 18: grpc.BlkioStats + (*HugetlbStats)(nil), // 19: grpc.HugetlbStats + (*CgroupStats)(nil), // 20: grpc.CgroupStats + (*NetworkStats)(nil), // 21: grpc.NetworkStats + (*StatsContainerResponse)(nil), // 22: grpc.StatsContainerResponse + (*WriteStreamRequest)(nil), // 23: grpc.WriteStreamRequest + (*WriteStreamResponse)(nil), // 24: grpc.WriteStreamResponse + (*ReadStreamRequest)(nil), // 25: grpc.ReadStreamRequest + (*ReadStreamResponse)(nil), // 26: grpc.ReadStreamResponse + (*CloseStdinRequest)(nil), // 27: grpc.CloseStdinRequest + (*TtyWinResizeRequest)(nil), // 28: grpc.TtyWinResizeRequest + (*KernelModule)(nil), // 29: grpc.KernelModule + (*CreateSandboxRequest)(nil), // 30: grpc.CreateSandboxRequest + (*DestroySandboxRequest)(nil), // 31: grpc.DestroySandboxRequest + (*RemoveStaleVirtiofsShareMountsRequest)(nil), // 32: grpc.RemoveStaleVirtiofsShareMountsRequest + (*Interfaces)(nil), // 33: grpc.Interfaces + (*Routes)(nil), // 34: grpc.Routes + (*UpdateInterfaceRequest)(nil), // 35: grpc.UpdateInterfaceRequest + (*UpdateRoutesRequest)(nil), // 36: grpc.UpdateRoutesRequest + (*UpdateEphemeralMountsRequest)(nil), // 37: grpc.UpdateEphemeralMountsRequest + (*ListInterfacesRequest)(nil), // 38: grpc.ListInterfacesRequest + (*ListRoutesRequest)(nil), // 39: grpc.ListRoutesRequest + (*ARPNeighbors)(nil), // 40: grpc.ARPNeighbors + (*AddARPNeighborsRequest)(nil), // 41: grpc.AddARPNeighborsRequest + (*GetIPTablesRequest)(nil), // 42: grpc.GetIPTablesRequest + (*GetIPTablesResponse)(nil), // 43: grpc.GetIPTablesResponse + (*SetIPTablesRequest)(nil), // 44: grpc.SetIPTablesRequest + (*SetIPTablesResponse)(nil), // 45: grpc.SetIPTablesResponse + (*OnlineCPUMemRequest)(nil), // 46: grpc.OnlineCPUMemRequest + (*ReseedRandomDevRequest)(nil), // 47: grpc.ReseedRandomDevRequest + (*AgentDetails)(nil), // 48: grpc.AgentDetails + (*GuestDetailsRequest)(nil), // 49: grpc.GuestDetailsRequest + (*GuestDetailsResponse)(nil), // 50: grpc.GuestDetailsResponse + (*MemHotplugByProbeRequest)(nil), // 51: grpc.MemHotplugByProbeRequest + (*SetGuestDateTimeRequest)(nil), // 52: grpc.SetGuestDateTimeRequest + (*FSGroup)(nil), // 53: grpc.FSGroup + (*SharedMount)(nil), // 54: grpc.SharedMount + (*Storage)(nil), // 55: grpc.Storage + (*Device)(nil), // 56: grpc.Device + (*StringUser)(nil), // 57: grpc.StringUser + (*CopyFileRequest)(nil), // 58: grpc.CopyFileRequest + (*GetOOMEventRequest)(nil), // 59: grpc.GetOOMEventRequest + (*OOMEvent)(nil), // 60: grpc.OOMEvent + (*AddSwapRequest)(nil), // 61: grpc.AddSwapRequest + (*AddSwapPathRequest)(nil), // 62: grpc.AddSwapPathRequest + (*GetMetricsRequest)(nil), // 63: grpc.GetMetricsRequest + (*Metrics)(nil), // 64: grpc.Metrics + (*VolumeStatsRequest)(nil), // 65: grpc.VolumeStatsRequest + (*ResizeVolumeRequest)(nil), // 66: grpc.ResizeVolumeRequest + (*SetPolicyRequest)(nil), // 67: grpc.SetPolicyRequest + (*GetDiagnosticDataRequest)(nil), // 68: grpc.GetDiagnosticDataRequest + (*GetDiagnosticDataResponse)(nil), // 69: grpc.GetDiagnosticDataResponse + (*MemAgentMemcgConfig)(nil), // 70: grpc.MemAgentMemcgConfig + (*MemAgentCompactConfig)(nil), // 71: grpc.MemAgentCompactConfig + nil, // 72: grpc.MemoryStats.StatsEntry + nil, // 73: grpc.CgroupStats.HugetlbStatsEntry + (*Spec)(nil), // 74: grpc.Spec + (*Process)(nil), // 75: grpc.Process + (*LinuxResources)(nil), // 76: grpc.LinuxResources + (*Interface)(nil), // 77: types.Interface + (*Route)(nil), // 78: types.Route + (*ARPNeighbor)(nil), // 79: types.ARPNeighbor + (FSGroupChangePolicy)(0), // 80: types.FSGroupChangePolicy + (*emptypb.Empty)(nil), // 81: google.protobuf.Empty + (*VolumeStatsResponse)(nil), // 82: grpc.VolumeStatsResponse +} +var file_agent_proto_depIdxs = []int32{ + 57, // 0: grpc.CreateContainerRequest.string_user:type_name -> grpc.StringUser + 56, // 1: grpc.CreateContainerRequest.devices:type_name -> grpc.Device + 55, // 2: grpc.CreateContainerRequest.storages:type_name -> grpc.Storage + 74, // 3: grpc.CreateContainerRequest.OCI:type_name -> grpc.Spec + 54, // 4: grpc.CreateContainerRequest.shared_mounts:type_name -> grpc.SharedMount + 57, // 5: grpc.ExecProcessRequest.string_user:type_name -> grpc.StringUser + 75, // 6: grpc.ExecProcessRequest.process:type_name -> grpc.Process + 76, // 7: grpc.UpdateContainerRequest.resources:type_name -> grpc.LinuxResources + 11, // 8: grpc.CpuStats.cpu_usage:type_name -> grpc.CpuUsage + 12, // 9: grpc.CpuStats.throttling_data:type_name -> grpc.ThrottlingData + 15, // 10: grpc.MemoryStats.usage:type_name -> grpc.MemoryData + 15, // 11: grpc.MemoryStats.swap_usage:type_name -> grpc.MemoryData + 15, // 12: grpc.MemoryStats.kernel_usage:type_name -> grpc.MemoryData + 72, // 13: grpc.MemoryStats.stats:type_name -> grpc.MemoryStats.StatsEntry + 17, // 14: grpc.BlkioStats.io_service_bytes_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 15: grpc.BlkioStats.io_serviced_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 16: grpc.BlkioStats.io_queued_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 17: grpc.BlkioStats.io_service_time_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 18: grpc.BlkioStats.io_wait_time_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 19: grpc.BlkioStats.io_merged_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 20: grpc.BlkioStats.io_time_recursive:type_name -> grpc.BlkioStatsEntry + 17, // 21: grpc.BlkioStats.sectors_recursive:type_name -> grpc.BlkioStatsEntry + 13, // 22: grpc.CgroupStats.cpu_stats:type_name -> grpc.CpuStats + 16, // 23: grpc.CgroupStats.memory_stats:type_name -> grpc.MemoryStats + 14, // 24: grpc.CgroupStats.pids_stats:type_name -> grpc.PidsStats + 18, // 25: grpc.CgroupStats.blkio_stats:type_name -> grpc.BlkioStats + 73, // 26: grpc.CgroupStats.hugetlb_stats:type_name -> grpc.CgroupStats.HugetlbStatsEntry + 20, // 27: grpc.StatsContainerResponse.cgroup_stats:type_name -> grpc.CgroupStats + 21, // 28: grpc.StatsContainerResponse.network_stats:type_name -> grpc.NetworkStats + 55, // 29: grpc.CreateSandboxRequest.storages:type_name -> grpc.Storage + 29, // 30: grpc.CreateSandboxRequest.kernel_modules:type_name -> grpc.KernelModule + 77, // 31: grpc.Interfaces.Interfaces:type_name -> types.Interface + 78, // 32: grpc.Routes.Routes:type_name -> types.Route + 77, // 33: grpc.UpdateInterfaceRequest.interface:type_name -> types.Interface + 34, // 34: grpc.UpdateRoutesRequest.routes:type_name -> grpc.Routes + 55, // 35: grpc.UpdateEphemeralMountsRequest.storages:type_name -> grpc.Storage + 79, // 36: grpc.ARPNeighbors.ARPNeighbors:type_name -> types.ARPNeighbor + 40, // 37: grpc.AddARPNeighborsRequest.neighbors:type_name -> grpc.ARPNeighbors + 48, // 38: grpc.GuestDetailsResponse.agent_details:type_name -> grpc.AgentDetails + 80, // 39: grpc.FSGroup.group_change_policy:type_name -> types.FSGroupChangePolicy + 53, // 40: grpc.Storage.fs_group:type_name -> grpc.FSGroup + 19, // 41: grpc.CgroupStats.HugetlbStatsEntry.value:type_name -> grpc.HugetlbStats + 0, // 42: grpc.AgentService.CreateContainer:input_type -> grpc.CreateContainerRequest + 1, // 43: grpc.AgentService.StartContainer:input_type -> grpc.StartContainerRequest + 2, // 44: grpc.AgentService.RemoveContainer:input_type -> grpc.RemoveContainerRequest + 3, // 45: grpc.AgentService.ExecProcess:input_type -> grpc.ExecProcessRequest + 4, // 46: grpc.AgentService.SignalProcess:input_type -> grpc.SignalProcessRequest + 5, // 47: grpc.AgentService.WaitProcess:input_type -> grpc.WaitProcessRequest + 7, // 48: grpc.AgentService.UpdateContainer:input_type -> grpc.UpdateContainerRequest + 37, // 49: grpc.AgentService.UpdateEphemeralMounts:input_type -> grpc.UpdateEphemeralMountsRequest + 8, // 50: grpc.AgentService.StatsContainer:input_type -> grpc.StatsContainerRequest + 9, // 51: grpc.AgentService.PauseContainer:input_type -> grpc.PauseContainerRequest + 10, // 52: grpc.AgentService.ResumeContainer:input_type -> grpc.ResumeContainerRequest + 32, // 53: grpc.AgentService.RemoveStaleVirtiofsShareMounts:input_type -> grpc.RemoveStaleVirtiofsShareMountsRequest + 68, // 54: grpc.AgentService.GetDiagnosticData:input_type -> grpc.GetDiagnosticDataRequest + 23, // 55: grpc.AgentService.WriteStdin:input_type -> grpc.WriteStreamRequest + 25, // 56: grpc.AgentService.ReadStdout:input_type -> grpc.ReadStreamRequest + 25, // 57: grpc.AgentService.ReadStderr:input_type -> grpc.ReadStreamRequest + 27, // 58: grpc.AgentService.CloseStdin:input_type -> grpc.CloseStdinRequest + 28, // 59: grpc.AgentService.TtyWinResize:input_type -> grpc.TtyWinResizeRequest + 35, // 60: grpc.AgentService.UpdateInterface:input_type -> grpc.UpdateInterfaceRequest + 36, // 61: grpc.AgentService.UpdateRoutes:input_type -> grpc.UpdateRoutesRequest + 38, // 62: grpc.AgentService.ListInterfaces:input_type -> grpc.ListInterfacesRequest + 39, // 63: grpc.AgentService.ListRoutes:input_type -> grpc.ListRoutesRequest + 41, // 64: grpc.AgentService.AddARPNeighbors:input_type -> grpc.AddARPNeighborsRequest + 42, // 65: grpc.AgentService.GetIPTables:input_type -> grpc.GetIPTablesRequest + 44, // 66: grpc.AgentService.SetIPTables:input_type -> grpc.SetIPTablesRequest + 63, // 67: grpc.AgentService.GetMetrics:input_type -> grpc.GetMetricsRequest + 70, // 68: grpc.AgentService.MemAgentMemcgSet:input_type -> grpc.MemAgentMemcgConfig + 71, // 69: grpc.AgentService.MemAgentCompactSet:input_type -> grpc.MemAgentCompactConfig + 30, // 70: grpc.AgentService.CreateSandbox:input_type -> grpc.CreateSandboxRequest + 31, // 71: grpc.AgentService.DestroySandbox:input_type -> grpc.DestroySandboxRequest + 46, // 72: grpc.AgentService.OnlineCPUMem:input_type -> grpc.OnlineCPUMemRequest + 47, // 73: grpc.AgentService.ReseedRandomDev:input_type -> grpc.ReseedRandomDevRequest + 49, // 74: grpc.AgentService.GetGuestDetails:input_type -> grpc.GuestDetailsRequest + 51, // 75: grpc.AgentService.MemHotplugByProbe:input_type -> grpc.MemHotplugByProbeRequest + 52, // 76: grpc.AgentService.SetGuestDateTime:input_type -> grpc.SetGuestDateTimeRequest + 58, // 77: grpc.AgentService.CopyFile:input_type -> grpc.CopyFileRequest + 59, // 78: grpc.AgentService.GetOOMEvent:input_type -> grpc.GetOOMEventRequest + 61, // 79: grpc.AgentService.AddSwap:input_type -> grpc.AddSwapRequest + 62, // 80: grpc.AgentService.AddSwapPath:input_type -> grpc.AddSwapPathRequest + 65, // 81: grpc.AgentService.GetVolumeStats:input_type -> grpc.VolumeStatsRequest + 66, // 82: grpc.AgentService.ResizeVolume:input_type -> grpc.ResizeVolumeRequest + 67, // 83: grpc.AgentService.SetPolicy:input_type -> grpc.SetPolicyRequest + 81, // 84: grpc.AgentService.CreateContainer:output_type -> google.protobuf.Empty + 81, // 85: grpc.AgentService.StartContainer:output_type -> google.protobuf.Empty + 81, // 86: grpc.AgentService.RemoveContainer:output_type -> google.protobuf.Empty + 81, // 87: grpc.AgentService.ExecProcess:output_type -> google.protobuf.Empty + 81, // 88: grpc.AgentService.SignalProcess:output_type -> google.protobuf.Empty + 6, // 89: grpc.AgentService.WaitProcess:output_type -> grpc.WaitProcessResponse + 81, // 90: grpc.AgentService.UpdateContainer:output_type -> google.protobuf.Empty + 81, // 91: grpc.AgentService.UpdateEphemeralMounts:output_type -> google.protobuf.Empty + 22, // 92: grpc.AgentService.StatsContainer:output_type -> grpc.StatsContainerResponse + 81, // 93: grpc.AgentService.PauseContainer:output_type -> google.protobuf.Empty + 81, // 94: grpc.AgentService.ResumeContainer:output_type -> google.protobuf.Empty + 81, // 95: grpc.AgentService.RemoveStaleVirtiofsShareMounts:output_type -> google.protobuf.Empty + 69, // 96: grpc.AgentService.GetDiagnosticData:output_type -> grpc.GetDiagnosticDataResponse + 24, // 97: grpc.AgentService.WriteStdin:output_type -> grpc.WriteStreamResponse + 26, // 98: grpc.AgentService.ReadStdout:output_type -> grpc.ReadStreamResponse + 26, // 99: grpc.AgentService.ReadStderr:output_type -> grpc.ReadStreamResponse + 81, // 100: grpc.AgentService.CloseStdin:output_type -> google.protobuf.Empty + 81, // 101: grpc.AgentService.TtyWinResize:output_type -> google.protobuf.Empty + 77, // 102: grpc.AgentService.UpdateInterface:output_type -> types.Interface + 34, // 103: grpc.AgentService.UpdateRoutes:output_type -> grpc.Routes + 33, // 104: grpc.AgentService.ListInterfaces:output_type -> grpc.Interfaces + 34, // 105: grpc.AgentService.ListRoutes:output_type -> grpc.Routes + 81, // 106: grpc.AgentService.AddARPNeighbors:output_type -> google.protobuf.Empty + 43, // 107: grpc.AgentService.GetIPTables:output_type -> grpc.GetIPTablesResponse + 45, // 108: grpc.AgentService.SetIPTables:output_type -> grpc.SetIPTablesResponse + 64, // 109: grpc.AgentService.GetMetrics:output_type -> grpc.Metrics + 81, // 110: grpc.AgentService.MemAgentMemcgSet:output_type -> google.protobuf.Empty + 81, // 111: grpc.AgentService.MemAgentCompactSet:output_type -> google.protobuf.Empty + 81, // 112: grpc.AgentService.CreateSandbox:output_type -> google.protobuf.Empty + 81, // 113: grpc.AgentService.DestroySandbox:output_type -> google.protobuf.Empty + 81, // 114: grpc.AgentService.OnlineCPUMem:output_type -> google.protobuf.Empty + 81, // 115: grpc.AgentService.ReseedRandomDev:output_type -> google.protobuf.Empty + 50, // 116: grpc.AgentService.GetGuestDetails:output_type -> grpc.GuestDetailsResponse + 81, // 117: grpc.AgentService.MemHotplugByProbe:output_type -> google.protobuf.Empty + 81, // 118: grpc.AgentService.SetGuestDateTime:output_type -> google.protobuf.Empty + 81, // 119: grpc.AgentService.CopyFile:output_type -> google.protobuf.Empty + 60, // 120: grpc.AgentService.GetOOMEvent:output_type -> grpc.OOMEvent + 81, // 121: grpc.AgentService.AddSwap:output_type -> google.protobuf.Empty + 81, // 122: grpc.AgentService.AddSwapPath:output_type -> google.protobuf.Empty + 82, // 123: grpc.AgentService.GetVolumeStats:output_type -> grpc.VolumeStatsResponse + 81, // 124: grpc.AgentService.ResizeVolume:output_type -> google.protobuf.Empty + 81, // 125: grpc.AgentService.SetPolicy:output_type -> google.protobuf.Empty + 84, // [84:126] is the sub-list for method output_type + 42, // [42:84] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name +} + +func init() { file_agent_proto_init() } +func file_agent_proto_init() { + if File_agent_proto != nil { + return + } + file_oci_proto_init() + file_csi_proto_init() + file_types_proto_init() + file_agent_proto_msgTypes[70].OneofWrappers = []any{} + file_agent_proto_msgTypes[71].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), + NumEnums: 0, + NumMessages: 74, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_agent_proto_goTypes, + DependencyIndexes: file_agent_proto_depIdxs, + MessageInfos: file_agent_proto_msgTypes, + }.Build() + File_agent_proto = out.File + file_agent_proto_goTypes = nil + file_agent_proto_depIdxs = nil +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.proto b/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.proto new file mode 100644 index 000000000..d5719b494 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/agent.proto @@ -0,0 +1,658 @@ +// +// Copyright 2017 HyperHQ Inc. +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +syntax = "proto3"; + +option go_package = "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb;agentpb"; + +package grpc; + +import "oci.proto"; +import "csi.proto"; +import "types.proto"; + +import "google/protobuf/empty.proto"; + +// unstable +service AgentService { + // execution + rpc CreateContainer(CreateContainerRequest) returns (google.protobuf.Empty); + rpc StartContainer(StartContainerRequest) returns (google.protobuf.Empty); + + // RemoveContainer will tear down an existing container by forcibly terminating + // all processes running inside that container and releasing all internal + // resources associated with it. + // RemoveContainer will wait for all processes termination before returning. + // If any process can not be killed or if it can not be killed after + // the RemoveContainerRequest timeout, RemoveContainer will return an error. + rpc RemoveContainer(RemoveContainerRequest) returns (google.protobuf.Empty); + rpc ExecProcess(ExecProcessRequest) returns (google.protobuf.Empty); + rpc SignalProcess(SignalProcessRequest) returns (google.protobuf.Empty); + rpc WaitProcess(WaitProcessRequest) returns (WaitProcessResponse); // wait & reap like waitpid(2) + rpc UpdateContainer(UpdateContainerRequest) returns (google.protobuf.Empty); + rpc UpdateEphemeralMounts(UpdateEphemeralMountsRequest) returns (google.protobuf.Empty); + rpc StatsContainer(StatsContainerRequest) returns (StatsContainerResponse); + rpc PauseContainer(PauseContainerRequest) returns (google.protobuf.Empty); + rpc ResumeContainer(ResumeContainerRequest) returns (google.protobuf.Empty); + rpc RemoveStaleVirtiofsShareMounts(RemoveStaleVirtiofsShareMountsRequest) returns (google.protobuf.Empty); + rpc GetDiagnosticData(GetDiagnosticDataRequest) returns (GetDiagnosticDataResponse); + + // stdio + rpc WriteStdin(WriteStreamRequest) returns (WriteStreamResponse); + rpc ReadStdout(ReadStreamRequest) returns (ReadStreamResponse); + rpc ReadStderr(ReadStreamRequest) returns (ReadStreamResponse); + rpc CloseStdin(CloseStdinRequest) returns (google.protobuf.Empty); + rpc TtyWinResize(TtyWinResizeRequest) returns (google.protobuf.Empty); + + // networking + rpc UpdateInterface(UpdateInterfaceRequest) returns (types.Interface); + rpc UpdateRoutes(UpdateRoutesRequest) returns (Routes); + rpc ListInterfaces(ListInterfacesRequest) returns(Interfaces); + rpc ListRoutes(ListRoutesRequest) returns (Routes); + rpc AddARPNeighbors(AddARPNeighborsRequest) returns (google.protobuf.Empty); + rpc GetIPTables(GetIPTablesRequest) returns (GetIPTablesResponse); + rpc SetIPTables(SetIPTablesRequest) returns (SetIPTablesResponse); + + // observability + rpc GetMetrics(GetMetricsRequest) returns (Metrics); + + // mem-agent + rpc MemAgentMemcgSet(MemAgentMemcgConfig) returns (google.protobuf.Empty); + rpc MemAgentCompactSet(MemAgentCompactConfig) returns (google.protobuf.Empty); + + // misc (TODO: some rpcs can be replaced by hyperstart-exec) + rpc CreateSandbox(CreateSandboxRequest) returns (google.protobuf.Empty); + rpc DestroySandbox(DestroySandboxRequest) returns (google.protobuf.Empty); + rpc OnlineCPUMem(OnlineCPUMemRequest) returns (google.protobuf.Empty); + rpc ReseedRandomDev(ReseedRandomDevRequest) returns (google.protobuf.Empty); + rpc GetGuestDetails(GuestDetailsRequest) returns (GuestDetailsResponse); + rpc MemHotplugByProbe(MemHotplugByProbeRequest) returns (google.protobuf.Empty); + rpc SetGuestDateTime(SetGuestDateTimeRequest) returns (google.protobuf.Empty); + rpc CopyFile(CopyFileRequest) returns (google.protobuf.Empty); + rpc GetOOMEvent(GetOOMEventRequest) returns (OOMEvent); + rpc AddSwap(AddSwapRequest) returns (google.protobuf.Empty); + rpc AddSwapPath(AddSwapPathRequest) returns (google.protobuf.Empty); + rpc GetVolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse); + rpc ResizeVolume(ResizeVolumeRequest) returns (google.protobuf.Empty); + rpc SetPolicy(SetPolicyRequest) returns (google.protobuf.Empty); +} + +message CreateContainerRequest { + string container_id = 1; + string exec_id = 2; + StringUser string_user = 3; + repeated Device devices = 4; + repeated Storage storages = 5; + Spec OCI = 6; + + // This field is used to indicate if the container needs to join + // sandbox shared pid ns or create a new namespace. This field is + // meant to override the NEWPID config settings in the OCI spec. + // The agent would receive an OCI spec with PID namespace cleared + // out altogether and not just the pid ns path. + bool sandbox_pidns = 7; + + // This field is used to declare a set of shared mount points + // that support cross-container sharing of mount objects. + repeated SharedMount shared_mounts = 8; + + // These fields are the host-side vport numbers of passfd streams + // pre-created by runtime-rs, and used as identifiers for the agent + // to select the right streams for init process's stdin/stdout/stderr. + // Disable the feature by setting the associated port to 0. + uint32 stdin_port = 9; + uint32 stdout_port = 10; + uint32 stderr_port = 11; +} + +message StartContainerRequest { + string container_id = 1; +} + +message RemoveContainerRequest { + string container_id = 1; + + // RemoveContainer will return an error if + // it could not kill some container processes + // after timeout seconds. + // Setting timeout to 0 means RemoveContainer will + // wait for ever. + uint32 timeout = 2; +} + +message ExecProcessRequest { + string container_id = 1; + string exec_id = 2; + StringUser string_user = 3; + Process process = 4; + + // These fields are the host-side vport numbers of passfd streams + // pre-created by runtime-rs, and used as identifiers for the agent + // to select the right streams for process's stdin/stdout/stderr. + // Disable the feature by setting the associated port to 0. + uint32 stdin_port = 5; + uint32 stdout_port = 6; + uint32 stderr_port = 7; +} + +message SignalProcessRequest { + string container_id = 1; + + // Special case for SignalProcess(): exec_id can be empty(""), + // which means to send the signal to all the processes including their descendants. + // Other APIs with exec_id should treat empty exec_id as an invalid request. + string exec_id = 2; + uint32 signal = 3; +} + +message WaitProcessRequest { + string container_id = 1; + string exec_id = 2; +} + +message WaitProcessResponse { + int32 status = 1; +} + +message UpdateContainerRequest { + string container_id = 1; + LinuxResources resources = 2; +} + +message StatsContainerRequest { + string container_id = 1; +} + +message PauseContainerRequest { + string container_id = 1; +} + +message ResumeContainerRequest { + string container_id = 1; +} + +message CpuUsage { + uint64 total_usage = 1; + repeated uint64 percpu_usage = 2; + uint64 usage_in_kernelmode = 3; + uint64 usage_in_usermode = 4; +} + +message ThrottlingData { + uint64 periods = 1; + uint64 throttled_periods = 2; + uint64 throttled_time = 3; +} + +message CpuStats { + CpuUsage cpu_usage = 1; + ThrottlingData throttling_data = 2; +} + +message PidsStats { + uint64 current = 1; + uint64 limit = 2; +} + +message MemoryData { + uint64 usage = 1; + uint64 max_usage = 2; + uint64 failcnt = 3; + uint64 limit = 4; +} + +message MemoryStats { + uint64 cache = 1; + MemoryData usage = 2; + MemoryData swap_usage = 3; + MemoryData kernel_usage = 4; + bool use_hierarchy = 5; + map stats = 6; +} + + +message BlkioStatsEntry { + uint64 major = 1; + uint64 minor = 2; + string op = 3; + uint64 value = 4; +} + +message BlkioStats { + repeated BlkioStatsEntry io_service_bytes_recursive = 1; // number of bytes transferred to and from the block device + repeated BlkioStatsEntry io_serviced_recursive = 2; + repeated BlkioStatsEntry io_queued_recursive = 3; + repeated BlkioStatsEntry io_service_time_recursive = 4; + repeated BlkioStatsEntry io_wait_time_recursive = 5; + repeated BlkioStatsEntry io_merged_recursive = 6; + repeated BlkioStatsEntry io_time_recursive = 7; + repeated BlkioStatsEntry sectors_recursive = 8; +} + +message HugetlbStats { + uint64 usage = 1; + uint64 max_usage = 2; + uint64 failcnt = 3; +} + +message CgroupStats { + CpuStats cpu_stats = 1; + MemoryStats memory_stats = 2; + PidsStats pids_stats = 3; + BlkioStats blkio_stats = 4; + map hugetlb_stats = 5; // the map is in the format "size of hugepage: stats of the hugepage" + +} + +message NetworkStats { + string name = 1; + uint64 rx_bytes = 2; + uint64 rx_packets = 3; + uint64 rx_errors = 4; + uint64 rx_dropped = 5; + uint64 tx_bytes = 6; + uint64 tx_packets = 7; + uint64 tx_errors = 8; + uint64 tx_dropped = 9; +} + +message StatsContainerResponse { + CgroupStats cgroup_stats = 1; + repeated NetworkStats network_stats = 2; +} + +message WriteStreamRequest { + string container_id = 1; + string exec_id = 2; + bytes data = 3; +} + +message WriteStreamResponse { + uint32 len = 1; +} + +message ReadStreamRequest { + string container_id = 1; + string exec_id = 2; + uint32 len = 3; +} + +message ReadStreamResponse { + bytes data = 1; +} + +message CloseStdinRequest { + string container_id = 1; + string exec_id = 2; +} + +message TtyWinResizeRequest { + string container_id = 1; + string exec_id = 2; + uint32 row = 3; + uint32 column = 4; +} + +message KernelModule { + // This field is the name of the kernel module. + string name = 1; + // This field are the parameters for the kernel module which are + // whitespace-delimited key=value pairs passed to modprobe(8). + repeated string parameters = 2; +} + +message CreateSandboxRequest { + string hostname = 1; + repeated string dns = 2; + repeated Storage storages = 3; + + // This field means that a pause process needs to be created by the + // agent. This pid namespace of the pause process will be treated as + // a shared pid namespace. All containers created will join this shared + // pid namespace. + bool sandbox_pidns = 4; + // SandboxId identifies which sandbox is using the agent. We allow only + // one sandbox per agent and implicitly require that CreateSandbox is + // called before other sandbox/network calls. + string sandbox_id = 5; + // This field, if non-empty, designates an absolute path to a directory + // that the agent will search for OCI hooks to run within the guest. + string guest_hook_path = 6; + // This field is the list of kernel modules to be loaded in the guest kernel. + repeated KernelModule kernel_modules = 7; +} + +message DestroySandboxRequest { +} + +message RemoveStaleVirtiofsShareMountsRequest {} + +message Interfaces { + repeated types.Interface Interfaces = 1; +} + +message Routes { + repeated types.Route Routes = 1; +} + +message UpdateInterfaceRequest { + types.Interface interface = 1; +} + +message UpdateRoutesRequest { + Routes routes = 1; +} + +message UpdateEphemeralMountsRequest { + repeated Storage storages = 1; +} + +message ListInterfacesRequest { +} + +message ListRoutesRequest { +} + +message ARPNeighbors { + repeated types.ARPNeighbor ARPNeighbors = 1; +} + +message AddARPNeighborsRequest { + ARPNeighbors neighbors = 1; +} + +message GetIPTablesRequest { + bool is_ipv6 = 1; +} + +message GetIPTablesResponse{ + // raw stdout from iptables-save or ip6tables-save + bytes data = 1; +} + +message SetIPTablesRequest { + bool is_ipv6 = 1; + + // iptables, in raw format expected to be passed to stdin + // of iptables-save or ip6tables-save + bytes data = 2; +} + +message SetIPTablesResponse{ + // raw stdout from iptables-restore or ip6tables-restore + bytes data = 1; +} + +message OnlineCPUMemRequest { + // Wait specifies if the caller waits for the agent to online all resources. + // If true the agent returns once all resources have been connected, otherwise all + // resources are connected asynchronously and the agent returns immediately. + bool wait = 1; + + // NbCpus specifies the number of CPUs that should be onlined in the guest. + // Special value 0 means agent will skip this check. + uint32 nb_cpus = 2; + + // CpuOnly specifies whether only online CPU or not. + bool cpu_only = 3; +} + +message ReseedRandomDevRequest { + // Data specifies the random data used to reseed the guest crng. + bytes data = 2; +} + +// AgentDetails provides information to the client about the running agent. +message AgentDetails { + // Semantic version of agent (see https://semver.org). + string version = 1; + + // Set if the agent is running as PID 1. + bool init_daemon = 2; + + // List of available device handlers. + repeated string device_handlers = 3; + + // List of available storage handlers. + repeated string storage_handlers = 4; + + // Set only if the agent is built with seccomp support and the guest + // environment supports seccomp. + bool supports_seccomp = 5; + + // List of additional features enabled at agent build time. + repeated string extra_features = 6; +} + +message GuestDetailsRequest { + // MemBlockSize asks server to return the system memory block size that can be used + // for memory hotplug alignment. Typically the server returns what's in + // /sys/devices/system/memory/block_size_bytes. + bool mem_block_size = 1; + + // MemoryHotplugProbe asks server to return whether guest kernel supports memory hotplug + // via probeinterface. Typically the server will check if the path + // /sys/devices/system/memory/probe exists. + bool mem_hotplug_probe = 2; +} + +message GuestDetailsResponse { + // MemBlockSizeBytes returns the system memory block size in bytes. + uint64 mem_block_size_bytes = 1; + + AgentDetails agent_details = 2; + + bool support_mem_hotplug_probe = 3; +} + +message MemHotplugByProbeRequest { + // server needs to send the value of memHotplugProbeAddr into file /sys/devices/system/memory/probe, + // in order to notify the guest kernel about hot-add memory event + repeated uint64 memHotplugProbeAddr = 1; +} + +message SetGuestDateTimeRequest { + // Sec the second since the Epoch. + int64 Sec = 1; + // Usec the microseconds portion of time since the Epoch. + int64 Usec = 2; +} + +// FSGroup consists of the group id and group ownership change policy +// that a volume should have its ownership changed to. +message FSGroup { + // GroupID is the ID that the group ownership of the + // files in the mounted volume will need to be changed to. + uint32 group_id = 2; + // GroupChangePolicy specifies the policy for applying group id + // ownership change on a mounted volume. + types.FSGroupChangePolicy group_change_policy = 3; +} + +// SharedMount declares a set of shared mount points that support +// cross-container sharing of mount objects. +message SharedMount { + // Name is used to identify a pair of shared mount points. + string name = 1; + // Src_ctr is used to specify the name of the source container. + string src_ctr = 2; + // Src_path is used to specify the path of the mount point. If the path doesn't + // exist in the rootfs, it will be created. + string src_path = 3; + // Dst_ctr is used to specify the name of the destination container. + string dst_ctr = 4; + // Dst_path is used to specify the path of the mount point. If the path doesn't + // exist in the rootfs, it will be created. + string dst_path = 5; +} + +// Storage represents both the rootfs of the container, and any volume that +// could have been defined through the Mount list of the OCI specification. +message Storage { + // Driver is used to define the way the storage is passed through the + // virtual machine. It can be "blk", or something else, but for + // all cases, this will define if some extra steps are required before + // this storage gets mounted into the container. + string driver = 1; + // DriverOptions allows the caller to define a list of options such + // as block sizes, numbers of luns, ... which are very specific to + // every device and cannot be generalized through extra fields. + repeated string driver_options = 2; + // Source can be anything representing the source of the storage. This + // will be handled by the proper handler based on the Driver used. + // For instance, it can be a very simple path if the caller knows the + // name of device inside the VM, or it can be some sort of identifier + // to let the agent find the device inside the VM. + string source = 3; + // Fstype represents the filesystem that needs to be used to mount the + // storage inside the VM. For instance, it could be "xfs" for block + // device, or "tmpfs" for shared /dev/shm. + string fstype = 4; + // Options describes the additional options that might be needed to + // mount properly the storage filesystem. + repeated string options = 5; + // MountPoint refers to the path where the storage should be mounted + // inside the VM. + string mount_point = 6; + // FSGroup consists of the group ID and group ownership change policy + // that the mounted volume must have its group ID changed to when specified. + FSGroup fs_group = 7; + // Shared indicates this storage is shared across multiple containers + // (e.g., block-based emptyDirs). When true, the agent should not clean up + // the storage when a container using it exits, as other containers + // may still need it. Cleanup will happen when the sandbox is destroyed. + bool shared = 8; +} + +// Device represents only the devices that could have been defined through the +// Linux Device list of the OCI specification. +message Device { + // Id can be used to identify the device inside the VM. Some devices + // might not need it to be identified on the VM, and will rely on the + // provided VmPath instead. + string id = 1; + // Type defines the type of device described. This can be "blk", + // "scsi", "vfio", ... + // Particularly, this should be used to trigger the use of the + // appropriate device handler. + string type = 2; + // VmPath can be used by the caller to provide directly the path of + // the device as it will appear inside the VM. For some devices, the + // device id or the list of options passed might not be enough to find + // the device. In those cases, the caller should predict and provide + // this vm_path. + string vm_path = 3; + // ContainerPath defines the path where the device should be found inside + // the container. This path should match the path of the device from + // the device list listed inside the OCI spec. This is used in order + // to identify the right device in the spec and update it with the + // right options such as major/minor numbers as they appear inside + // the VM for instance. Note that an empty ctr_path should be used + // to make sure the device handler inside the agent is called, but + // no spec update needs to be performed. This has to happen for the + // case of rootfs, when a device has to be waited for after it has + // been hotplugged. An equivalent Storage entry should be defined if + // any mount needs to be performed afterwards. + string container_path = 4; + // Options allows the caller to define a list of options such as block + // sizes, numbers of luns, ... which are very specific to every device + // and cannot be generalized through extra fields. + repeated string options = 5; +} + +message StringUser { + string uid = 1; + string gid = 2; + repeated string additionalGids = 3; +} + +message CopyFileRequest { + // Path is the destination file in the guest. It must be absolute, + // canonical and below /run. + string path = 1; + // FileSize is the expected file size, for security reasons write operations + // are made in a temporary file, once it has the expected size, it's moved + // to the destination path. + int64 file_size = 2; + // FileMode is the file mode. + uint32 file_mode = 3; + // DirMode is the mode for the parent directories of destination path. + uint32 dir_mode = 4; + // Uid is the numeric user id. + int32 uid = 5; + // Gid is the numeric group id. + int32 gid = 6; + // Offset for the next write operation. + int64 offset = 7; + // Data to write in the destination file. + bytes data = 8; +} + +message GetOOMEventRequest {} + +message OOMEvent { + string container_id = 1; +} + +message AddSwapRequest { + repeated uint32 PCIPath = 1; +} + +message AddSwapPathRequest { + string path = 1; +} + +message GetMetricsRequest {} + +message Metrics { + string metrics = 1; +} + +message VolumeStatsRequest { + // The volume path on the guest outside the container + string volume_guest_path = 1; +} + +message ResizeVolumeRequest { + // Full VM guest path of the volume (outside the container) + string volume_guest_path = 1; + uint64 size = 2; +} + +message SetPolicyRequest { + string policy = 1; +} + +message GetDiagnosticDataRequest { + string log_type = 1; + string container_id = 2; +} + +message GetDiagnosticDataResponse { + string data = 1; +} + +message MemAgentMemcgConfig { + optional bool disabled = 1; + optional bool swap = 2; + optional uint32 swappiness_max = 3; + optional uint64 period_secs = 4; + optional uint32 period_psi_percent_limit = 5; + optional uint32 eviction_psi_percent_limit = 6; + optional uint64 eviction_run_aging_count_min = 7; +} + +message MemAgentCompactConfig { + optional bool disabled = 1; + optional uint64 period_secs = 2; + optional uint32 period_psi_percent_limit = 3; + optional uint32 compact_psi_percent_limit = 4; + optional int64 compact_sec_max = 5; + optional uint32 compact_order = 6; + optional uint64 compact_threshold = 7; + optional uint64 compact_force_times = 8; +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.pb.go b/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.pb.go new file mode 100644 index 000000000..66d1e8722 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.pb.go @@ -0,0 +1,345 @@ +// Copyright (c) 2022 Databricks Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11-devel +// protoc v4.25.3 +// source: csi.proto + +package agentpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type VolumeUsage_Unit int32 + +const ( + VolumeUsage_UNKNOWN VolumeUsage_Unit = 0 + VolumeUsage_BYTES VolumeUsage_Unit = 1 + VolumeUsage_INODES VolumeUsage_Unit = 2 +) + +// Enum value maps for VolumeUsage_Unit. +var ( + VolumeUsage_Unit_name = map[int32]string{ + 0: "UNKNOWN", + 1: "BYTES", + 2: "INODES", + } + VolumeUsage_Unit_value = map[string]int32{ + "UNKNOWN": 0, + "BYTES": 1, + "INODES": 2, + } +) + +func (x VolumeUsage_Unit) Enum() *VolumeUsage_Unit { + p := new(VolumeUsage_Unit) + *p = x + return p +} + +func (x VolumeUsage_Unit) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (VolumeUsage_Unit) Descriptor() protoreflect.EnumDescriptor { + return file_csi_proto_enumTypes[0].Descriptor() +} + +func (VolumeUsage_Unit) Type() protoreflect.EnumType { + return &file_csi_proto_enumTypes[0] +} + +func (x VolumeUsage_Unit) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use VolumeUsage_Unit.Descriptor instead. +func (VolumeUsage_Unit) EnumDescriptor() ([]byte, []int) { + return file_csi_proto_rawDescGZIP(), []int{1, 0} +} + +// This should be kept in sync with CSI NodeGetVolumeStatsResponse (https://github.com/container-storage-interface/spec/blob/v1.5.0/csi.proto) +type VolumeStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This field is OPTIONAL. + Usage []*VolumeUsage `protobuf:"bytes,1,rep,name=usage,proto3" json:"usage,omitempty"` + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the VOLUME_CONDITION node + // capability is supported. + VolumeCondition *VolumeCondition `protobuf:"bytes,2,opt,name=volume_condition,json=volumeCondition,proto3" json:"volume_condition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeStatsResponse) Reset() { + *x = VolumeStatsResponse{} + mi := &file_csi_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeStatsResponse) ProtoMessage() {} + +func (x *VolumeStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_csi_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeStatsResponse.ProtoReflect.Descriptor instead. +func (*VolumeStatsResponse) Descriptor() ([]byte, []int) { + return file_csi_proto_rawDescGZIP(), []int{0} +} + +func (x *VolumeStatsResponse) GetUsage() []*VolumeUsage { + if x != nil { + return x.Usage + } + return nil +} + +func (x *VolumeStatsResponse) GetVolumeCondition() *VolumeCondition { + if x != nil { + return x.VolumeCondition + } + return nil +} + +type VolumeUsage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The available capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + Available uint64 `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` + // The total capacity in specified Unit. This field is REQUIRED. + // The value of this field MUST NOT be negative. + Total uint64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + // The used capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + Used uint64 `protobuf:"varint,3,opt,name=used,proto3" json:"used,omitempty"` + // Units by which values are measured. This field is REQUIRED. + Unit VolumeUsage_Unit `protobuf:"varint,4,opt,name=unit,proto3,enum=grpc.VolumeUsage_Unit" json:"unit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeUsage) Reset() { + *x = VolumeUsage{} + mi := &file_csi_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeUsage) ProtoMessage() {} + +func (x *VolumeUsage) ProtoReflect() protoreflect.Message { + mi := &file_csi_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeUsage.ProtoReflect.Descriptor instead. +func (*VolumeUsage) Descriptor() ([]byte, []int) { + return file_csi_proto_rawDescGZIP(), []int{1} +} + +func (x *VolumeUsage) GetAvailable() uint64 { + if x != nil { + return x.Available + } + return 0 +} + +func (x *VolumeUsage) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *VolumeUsage) GetUsed() uint64 { + if x != nil { + return x.Used + } + return 0 +} + +func (x *VolumeUsage) GetUnit() VolumeUsage_Unit { + if x != nil { + return x.Unit + } + return VolumeUsage_UNKNOWN +} + +// VolumeCondition represents the current condition of a volume. +type VolumeCondition struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Normal volumes are available for use and operating optimally. + // An abnormal volume does not meet these criteria. + // This field is REQUIRED. + Abnormal bool `protobuf:"varint,1,opt,name=abnormal,proto3" json:"abnormal,omitempty"` + // The message describing the condition of the volume. + // This field is REQUIRED. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeCondition) Reset() { + *x = VolumeCondition{} + mi := &file_csi_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeCondition) ProtoMessage() {} + +func (x *VolumeCondition) ProtoReflect() protoreflect.Message { + mi := &file_csi_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeCondition.ProtoReflect.Descriptor instead. +func (*VolumeCondition) Descriptor() ([]byte, []int) { + return file_csi_proto_rawDescGZIP(), []int{2} +} + +func (x *VolumeCondition) GetAbnormal() bool { + if x != nil { + return x.Abnormal + } + return false +} + +func (x *VolumeCondition) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_csi_proto protoreflect.FileDescriptor + +const file_csi_proto_rawDesc = "" + + "\n" + + "\tcsi.proto\x12\x04grpc\"\x80\x01\n" + + "\x13VolumeStatsResponse\x12'\n" + + "\x05usage\x18\x01 \x03(\v2\x11.grpc.VolumeUsageR\x05usage\x12@\n" + + "\x10volume_condition\x18\x02 \x01(\v2\x15.grpc.VolumeConditionR\x0fvolumeCondition\"\xad\x01\n" + + "\vVolumeUsage\x12\x1c\n" + + "\tavailable\x18\x01 \x01(\x04R\tavailable\x12\x14\n" + + "\x05total\x18\x02 \x01(\x04R\x05total\x12\x12\n" + + "\x04used\x18\x03 \x01(\x04R\x04used\x12*\n" + + "\x04unit\x18\x04 \x01(\x0e2\x16.grpc.VolumeUsage.UnitR\x04unit\"*\n" + + "\x04Unit\x12\v\n" + + "\aUNKNOWN\x10\x00\x12\t\n" + + "\x05BYTES\x10\x01\x12\n" + + "\n" + + "\x06INODES\x10\x02\"G\n" + + "\x0fVolumeCondition\x12\x1a\n" + + "\babnormal\x18\x01 \x01(\bR\babnormal\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessageBVZTgithub.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata/agentpb;agentpbb\x06proto3" + +var ( + file_csi_proto_rawDescOnce sync.Once + file_csi_proto_rawDescData []byte +) + +func file_csi_proto_rawDescGZIP() []byte { + file_csi_proto_rawDescOnce.Do(func() { + file_csi_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_csi_proto_rawDesc), len(file_csi_proto_rawDesc))) + }) + return file_csi_proto_rawDescData +} + +var file_csi_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_csi_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_csi_proto_goTypes = []any{ + (VolumeUsage_Unit)(0), // 0: grpc.VolumeUsage.Unit + (*VolumeStatsResponse)(nil), // 1: grpc.VolumeStatsResponse + (*VolumeUsage)(nil), // 2: grpc.VolumeUsage + (*VolumeCondition)(nil), // 3: grpc.VolumeCondition +} +var file_csi_proto_depIdxs = []int32{ + 2, // 0: grpc.VolumeStatsResponse.usage:type_name -> grpc.VolumeUsage + 3, // 1: grpc.VolumeStatsResponse.volume_condition:type_name -> grpc.VolumeCondition + 0, // 2: grpc.VolumeUsage.unit:type_name -> grpc.VolumeUsage.Unit + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_csi_proto_init() } +func file_csi_proto_init() { + if File_csi_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_csi_proto_rawDesc), len(file_csi_proto_rawDesc)), + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_csi_proto_goTypes, + DependencyIndexes: file_csi_proto_depIdxs, + EnumInfos: file_csi_proto_enumTypes, + MessageInfos: file_csi_proto_msgTypes, + }.Build() + File_csi_proto = out.File + file_csi_proto_goTypes = nil + file_csi_proto_depIdxs = nil +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.proto b/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.proto new file mode 100644 index 000000000..6bdf4de81 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/csi.proto @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Databricks Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// + +syntax = "proto3"; +option go_package = "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb;agentpb"; + +package grpc; + +// This should be kept in sync with CSI NodeGetVolumeStatsResponse (https://github.com/container-storage-interface/spec/blob/v1.5.0/csi.proto) +message VolumeStatsResponse { + // This field is OPTIONAL. + repeated VolumeUsage usage = 1; + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the VOLUME_CONDITION node + // capability is supported. + VolumeCondition volume_condition = 2; +} +message VolumeUsage { + enum Unit { + UNKNOWN = 0; + BYTES = 1; + INODES = 2; + } + // The available capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + uint64 available = 1; + + // The total capacity in specified Unit. This field is REQUIRED. + // The value of this field MUST NOT be negative. + uint64 total = 2; + + // The used capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + uint64 used = 3; + + // Units by which values are measured. This field is REQUIRED. + Unit unit = 4; +} + +// VolumeCondition represents the current condition of a volume. +message VolumeCondition { + + // Normal volumes are available for use and operating optimally. + // An abnormal volume does not meet these criteria. + // This field is REQUIRED. + bool abnormal = 1; + + // The message describing the condition of the volume. + // This field is REQUIRED. + string message = 2; +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.pb.go b/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.pb.go new file mode 100644 index 000000000..b5f82d0d2 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.pb.go @@ -0,0 +1,2745 @@ +// +// Copyright (c) 2017 Intel Corporation +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11-devel +// protoc v4.25.3 +// source: oci.proto + +package agentpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Spec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Version of the Open Container Initiative Runtime Specification with which the bundle complies. + Version string `protobuf:"bytes,1,opt,name=Version,proto3" json:"Version,omitempty"` + // Process configures the container process. + Process *Process `protobuf:"bytes,2,opt,name=Process,proto3" json:"Process,omitempty"` + // Root configures the container's root filesystem. + Root *Root `protobuf:"bytes,3,opt,name=Root,proto3" json:"Root,omitempty"` + // Hostname configures the container's hostname. + Hostname string `protobuf:"bytes,4,opt,name=Hostname,proto3" json:"Hostname,omitempty"` + // Mounts configures additional mounts (on top of Root). + Mounts []*Mount `protobuf:"bytes,5,rep,name=Mounts,proto3" json:"Mounts,omitempty"` + // Hooks configures callbacks for container lifecycle events. + Hooks *Hooks `protobuf:"bytes,6,opt,name=Hooks,proto3" json:"Hooks,omitempty"` + // Annotations contains arbitrary metadata for the container. + Annotations map[string]string `protobuf:"bytes,7,rep,name=Annotations,proto3" json:"Annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Linux is platform-specific configuration for Linux based containers. + Linux *Linux `protobuf:"bytes,8,opt,name=Linux,proto3" json:"Linux,omitempty"` + // Solaris is platform-specific configuration for Solaris based containers. + Solaris *Solaris `protobuf:"bytes,9,opt,name=Solaris,proto3" json:"Solaris,omitempty"` + // Windows is platform-specific configuration for Windows based containers. + Windows *Windows `protobuf:"bytes,10,opt,name=Windows,proto3" json:"Windows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Spec) Reset() { + *x = Spec{} + mi := &file_oci_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Spec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Spec) ProtoMessage() {} + +func (x *Spec) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Spec.ProtoReflect.Descriptor instead. +func (*Spec) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{0} +} + +func (x *Spec) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Spec) GetProcess() *Process { + if x != nil { + return x.Process + } + return nil +} + +func (x *Spec) GetRoot() *Root { + if x != nil { + return x.Root + } + return nil +} + +func (x *Spec) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *Spec) GetMounts() []*Mount { + if x != nil { + return x.Mounts + } + return nil +} + +func (x *Spec) GetHooks() *Hooks { + if x != nil { + return x.Hooks + } + return nil +} + +func (x *Spec) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *Spec) GetLinux() *Linux { + if x != nil { + return x.Linux + } + return nil +} + +func (x *Spec) GetSolaris() *Solaris { + if x != nil { + return x.Solaris + } + return nil +} + +func (x *Spec) GetWindows() *Windows { + if x != nil { + return x.Windows + } + return nil +} + +type Process struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Terminal creates an interactive terminal for the container. + Terminal bool `protobuf:"varint,1,opt,name=Terminal,proto3" json:"Terminal,omitempty"` + // ConsoleSize specifies the size of the console. + ConsoleSize *Box `protobuf:"bytes,2,opt,name=ConsoleSize,proto3" json:"ConsoleSize,omitempty"` + // User specifies user information for the process. + User *User `protobuf:"bytes,3,opt,name=User,proto3" json:"User,omitempty"` + // Args specifies the binary and arguments for the application to execute. + Args []string `protobuf:"bytes,4,rep,name=Args,proto3" json:"Args,omitempty"` + // Env populates the process environment for the process. + Env []string `protobuf:"bytes,5,rep,name=Env,proto3" json:"Env,omitempty"` + // Cwd is the current working directory for the process and must be + // relative to the container's root. + Cwd string `protobuf:"bytes,6,opt,name=Cwd,proto3" json:"Cwd,omitempty"` + // Capabilities are Linux capabilities that are kept for the process. + Capabilities *LinuxCapabilities `protobuf:"bytes,7,opt,name=Capabilities,proto3" json:"Capabilities,omitempty"` + // Rlimits specifies rlimit options to apply to the process. + Rlimits []*POSIXRlimit `protobuf:"bytes,8,rep,name=Rlimits,proto3" json:"Rlimits,omitempty"` + // NoNewPrivileges controls whether additional privileges could be gained by processes in the container. + NoNewPrivileges bool `protobuf:"varint,9,opt,name=NoNewPrivileges,proto3" json:"NoNewPrivileges,omitempty"` + // ApparmorProfile specifies the apparmor profile for the container. + ApparmorProfile string `protobuf:"bytes,10,opt,name=ApparmorProfile,proto3" json:"ApparmorProfile,omitempty"` + // Specify an oom_score_adj for the container. + OOMScoreAdj int64 `protobuf:"varint,11,opt,name=OOMScoreAdj,proto3" json:"OOMScoreAdj,omitempty"` + // SelinuxLabel specifies the selinux context that the container process is run as. + SelinuxLabel string `protobuf:"bytes,12,opt,name=SelinuxLabel,proto3" json:"SelinuxLabel,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Process) Reset() { + *x = Process{} + mi := &file_oci_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Process) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Process) ProtoMessage() {} + +func (x *Process) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Process.ProtoReflect.Descriptor instead. +func (*Process) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{1} +} + +func (x *Process) GetTerminal() bool { + if x != nil { + return x.Terminal + } + return false +} + +func (x *Process) GetConsoleSize() *Box { + if x != nil { + return x.ConsoleSize + } + return nil +} + +func (x *Process) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *Process) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *Process) GetEnv() []string { + if x != nil { + return x.Env + } + return nil +} + +func (x *Process) GetCwd() string { + if x != nil { + return x.Cwd + } + return "" +} + +func (x *Process) GetCapabilities() *LinuxCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *Process) GetRlimits() []*POSIXRlimit { + if x != nil { + return x.Rlimits + } + return nil +} + +func (x *Process) GetNoNewPrivileges() bool { + if x != nil { + return x.NoNewPrivileges + } + return false +} + +func (x *Process) GetApparmorProfile() string { + if x != nil { + return x.ApparmorProfile + } + return "" +} + +func (x *Process) GetOOMScoreAdj() int64 { + if x != nil { + return x.OOMScoreAdj + } + return 0 +} + +func (x *Process) GetSelinuxLabel() string { + if x != nil { + return x.SelinuxLabel + } + return "" +} + +type Box struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Height is the vertical dimension of a box. + Height uint32 `protobuf:"varint,1,opt,name=Height,proto3" json:"Height,omitempty"` + // Width is the horizontal dimension of a box. + Width uint32 `protobuf:"varint,2,opt,name=Width,proto3" json:"Width,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Box) Reset() { + *x = Box{} + mi := &file_oci_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Box) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Box) ProtoMessage() {} + +func (x *Box) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Box.ProtoReflect.Descriptor instead. +func (*Box) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{2} +} + +func (x *Box) GetHeight() uint32 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *Box) GetWidth() uint32 { + if x != nil { + return x.Width + } + return 0 +} + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UID is the user id. + UID uint32 `protobuf:"varint,1,opt,name=UID,proto3" json:"UID,omitempty"` + // GID is the group id. + GID uint32 `protobuf:"varint,2,opt,name=GID,proto3" json:"GID,omitempty"` + // AdditionalGids are additional group ids set for the container's process. + AdditionalGids []uint32 `protobuf:"varint,3,rep,packed,name=AdditionalGids,proto3" json:"AdditionalGids,omitempty"` + // Username is the user name. + Username string `protobuf:"bytes,4,opt,name=Username,proto3" json:"Username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_oci_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{3} +} + +func (x *User) GetUID() uint32 { + if x != nil { + return x.UID + } + return 0 +} + +func (x *User) GetGID() uint32 { + if x != nil { + return x.GID + } + return 0 +} + +func (x *User) GetAdditionalGids() []uint32 { + if x != nil { + return x.AdditionalGids + } + return nil +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type LinuxCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Bounding is the set of capabilities checked by the kernel. + Bounding []string `protobuf:"bytes,1,rep,name=Bounding,proto3" json:"Bounding,omitempty"` + // Effective is the set of capabilities checked by the kernel. + Effective []string `protobuf:"bytes,2,rep,name=Effective,proto3" json:"Effective,omitempty"` + // Inheritable is the capabilities preserved across execve. + Inheritable []string `protobuf:"bytes,3,rep,name=Inheritable,proto3" json:"Inheritable,omitempty"` + // Permitted is the limiting superset for effective capabilities. + Permitted []string `protobuf:"bytes,4,rep,name=Permitted,proto3" json:"Permitted,omitempty"` + // Ambient is the ambient set of capabilities that are kept. + Ambient []string `protobuf:"bytes,5,rep,name=Ambient,proto3" json:"Ambient,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxCapabilities) Reset() { + *x = LinuxCapabilities{} + mi := &file_oci_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxCapabilities) ProtoMessage() {} + +func (x *LinuxCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxCapabilities.ProtoReflect.Descriptor instead. +func (*LinuxCapabilities) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{4} +} + +func (x *LinuxCapabilities) GetBounding() []string { + if x != nil { + return x.Bounding + } + return nil +} + +func (x *LinuxCapabilities) GetEffective() []string { + if x != nil { + return x.Effective + } + return nil +} + +func (x *LinuxCapabilities) GetInheritable() []string { + if x != nil { + return x.Inheritable + } + return nil +} + +func (x *LinuxCapabilities) GetPermitted() []string { + if x != nil { + return x.Permitted + } + return nil +} + +func (x *LinuxCapabilities) GetAmbient() []string { + if x != nil { + return x.Ambient + } + return nil +} + +type POSIXRlimit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Type of the rlimit to set + Type string `protobuf:"bytes,1,opt,name=Type,proto3" json:"Type,omitempty"` + // Hard is the hard limit for the specified type + Hard uint64 `protobuf:"varint,2,opt,name=Hard,proto3" json:"Hard,omitempty"` + // Soft is the soft limit for the specified type + Soft uint64 `protobuf:"varint,3,opt,name=Soft,proto3" json:"Soft,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *POSIXRlimit) Reset() { + *x = POSIXRlimit{} + mi := &file_oci_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *POSIXRlimit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*POSIXRlimit) ProtoMessage() {} + +func (x *POSIXRlimit) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use POSIXRlimit.ProtoReflect.Descriptor instead. +func (*POSIXRlimit) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{5} +} + +func (x *POSIXRlimit) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *POSIXRlimit) GetHard() uint64 { + if x != nil { + return x.Hard + } + return 0 +} + +func (x *POSIXRlimit) GetSoft() uint64 { + if x != nil { + return x.Soft + } + return 0 +} + +type Mount struct { + state protoimpl.MessageState `protogen:"open.v1"` + // destination is the path inside the container expect when it starts with "tmp:/" + Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` + // source is the path inside the container expect when it starts with "vm:/dev/" or "tmp:/" + // the path which starts with "vm:/dev/" refers the guest vm's "/dev", + // especially, "vm:/dev/hostfs/" refers to the shared filesystem. + // "tmp:/" is a temporary directory which is used for temporary mounts. + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Options []string `protobuf:"bytes,4,rep,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Mount) Reset() { + *x = Mount{} + mi := &file_oci_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Mount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Mount) ProtoMessage() {} + +func (x *Mount) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Mount.ProtoReflect.Descriptor instead. +func (*Mount) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{6} +} + +func (x *Mount) GetDestination() string { + if x != nil { + return x.Destination + } + return "" +} + +func (x *Mount) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Mount) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Mount) GetOptions() []string { + if x != nil { + return x.Options + } + return nil +} + +type Root struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Path is the absolute path to the container's root filesystem. + Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"` + // Readonly makes the root filesystem for the container readonly before the process is executed. + Readonly bool `protobuf:"varint,2,opt,name=Readonly,proto3" json:"Readonly,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Root) Reset() { + *x = Root{} + mi := &file_oci_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Root) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Root) ProtoMessage() {} + +func (x *Root) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Root.ProtoReflect.Descriptor instead. +func (*Root) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{7} +} + +func (x *Root) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Root) GetReadonly() bool { + if x != nil { + return x.Readonly + } + return false +} + +type Hooks struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Prestart is a list of hooks to be run before the container process is executed. + Prestart []*Hook `protobuf:"bytes,1,rep,name=Prestart,proto3" json:"Prestart,omitempty"` + // Poststart is a list of hooks to be run after the container process is started. + Poststart []*Hook `protobuf:"bytes,2,rep,name=Poststart,proto3" json:"Poststart,omitempty"` + // Poststop is a list of hooks to be run after the container process exits. + Poststop []*Hook `protobuf:"bytes,3,rep,name=Poststop,proto3" json:"Poststop,omitempty"` + // Createruntime is a list of hooks to be run during the creation of runtime(sandbox). + CreateRuntime []*Hook `protobuf:"bytes,4,rep,name=CreateRuntime,proto3" json:"CreateRuntime,omitempty"` + // CreateContainer is a list of hooks to be run after VM is started, and before container is created. + CreateContainer []*Hook `protobuf:"bytes,5,rep,name=CreateContainer,proto3" json:"CreateContainer,omitempty"` + // StartContainer is a list of hooks to be run after container is created, but before it is started. + StartContainer []*Hook `protobuf:"bytes,6,rep,name=StartContainer,proto3" json:"StartContainer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hooks) Reset() { + *x = Hooks{} + mi := &file_oci_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hooks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hooks) ProtoMessage() {} + +func (x *Hooks) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hooks.ProtoReflect.Descriptor instead. +func (*Hooks) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{8} +} + +func (x *Hooks) GetPrestart() []*Hook { + if x != nil { + return x.Prestart + } + return nil +} + +func (x *Hooks) GetPoststart() []*Hook { + if x != nil { + return x.Poststart + } + return nil +} + +func (x *Hooks) GetPoststop() []*Hook { + if x != nil { + return x.Poststop + } + return nil +} + +func (x *Hooks) GetCreateRuntime() []*Hook { + if x != nil { + return x.CreateRuntime + } + return nil +} + +func (x *Hooks) GetCreateContainer() []*Hook { + if x != nil { + return x.CreateContainer + } + return nil +} + +func (x *Hooks) GetStartContainer() []*Hook { + if x != nil { + return x.StartContainer + } + return nil +} + +type Hook struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Path is the absolute path to the container's root filesystem. + Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"` + // Arguments used for the binary, including the binary name itself. + Args []string `protobuf:"bytes,2,rep,name=Args,proto3" json:"Args,omitempty"` + // Additional `key=value` environment variables. + Env []string `protobuf:"bytes,3,rep,name=Env,proto3" json:"Env,omitempty"` + // Timeout is the number of seconds before aborting the hook. If set, timeout MUST be greater than zero. + Timeout int64 `protobuf:"varint,4,opt,name=Timeout,proto3" json:"Timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hook) Reset() { + *x = Hook{} + mi := &file_oci_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook) ProtoMessage() {} + +func (x *Hook) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook.ProtoReflect.Descriptor instead. +func (*Hook) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{9} +} + +func (x *Hook) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Hook) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +func (x *Hook) GetEnv() []string { + if x != nil { + return x.Env + } + return nil +} + +func (x *Hook) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +type Linux struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UIDMapping specifies user mappings for supporting user namespaces. + UIDMappings []*LinuxIDMapping `protobuf:"bytes,1,rep,name=UIDMappings,proto3" json:"UIDMappings,omitempty"` + // GIDMapping specifies group mappings for supporting user namespaces. + GIDMappings []*LinuxIDMapping `protobuf:"bytes,2,rep,name=GIDMappings,proto3" json:"GIDMappings,omitempty"` + // Sysctl are a set of key value pairs that are set for the container on start + Sysctl map[string]string `protobuf:"bytes,3,rep,name=Sysctl,proto3" json:"Sysctl,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Resources contain cgroup information for handling resource constraints + // for the container + Resources *LinuxResources `protobuf:"bytes,4,opt,name=Resources,proto3" json:"Resources,omitempty"` + // CgroupsPath specifies the path to cgroups that are created and/or joined by the container. + // The path is expected to be relative to the cgroups mountpoint. + // If resources are specified, the cgroups at CgroupsPath will be updated based on resources. + CgroupsPath string `protobuf:"bytes,5,opt,name=CgroupsPath,proto3" json:"CgroupsPath,omitempty"` + // Namespaces contains the namespaces that are created and/or joined by the container + Namespaces []*LinuxNamespace `protobuf:"bytes,6,rep,name=Namespaces,proto3" json:"Namespaces,omitempty"` + // Devices are a list of device nodes that are created for the container + Devices []*LinuxDevice `protobuf:"bytes,7,rep,name=Devices,proto3" json:"Devices,omitempty"` + // Seccomp specifies the seccomp security settings for the container. + Seccomp *LinuxSeccomp `protobuf:"bytes,8,opt,name=Seccomp,proto3" json:"Seccomp,omitempty"` + // RootfsPropagation is the rootfs mount propagation mode for the container. + RootfsPropagation string `protobuf:"bytes,9,opt,name=RootfsPropagation,proto3" json:"RootfsPropagation,omitempty"` + // MaskedPaths masks over the provided paths inside the container. + MaskedPaths []string `protobuf:"bytes,10,rep,name=MaskedPaths,proto3" json:"MaskedPaths,omitempty"` + // ReadonlyPaths sets the provided paths as RO inside the container. + ReadonlyPaths []string `protobuf:"bytes,11,rep,name=ReadonlyPaths,proto3" json:"ReadonlyPaths,omitempty"` + // MountLabel specifies the selinux context for the mounts in the container. + MountLabel string `protobuf:"bytes,12,opt,name=MountLabel,proto3" json:"MountLabel,omitempty"` + // IntelRdt contains Intel Resource Director Technology (RDT) information + // for handling resource constraints (e.g., L3 cache) for the container + IntelRdt *LinuxIntelRdt `protobuf:"bytes,13,opt,name=IntelRdt,proto3" json:"IntelRdt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Linux) Reset() { + *x = Linux{} + mi := &file_oci_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Linux) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Linux) ProtoMessage() {} + +func (x *Linux) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Linux.ProtoReflect.Descriptor instead. +func (*Linux) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{10} +} + +func (x *Linux) GetUIDMappings() []*LinuxIDMapping { + if x != nil { + return x.UIDMappings + } + return nil +} + +func (x *Linux) GetGIDMappings() []*LinuxIDMapping { + if x != nil { + return x.GIDMappings + } + return nil +} + +func (x *Linux) GetSysctl() map[string]string { + if x != nil { + return x.Sysctl + } + return nil +} + +func (x *Linux) GetResources() *LinuxResources { + if x != nil { + return x.Resources + } + return nil +} + +func (x *Linux) GetCgroupsPath() string { + if x != nil { + return x.CgroupsPath + } + return "" +} + +func (x *Linux) GetNamespaces() []*LinuxNamespace { + if x != nil { + return x.Namespaces + } + return nil +} + +func (x *Linux) GetDevices() []*LinuxDevice { + if x != nil { + return x.Devices + } + return nil +} + +func (x *Linux) GetSeccomp() *LinuxSeccomp { + if x != nil { + return x.Seccomp + } + return nil +} + +func (x *Linux) GetRootfsPropagation() string { + if x != nil { + return x.RootfsPropagation + } + return "" +} + +func (x *Linux) GetMaskedPaths() []string { + if x != nil { + return x.MaskedPaths + } + return nil +} + +func (x *Linux) GetReadonlyPaths() []string { + if x != nil { + return x.ReadonlyPaths + } + return nil +} + +func (x *Linux) GetMountLabel() string { + if x != nil { + return x.MountLabel + } + return "" +} + +func (x *Linux) GetIntelRdt() *LinuxIntelRdt { + if x != nil { + return x.IntelRdt + } + return nil +} + +type Windows struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Dummy string, never used. + Dummy string `protobuf:"bytes,1,opt,name=dummy,proto3" json:"dummy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Windows) Reset() { + *x = Windows{} + mi := &file_oci_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Windows) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Windows) ProtoMessage() {} + +func (x *Windows) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Windows.ProtoReflect.Descriptor instead. +func (*Windows) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{11} +} + +func (x *Windows) GetDummy() string { + if x != nil { + return x.Dummy + } + return "" +} + +type Solaris struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Dummy string, never used. + Dummy string `protobuf:"bytes,1,opt,name=dummy,proto3" json:"dummy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Solaris) Reset() { + *x = Solaris{} + mi := &file_oci_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Solaris) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Solaris) ProtoMessage() {} + +func (x *Solaris) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Solaris.ProtoReflect.Descriptor instead. +func (*Solaris) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{12} +} + +func (x *Solaris) GetDummy() string { + if x != nil { + return x.Dummy + } + return "" +} + +type LinuxIDMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + // HostID is the starting UID/GID on the host to be mapped to 'ContainerID' + HostID uint32 `protobuf:"varint,1,opt,name=HostID,proto3" json:"HostID,omitempty"` + // ContainerID is the starting UID/GID in the container + ContainerID uint32 `protobuf:"varint,2,opt,name=ContainerID,proto3" json:"ContainerID,omitempty"` + // Size is the number of IDs to be mapped + Size uint32 `protobuf:"varint,3,opt,name=Size,proto3" json:"Size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxIDMapping) Reset() { + *x = LinuxIDMapping{} + mi := &file_oci_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxIDMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxIDMapping) ProtoMessage() {} + +func (x *LinuxIDMapping) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxIDMapping.ProtoReflect.Descriptor instead. +func (*LinuxIDMapping) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{13} +} + +func (x *LinuxIDMapping) GetHostID() uint32 { + if x != nil { + return x.HostID + } + return 0 +} + +func (x *LinuxIDMapping) GetContainerID() uint32 { + if x != nil { + return x.ContainerID + } + return 0 +} + +func (x *LinuxIDMapping) GetSize() uint32 { + if x != nil { + return x.Size + } + return 0 +} + +type LinuxNamespace struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Type is the type of namespace + Type string `protobuf:"bytes,1,opt,name=Type,proto3" json:"Type,omitempty"` + // Path is a path to an existing namespace persisted on disk that can be joined + // and is of the same type + Path string `protobuf:"bytes,2,opt,name=Path,proto3" json:"Path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxNamespace) Reset() { + *x = LinuxNamespace{} + mi := &file_oci_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxNamespace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxNamespace) ProtoMessage() {} + +func (x *LinuxNamespace) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxNamespace.ProtoReflect.Descriptor instead. +func (*LinuxNamespace) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{14} +} + +func (x *LinuxNamespace) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *LinuxNamespace) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type LinuxDevice struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Path to the device. + Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"` + // Device type, block, char, etc. + Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` + // Major is the device's major number. + Major int64 `protobuf:"varint,3,opt,name=Major,proto3" json:"Major,omitempty"` + // Minor is the device's minor number. + Minor int64 `protobuf:"varint,4,opt,name=Minor,proto3" json:"Minor,omitempty"` + // FileMode permission bits for the device. + FileMode uint32 `protobuf:"varint,5,opt,name=FileMode,proto3" json:"FileMode,omitempty"` + // UID of the device. + UID uint32 `protobuf:"varint,6,opt,name=UID,proto3" json:"UID,omitempty"` + // Gid of the device. + GID uint32 `protobuf:"varint,7,opt,name=GID,proto3" json:"GID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxDevice) Reset() { + *x = LinuxDevice{} + mi := &file_oci_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxDevice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxDevice) ProtoMessage() {} + +func (x *LinuxDevice) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxDevice.ProtoReflect.Descriptor instead. +func (*LinuxDevice) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{15} +} + +func (x *LinuxDevice) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *LinuxDevice) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *LinuxDevice) GetMajor() int64 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *LinuxDevice) GetMinor() int64 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *LinuxDevice) GetFileMode() uint32 { + if x != nil { + return x.FileMode + } + return 0 +} + +func (x *LinuxDevice) GetUID() uint32 { + if x != nil { + return x.UID + } + return 0 +} + +func (x *LinuxDevice) GetGID() uint32 { + if x != nil { + return x.GID + } + return 0 +} + +type LinuxResources struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Devices configures the device whitelist. + Devices []*LinuxDeviceCgroup `protobuf:"bytes,1,rep,name=Devices,proto3" json:"Devices,omitempty"` + // Memory restriction configuration + Memory *LinuxMemory `protobuf:"bytes,2,opt,name=Memory,proto3" json:"Memory,omitempty"` + // CPU resource restriction configuration + CPU *LinuxCPU `protobuf:"bytes,3,opt,name=CPU,proto3" json:"CPU,omitempty"` + // Task resource restriction configuration. + Pids *LinuxPids `protobuf:"bytes,4,opt,name=Pids,proto3" json:"Pids,omitempty"` + // BlockIO restriction configuration + BlockIO *LinuxBlockIO `protobuf:"bytes,5,opt,name=BlockIO,proto3" json:"BlockIO,omitempty"` + // Hugetlb limit (in bytes) + HugepageLimits []*LinuxHugepageLimit `protobuf:"bytes,6,rep,name=HugepageLimits,proto3" json:"HugepageLimits,omitempty"` + // Network restriction configuration + Network *LinuxNetwork `protobuf:"bytes,7,opt,name=Network,proto3" json:"Network,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxResources) Reset() { + *x = LinuxResources{} + mi := &file_oci_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxResources) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxResources) ProtoMessage() {} + +func (x *LinuxResources) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxResources.ProtoReflect.Descriptor instead. +func (*LinuxResources) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{16} +} + +func (x *LinuxResources) GetDevices() []*LinuxDeviceCgroup { + if x != nil { + return x.Devices + } + return nil +} + +func (x *LinuxResources) GetMemory() *LinuxMemory { + if x != nil { + return x.Memory + } + return nil +} + +func (x *LinuxResources) GetCPU() *LinuxCPU { + if x != nil { + return x.CPU + } + return nil +} + +func (x *LinuxResources) GetPids() *LinuxPids { + if x != nil { + return x.Pids + } + return nil +} + +func (x *LinuxResources) GetBlockIO() *LinuxBlockIO { + if x != nil { + return x.BlockIO + } + return nil +} + +func (x *LinuxResources) GetHugepageLimits() []*LinuxHugepageLimit { + if x != nil { + return x.HugepageLimits + } + return nil +} + +func (x *LinuxResources) GetNetwork() *LinuxNetwork { + if x != nil { + return x.Network + } + return nil +} + +type LinuxMemory struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Memory limit (in bytes). + Limit int64 `protobuf:"varint,1,opt,name=Limit,proto3" json:"Limit,omitempty"` + // Memory reservation or soft_limit (in bytes). + Reservation int64 `protobuf:"varint,2,opt,name=Reservation,proto3" json:"Reservation,omitempty"` + // Total memory limit (memory + swap). + Swap int64 `protobuf:"varint,3,opt,name=Swap,proto3" json:"Swap,omitempty"` + // Kernel memory limit (in bytes). + Kernel int64 `protobuf:"varint,4,opt,name=Kernel,proto3" json:"Kernel,omitempty"` + // Kernel memory limit for tcp (in bytes) + KernelTCP int64 `protobuf:"varint,5,opt,name=KernelTCP,proto3" json:"KernelTCP,omitempty"` + // How aggressive the kernel will swap memory pages. + Swappiness uint64 `protobuf:"varint,6,opt,name=Swappiness,proto3" json:"Swappiness,omitempty"` + // DisableOOMKiller disables the OOM killer for out of memory conditions + DisableOOMKiller bool `protobuf:"varint,7,opt,name=DisableOOMKiller,proto3" json:"DisableOOMKiller,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxMemory) Reset() { + *x = LinuxMemory{} + mi := &file_oci_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxMemory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxMemory) ProtoMessage() {} + +func (x *LinuxMemory) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxMemory.ProtoReflect.Descriptor instead. +func (*LinuxMemory) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{17} +} + +func (x *LinuxMemory) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *LinuxMemory) GetReservation() int64 { + if x != nil { + return x.Reservation + } + return 0 +} + +func (x *LinuxMemory) GetSwap() int64 { + if x != nil { + return x.Swap + } + return 0 +} + +func (x *LinuxMemory) GetKernel() int64 { + if x != nil { + return x.Kernel + } + return 0 +} + +func (x *LinuxMemory) GetKernelTCP() int64 { + if x != nil { + return x.KernelTCP + } + return 0 +} + +func (x *LinuxMemory) GetSwappiness() uint64 { + if x != nil { + return x.Swappiness + } + return 0 +} + +func (x *LinuxMemory) GetDisableOOMKiller() bool { + if x != nil { + return x.DisableOOMKiller + } + return false +} + +type LinuxCPU struct { + state protoimpl.MessageState `protogen:"open.v1"` + // CPU shares (relative weight (ratio) vs. other cgroups with cpu shares). + Shares uint64 `protobuf:"varint,1,opt,name=Shares,proto3" json:"Shares,omitempty"` + // CPU hardcap limit (in usecs). Allowed cpu time in a given period. + Quota int64 `protobuf:"varint,2,opt,name=Quota,proto3" json:"Quota,omitempty"` + // CPU period to be used for hardcapping (in usecs). + Period uint64 `protobuf:"varint,3,opt,name=Period,proto3" json:"Period,omitempty"` + // How much time realtime scheduling may use (in usecs). + RealtimeRuntime int64 `protobuf:"varint,4,opt,name=RealtimeRuntime,proto3" json:"RealtimeRuntime,omitempty"` + // CPU period to be used for realtime scheduling (in usecs). + RealtimePeriod uint64 `protobuf:"varint,5,opt,name=RealtimePeriod,proto3" json:"RealtimePeriod,omitempty"` + // CPUs to use within the cpuset. Default is to use any CPU available. + Cpus string `protobuf:"bytes,6,opt,name=Cpus,proto3" json:"Cpus,omitempty"` + // List of memory nodes in the cpuset. Default is to use any available memory node. + Mems string `protobuf:"bytes,7,opt,name=Mems,proto3" json:"Mems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxCPU) Reset() { + *x = LinuxCPU{} + mi := &file_oci_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxCPU) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxCPU) ProtoMessage() {} + +func (x *LinuxCPU) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxCPU.ProtoReflect.Descriptor instead. +func (*LinuxCPU) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{18} +} + +func (x *LinuxCPU) GetShares() uint64 { + if x != nil { + return x.Shares + } + return 0 +} + +func (x *LinuxCPU) GetQuota() int64 { + if x != nil { + return x.Quota + } + return 0 +} + +func (x *LinuxCPU) GetPeriod() uint64 { + if x != nil { + return x.Period + } + return 0 +} + +func (x *LinuxCPU) GetRealtimeRuntime() int64 { + if x != nil { + return x.RealtimeRuntime + } + return 0 +} + +func (x *LinuxCPU) GetRealtimePeriod() uint64 { + if x != nil { + return x.RealtimePeriod + } + return 0 +} + +func (x *LinuxCPU) GetCpus() string { + if x != nil { + return x.Cpus + } + return "" +} + +func (x *LinuxCPU) GetMems() string { + if x != nil { + return x.Mems + } + return "" +} + +type LinuxWeightDevice struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Major is the device's major number. + Major int64 `protobuf:"varint,1,opt,name=Major,proto3" json:"Major,omitempty"` + // Minor is the device's minor number. + Minor int64 `protobuf:"varint,2,opt,name=Minor,proto3" json:"Minor,omitempty"` + // Weight is the bandwidth rate for the device. + Weight uint32 `protobuf:"varint,3,opt,name=Weight,proto3" json:"Weight,omitempty"` + // LeafWeight is the bandwidth rate for the device while competing with the cgroup's child cgroups, CFQ scheduler only + LeafWeight uint32 `protobuf:"varint,4,opt,name=LeafWeight,proto3" json:"LeafWeight,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxWeightDevice) Reset() { + *x = LinuxWeightDevice{} + mi := &file_oci_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxWeightDevice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxWeightDevice) ProtoMessage() {} + +func (x *LinuxWeightDevice) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxWeightDevice.ProtoReflect.Descriptor instead. +func (*LinuxWeightDevice) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{19} +} + +func (x *LinuxWeightDevice) GetMajor() int64 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *LinuxWeightDevice) GetMinor() int64 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *LinuxWeightDevice) GetWeight() uint32 { + if x != nil { + return x.Weight + } + return 0 +} + +func (x *LinuxWeightDevice) GetLeafWeight() uint32 { + if x != nil { + return x.LeafWeight + } + return 0 +} + +type LinuxThrottleDevice struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Major is the device's major number. + Major int64 `protobuf:"varint,1,opt,name=Major,proto3" json:"Major,omitempty"` + // Minor is the device's minor number. + Minor int64 `protobuf:"varint,2,opt,name=Minor,proto3" json:"Minor,omitempty"` + // Rate is the IO rate limit per cgroup per device + Rate uint64 `protobuf:"varint,3,opt,name=Rate,proto3" json:"Rate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxThrottleDevice) Reset() { + *x = LinuxThrottleDevice{} + mi := &file_oci_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxThrottleDevice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxThrottleDevice) ProtoMessage() {} + +func (x *LinuxThrottleDevice) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxThrottleDevice.ProtoReflect.Descriptor instead. +func (*LinuxThrottleDevice) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{20} +} + +func (x *LinuxThrottleDevice) GetMajor() int64 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *LinuxThrottleDevice) GetMinor() int64 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *LinuxThrottleDevice) GetRate() uint64 { + if x != nil { + return x.Rate + } + return 0 +} + +type LinuxBlockIO struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Specifies per cgroup weight + Weight uint32 `protobuf:"varint,1,opt,name=Weight,proto3" json:"Weight,omitempty"` + // Specifies tasks' weight in the given cgroup while competing with the cgroup's child cgroups, CFQ scheduler only + LeafWeight uint32 `protobuf:"varint,2,opt,name=LeafWeight,proto3" json:"LeafWeight,omitempty"` + // Weight per cgroup per device, can override BlkioWeight + WeightDevice []*LinuxWeightDevice `protobuf:"bytes,3,rep,name=WeightDevice,proto3" json:"WeightDevice,omitempty"` + // IO read rate limit per cgroup per device, bytes per second + ThrottleReadBpsDevice []*LinuxThrottleDevice `protobuf:"bytes,4,rep,name=ThrottleReadBpsDevice,proto3" json:"ThrottleReadBpsDevice,omitempty"` + // IO write rate limit per cgroup per device, bytes per second + ThrottleWriteBpsDevice []*LinuxThrottleDevice `protobuf:"bytes,5,rep,name=ThrottleWriteBpsDevice,proto3" json:"ThrottleWriteBpsDevice,omitempty"` + // IO read rate limit per cgroup per device, IO per second + ThrottleReadIOPSDevice []*LinuxThrottleDevice `protobuf:"bytes,6,rep,name=ThrottleReadIOPSDevice,proto3" json:"ThrottleReadIOPSDevice,omitempty"` + // IO write rate limit per cgroup per device, IO per second + ThrottleWriteIOPSDevice []*LinuxThrottleDevice `protobuf:"bytes,7,rep,name=ThrottleWriteIOPSDevice,proto3" json:"ThrottleWriteIOPSDevice,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxBlockIO) Reset() { + *x = LinuxBlockIO{} + mi := &file_oci_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxBlockIO) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxBlockIO) ProtoMessage() {} + +func (x *LinuxBlockIO) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxBlockIO.ProtoReflect.Descriptor instead. +func (*LinuxBlockIO) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{21} +} + +func (x *LinuxBlockIO) GetWeight() uint32 { + if x != nil { + return x.Weight + } + return 0 +} + +func (x *LinuxBlockIO) GetLeafWeight() uint32 { + if x != nil { + return x.LeafWeight + } + return 0 +} + +func (x *LinuxBlockIO) GetWeightDevice() []*LinuxWeightDevice { + if x != nil { + return x.WeightDevice + } + return nil +} + +func (x *LinuxBlockIO) GetThrottleReadBpsDevice() []*LinuxThrottleDevice { + if x != nil { + return x.ThrottleReadBpsDevice + } + return nil +} + +func (x *LinuxBlockIO) GetThrottleWriteBpsDevice() []*LinuxThrottleDevice { + if x != nil { + return x.ThrottleWriteBpsDevice + } + return nil +} + +func (x *LinuxBlockIO) GetThrottleReadIOPSDevice() []*LinuxThrottleDevice { + if x != nil { + return x.ThrottleReadIOPSDevice + } + return nil +} + +func (x *LinuxBlockIO) GetThrottleWriteIOPSDevice() []*LinuxThrottleDevice { + if x != nil { + return x.ThrottleWriteIOPSDevice + } + return nil +} + +type LinuxPids struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Maximum number of PIDs. Default is "no limit". + Limit int64 `protobuf:"varint,1,opt,name=Limit,proto3" json:"Limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxPids) Reset() { + *x = LinuxPids{} + mi := &file_oci_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxPids) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxPids) ProtoMessage() {} + +func (x *LinuxPids) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxPids.ProtoReflect.Descriptor instead. +func (*LinuxPids) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{22} +} + +func (x *LinuxPids) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +type LinuxDeviceCgroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Allow or deny + Allow bool `protobuf:"varint,1,opt,name=Allow,proto3" json:"Allow,omitempty"` + // Device type, block, char, etc. + Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` + // Major is the device's major number. + Major int64 `protobuf:"varint,3,opt,name=Major,proto3" json:"Major,omitempty"` + // Minor is the device's minor number. + Minor int64 `protobuf:"varint,4,opt,name=Minor,proto3" json:"Minor,omitempty"` + // Cgroup access permissions format, rwm. + Access string `protobuf:"bytes,5,opt,name=Access,proto3" json:"Access,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxDeviceCgroup) Reset() { + *x = LinuxDeviceCgroup{} + mi := &file_oci_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxDeviceCgroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxDeviceCgroup) ProtoMessage() {} + +func (x *LinuxDeviceCgroup) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxDeviceCgroup.ProtoReflect.Descriptor instead. +func (*LinuxDeviceCgroup) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{23} +} + +func (x *LinuxDeviceCgroup) GetAllow() bool { + if x != nil { + return x.Allow + } + return false +} + +func (x *LinuxDeviceCgroup) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *LinuxDeviceCgroup) GetMajor() int64 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *LinuxDeviceCgroup) GetMinor() int64 { + if x != nil { + return x.Minor + } + return 0 +} + +func (x *LinuxDeviceCgroup) GetAccess() string { + if x != nil { + return x.Access + } + return "" +} + +type LinuxNetwork struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Set class identifier for container's network packets + ClassID uint32 `protobuf:"varint,1,opt,name=ClassID,proto3" json:"ClassID,omitempty"` + // Set priority of network traffic for container + Priorities []*LinuxInterfacePriority `protobuf:"bytes,2,rep,name=Priorities,proto3" json:"Priorities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxNetwork) Reset() { + *x = LinuxNetwork{} + mi := &file_oci_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxNetwork) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxNetwork) ProtoMessage() {} + +func (x *LinuxNetwork) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxNetwork.ProtoReflect.Descriptor instead. +func (*LinuxNetwork) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{24} +} + +func (x *LinuxNetwork) GetClassID() uint32 { + if x != nil { + return x.ClassID + } + return 0 +} + +func (x *LinuxNetwork) GetPriorities() []*LinuxInterfacePriority { + if x != nil { + return x.Priorities + } + return nil +} + +type LinuxHugepageLimit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Pagesize is the hugepage size + Pagesize string `protobuf:"bytes,1,opt,name=Pagesize,proto3" json:"Pagesize,omitempty"` + // Limit is the limit of "hugepagesize" hugetlb usage + Limit uint64 `protobuf:"varint,2,opt,name=Limit,proto3" json:"Limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxHugepageLimit) Reset() { + *x = LinuxHugepageLimit{} + mi := &file_oci_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxHugepageLimit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxHugepageLimit) ProtoMessage() {} + +func (x *LinuxHugepageLimit) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxHugepageLimit.ProtoReflect.Descriptor instead. +func (*LinuxHugepageLimit) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{25} +} + +func (x *LinuxHugepageLimit) GetPagesize() string { + if x != nil { + return x.Pagesize + } + return "" +} + +func (x *LinuxHugepageLimit) GetLimit() uint64 { + if x != nil { + return x.Limit + } + return 0 +} + +type LinuxInterfacePriority struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name is the name of the network interface + Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` + // Priority for the interface + Priority uint32 `protobuf:"varint,2,opt,name=Priority,proto3" json:"Priority,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxInterfacePriority) Reset() { + *x = LinuxInterfacePriority{} + mi := &file_oci_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxInterfacePriority) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxInterfacePriority) ProtoMessage() {} + +func (x *LinuxInterfacePriority) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxInterfacePriority.ProtoReflect.Descriptor instead. +func (*LinuxInterfacePriority) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{26} +} + +func (x *LinuxInterfacePriority) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LinuxInterfacePriority) GetPriority() uint32 { + if x != nil { + return x.Priority + } + return 0 +} + +type LinuxSeccomp struct { + state protoimpl.MessageState `protogen:"open.v1"` + DefaultAction string `protobuf:"bytes,1,opt,name=DefaultAction,proto3" json:"DefaultAction,omitempty"` + Architectures []string `protobuf:"bytes,2,rep,name=Architectures,proto3" json:"Architectures,omitempty"` + Flags []string `protobuf:"bytes,3,rep,name=Flags,proto3" json:"Flags,omitempty"` + Syscalls []*LinuxSyscall `protobuf:"bytes,4,rep,name=Syscalls,proto3" json:"Syscalls,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxSeccomp) Reset() { + *x = LinuxSeccomp{} + mi := &file_oci_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxSeccomp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxSeccomp) ProtoMessage() {} + +func (x *LinuxSeccomp) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxSeccomp.ProtoReflect.Descriptor instead. +func (*LinuxSeccomp) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{27} +} + +func (x *LinuxSeccomp) GetDefaultAction() string { + if x != nil { + return x.DefaultAction + } + return "" +} + +func (x *LinuxSeccomp) GetArchitectures() []string { + if x != nil { + return x.Architectures + } + return nil +} + +func (x *LinuxSeccomp) GetFlags() []string { + if x != nil { + return x.Flags + } + return nil +} + +func (x *LinuxSeccomp) GetSyscalls() []*LinuxSyscall { + if x != nil { + return x.Syscalls + } + return nil +} + +type LinuxSeccompArg struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index uint64 `protobuf:"varint,1,opt,name=Index,proto3" json:"Index,omitempty"` + Value uint64 `protobuf:"varint,2,opt,name=Value,proto3" json:"Value,omitempty"` + ValueTwo uint64 `protobuf:"varint,3,opt,name=ValueTwo,proto3" json:"ValueTwo,omitempty"` + Op string `protobuf:"bytes,4,opt,name=Op,proto3" json:"Op,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxSeccompArg) Reset() { + *x = LinuxSeccompArg{} + mi := &file_oci_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxSeccompArg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxSeccompArg) ProtoMessage() {} + +func (x *LinuxSeccompArg) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxSeccompArg.ProtoReflect.Descriptor instead. +func (*LinuxSeccompArg) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{28} +} + +func (x *LinuxSeccompArg) GetIndex() uint64 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *LinuxSeccompArg) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *LinuxSeccompArg) GetValueTwo() uint64 { + if x != nil { + return x.ValueTwo + } + return 0 +} + +func (x *LinuxSeccompArg) GetOp() string { + if x != nil { + return x.Op + } + return "" +} + +type LinuxSyscall struct { + state protoimpl.MessageState `protogen:"open.v1"` + Names []string `protobuf:"bytes,1,rep,name=Names,proto3" json:"Names,omitempty"` + Action string `protobuf:"bytes,2,opt,name=Action,proto3" json:"Action,omitempty"` + // Types that are valid to be assigned to ErrnoRet: + // + // *LinuxSyscall_Errnoret + ErrnoRet isLinuxSyscall_ErrnoRet `protobuf_oneof:"ErrnoRet"` + Args []*LinuxSeccompArg `protobuf:"bytes,4,rep,name=Args,proto3" json:"Args,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxSyscall) Reset() { + *x = LinuxSyscall{} + mi := &file_oci_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxSyscall) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxSyscall) ProtoMessage() {} + +func (x *LinuxSyscall) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxSyscall.ProtoReflect.Descriptor instead. +func (*LinuxSyscall) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{29} +} + +func (x *LinuxSyscall) GetNames() []string { + if x != nil { + return x.Names + } + return nil +} + +func (x *LinuxSyscall) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *LinuxSyscall) GetErrnoRet() isLinuxSyscall_ErrnoRet { + if x != nil { + return x.ErrnoRet + } + return nil +} + +func (x *LinuxSyscall) GetErrnoret() uint32 { + if x != nil { + if x, ok := x.ErrnoRet.(*LinuxSyscall_Errnoret); ok { + return x.Errnoret + } + } + return 0 +} + +func (x *LinuxSyscall) GetArgs() []*LinuxSeccompArg { + if x != nil { + return x.Args + } + return nil +} + +type isLinuxSyscall_ErrnoRet interface { + isLinuxSyscall_ErrnoRet() +} + +type LinuxSyscall_Errnoret struct { + Errnoret uint32 `protobuf:"varint,3,opt,name=errnoret,proto3,oneof"` +} + +func (*LinuxSyscall_Errnoret) isLinuxSyscall_ErrnoRet() {} + +type LinuxIntelRdt struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The schema for L3 cache id and capacity bitmask (CBM) + // Format: "L3:=;=;..." + L3CacheSchema string `protobuf:"bytes,1,opt,name=L3CacheSchema,proto3" json:"L3CacheSchema,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinuxIntelRdt) Reset() { + *x = LinuxIntelRdt{} + mi := &file_oci_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinuxIntelRdt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinuxIntelRdt) ProtoMessage() {} + +func (x *LinuxIntelRdt) ProtoReflect() protoreflect.Message { + mi := &file_oci_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinuxIntelRdt.ProtoReflect.Descriptor instead. +func (*LinuxIntelRdt) Descriptor() ([]byte, []int) { + return file_oci_proto_rawDescGZIP(), []int{30} +} + +func (x *LinuxIntelRdt) GetL3CacheSchema() string { + if x != nil { + return x.L3CacheSchema + } + return "" +} + +var File_oci_proto protoreflect.FileDescriptor + +const file_oci_proto_rawDesc = "" + + "\n" + + "\toci.proto\x12\x04grpc\"\xc1\x03\n" + + "\x04Spec\x12\x18\n" + + "\aVersion\x18\x01 \x01(\tR\aVersion\x12'\n" + + "\aProcess\x18\x02 \x01(\v2\r.grpc.ProcessR\aProcess\x12\x1e\n" + + "\x04Root\x18\x03 \x01(\v2\n" + + ".grpc.RootR\x04Root\x12\x1a\n" + + "\bHostname\x18\x04 \x01(\tR\bHostname\x12#\n" + + "\x06Mounts\x18\x05 \x03(\v2\v.grpc.MountR\x06Mounts\x12!\n" + + "\x05Hooks\x18\x06 \x01(\v2\v.grpc.HooksR\x05Hooks\x12=\n" + + "\vAnnotations\x18\a \x03(\v2\x1b.grpc.Spec.AnnotationsEntryR\vAnnotations\x12!\n" + + "\x05Linux\x18\b \x01(\v2\v.grpc.LinuxR\x05Linux\x12'\n" + + "\aSolaris\x18\t \x01(\v2\r.grpc.SolarisR\aSolaris\x12'\n" + + "\aWindows\x18\n" + + " \x01(\v2\r.grpc.WindowsR\aWindows\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xae\x03\n" + + "\aProcess\x12\x1a\n" + + "\bTerminal\x18\x01 \x01(\bR\bTerminal\x12+\n" + + "\vConsoleSize\x18\x02 \x01(\v2\t.grpc.BoxR\vConsoleSize\x12\x1e\n" + + "\x04User\x18\x03 \x01(\v2\n" + + ".grpc.UserR\x04User\x12\x12\n" + + "\x04Args\x18\x04 \x03(\tR\x04Args\x12\x10\n" + + "\x03Env\x18\x05 \x03(\tR\x03Env\x12\x10\n" + + "\x03Cwd\x18\x06 \x01(\tR\x03Cwd\x12;\n" + + "\fCapabilities\x18\a \x01(\v2\x17.grpc.LinuxCapabilitiesR\fCapabilities\x12+\n" + + "\aRlimits\x18\b \x03(\v2\x11.grpc.POSIXRlimitR\aRlimits\x12(\n" + + "\x0fNoNewPrivileges\x18\t \x01(\bR\x0fNoNewPrivileges\x12(\n" + + "\x0fApparmorProfile\x18\n" + + " \x01(\tR\x0fApparmorProfile\x12 \n" + + "\vOOMScoreAdj\x18\v \x01(\x03R\vOOMScoreAdj\x12\"\n" + + "\fSelinuxLabel\x18\f \x01(\tR\fSelinuxLabel\"3\n" + + "\x03Box\x12\x16\n" + + "\x06Height\x18\x01 \x01(\rR\x06Height\x12\x14\n" + + "\x05Width\x18\x02 \x01(\rR\x05Width\"n\n" + + "\x04User\x12\x10\n" + + "\x03UID\x18\x01 \x01(\rR\x03UID\x12\x10\n" + + "\x03GID\x18\x02 \x01(\rR\x03GID\x12&\n" + + "\x0eAdditionalGids\x18\x03 \x03(\rR\x0eAdditionalGids\x12\x1a\n" + + "\bUsername\x18\x04 \x01(\tR\bUsername\"\xa7\x01\n" + + "\x11LinuxCapabilities\x12\x1a\n" + + "\bBounding\x18\x01 \x03(\tR\bBounding\x12\x1c\n" + + "\tEffective\x18\x02 \x03(\tR\tEffective\x12 \n" + + "\vInheritable\x18\x03 \x03(\tR\vInheritable\x12\x1c\n" + + "\tPermitted\x18\x04 \x03(\tR\tPermitted\x12\x18\n" + + "\aAmbient\x18\x05 \x03(\tR\aAmbient\"I\n" + + "\vPOSIXRlimit\x12\x12\n" + + "\x04Type\x18\x01 \x01(\tR\x04Type\x12\x12\n" + + "\x04Hard\x18\x02 \x01(\x04R\x04Hard\x12\x12\n" + + "\x04Soft\x18\x03 \x01(\x04R\x04Soft\"o\n" + + "\x05Mount\x12 \n" + + "\vdestination\x18\x01 \x01(\tR\vdestination\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x18\n" + + "\aoptions\x18\x04 \x03(\tR\aoptions\"6\n" + + "\x04Root\x12\x12\n" + + "\x04Path\x18\x01 \x01(\tR\x04Path\x12\x1a\n" + + "\bReadonly\x18\x02 \x01(\bR\bReadonly\"\x9d\x02\n" + + "\x05Hooks\x12&\n" + + "\bPrestart\x18\x01 \x03(\v2\n" + + ".grpc.HookR\bPrestart\x12(\n" + + "\tPoststart\x18\x02 \x03(\v2\n" + + ".grpc.HookR\tPoststart\x12&\n" + + "\bPoststop\x18\x03 \x03(\v2\n" + + ".grpc.HookR\bPoststop\x120\n" + + "\rCreateRuntime\x18\x04 \x03(\v2\n" + + ".grpc.HookR\rCreateRuntime\x124\n" + + "\x0fCreateContainer\x18\x05 \x03(\v2\n" + + ".grpc.HookR\x0fCreateContainer\x122\n" + + "\x0eStartContainer\x18\x06 \x03(\v2\n" + + ".grpc.HookR\x0eStartContainer\"Z\n" + + "\x04Hook\x12\x12\n" + + "\x04Path\x18\x01 \x01(\tR\x04Path\x12\x12\n" + + "\x04Args\x18\x02 \x03(\tR\x04Args\x12\x10\n" + + "\x03Env\x18\x03 \x03(\tR\x03Env\x12\x18\n" + + "\aTimeout\x18\x04 \x01(\x03R\aTimeout\"\x91\x05\n" + + "\x05Linux\x126\n" + + "\vUIDMappings\x18\x01 \x03(\v2\x14.grpc.LinuxIDMappingR\vUIDMappings\x126\n" + + "\vGIDMappings\x18\x02 \x03(\v2\x14.grpc.LinuxIDMappingR\vGIDMappings\x12/\n" + + "\x06Sysctl\x18\x03 \x03(\v2\x17.grpc.Linux.SysctlEntryR\x06Sysctl\x122\n" + + "\tResources\x18\x04 \x01(\v2\x14.grpc.LinuxResourcesR\tResources\x12 \n" + + "\vCgroupsPath\x18\x05 \x01(\tR\vCgroupsPath\x124\n" + + "\n" + + "Namespaces\x18\x06 \x03(\v2\x14.grpc.LinuxNamespaceR\n" + + "Namespaces\x12+\n" + + "\aDevices\x18\a \x03(\v2\x11.grpc.LinuxDeviceR\aDevices\x12,\n" + + "\aSeccomp\x18\b \x01(\v2\x12.grpc.LinuxSeccompR\aSeccomp\x12,\n" + + "\x11RootfsPropagation\x18\t \x01(\tR\x11RootfsPropagation\x12 \n" + + "\vMaskedPaths\x18\n" + + " \x03(\tR\vMaskedPaths\x12$\n" + + "\rReadonlyPaths\x18\v \x03(\tR\rReadonlyPaths\x12\x1e\n" + + "\n" + + "MountLabel\x18\f \x01(\tR\n" + + "MountLabel\x12/\n" + + "\bIntelRdt\x18\r \x01(\v2\x13.grpc.LinuxIntelRdtR\bIntelRdt\x1a9\n" + + "\vSysctlEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x1f\n" + + "\aWindows\x12\x14\n" + + "\x05dummy\x18\x01 \x01(\tR\x05dummy\"\x1f\n" + + "\aSolaris\x12\x14\n" + + "\x05dummy\x18\x01 \x01(\tR\x05dummy\"^\n" + + "\x0eLinuxIDMapping\x12\x16\n" + + "\x06HostID\x18\x01 \x01(\rR\x06HostID\x12 \n" + + "\vContainerID\x18\x02 \x01(\rR\vContainerID\x12\x12\n" + + "\x04Size\x18\x03 \x01(\rR\x04Size\"8\n" + + "\x0eLinuxNamespace\x12\x12\n" + + "\x04Type\x18\x01 \x01(\tR\x04Type\x12\x12\n" + + "\x04Path\x18\x02 \x01(\tR\x04Path\"\xa1\x01\n" + + "\vLinuxDevice\x12\x12\n" + + "\x04Path\x18\x01 \x01(\tR\x04Path\x12\x12\n" + + "\x04Type\x18\x02 \x01(\tR\x04Type\x12\x14\n" + + "\x05Major\x18\x03 \x01(\x03R\x05Major\x12\x14\n" + + "\x05Minor\x18\x04 \x01(\x03R\x05Minor\x12\x1a\n" + + "\bFileMode\x18\x05 \x01(\rR\bFileMode\x12\x10\n" + + "\x03UID\x18\x06 \x01(\rR\x03UID\x12\x10\n" + + "\x03GID\x18\a \x01(\rR\x03GID\"\xd3\x02\n" + + "\x0eLinuxResources\x121\n" + + "\aDevices\x18\x01 \x03(\v2\x17.grpc.LinuxDeviceCgroupR\aDevices\x12)\n" + + "\x06Memory\x18\x02 \x01(\v2\x11.grpc.LinuxMemoryR\x06Memory\x12 \n" + + "\x03CPU\x18\x03 \x01(\v2\x0e.grpc.LinuxCPUR\x03CPU\x12#\n" + + "\x04Pids\x18\x04 \x01(\v2\x0f.grpc.LinuxPidsR\x04Pids\x12,\n" + + "\aBlockIO\x18\x05 \x01(\v2\x12.grpc.LinuxBlockIOR\aBlockIO\x12@\n" + + "\x0eHugepageLimits\x18\x06 \x03(\v2\x18.grpc.LinuxHugepageLimitR\x0eHugepageLimits\x12,\n" + + "\aNetwork\x18\a \x01(\v2\x12.grpc.LinuxNetworkR\aNetwork\"\xdb\x01\n" + + "\vLinuxMemory\x12\x14\n" + + "\x05Limit\x18\x01 \x01(\x03R\x05Limit\x12 \n" + + "\vReservation\x18\x02 \x01(\x03R\vReservation\x12\x12\n" + + "\x04Swap\x18\x03 \x01(\x03R\x04Swap\x12\x16\n" + + "\x06Kernel\x18\x04 \x01(\x03R\x06Kernel\x12\x1c\n" + + "\tKernelTCP\x18\x05 \x01(\x03R\tKernelTCP\x12\x1e\n" + + "\n" + + "Swappiness\x18\x06 \x01(\x04R\n" + + "Swappiness\x12*\n" + + "\x10DisableOOMKiller\x18\a \x01(\bR\x10DisableOOMKiller\"\xca\x01\n" + + "\bLinuxCPU\x12\x16\n" + + "\x06Shares\x18\x01 \x01(\x04R\x06Shares\x12\x14\n" + + "\x05Quota\x18\x02 \x01(\x03R\x05Quota\x12\x16\n" + + "\x06Period\x18\x03 \x01(\x04R\x06Period\x12(\n" + + "\x0fRealtimeRuntime\x18\x04 \x01(\x03R\x0fRealtimeRuntime\x12&\n" + + "\x0eRealtimePeriod\x18\x05 \x01(\x04R\x0eRealtimePeriod\x12\x12\n" + + "\x04Cpus\x18\x06 \x01(\tR\x04Cpus\x12\x12\n" + + "\x04Mems\x18\a \x01(\tR\x04Mems\"w\n" + + "\x11LinuxWeightDevice\x12\x14\n" + + "\x05Major\x18\x01 \x01(\x03R\x05Major\x12\x14\n" + + "\x05Minor\x18\x02 \x01(\x03R\x05Minor\x12\x16\n" + + "\x06Weight\x18\x03 \x01(\rR\x06Weight\x12\x1e\n" + + "\n" + + "LeafWeight\x18\x04 \x01(\rR\n" + + "LeafWeight\"U\n" + + "\x13LinuxThrottleDevice\x12\x14\n" + + "\x05Major\x18\x01 \x01(\x03R\x05Major\x12\x14\n" + + "\x05Minor\x18\x02 \x01(\x03R\x05Minor\x12\x12\n" + + "\x04Rate\x18\x03 \x01(\x04R\x04Rate\"\xcf\x03\n" + + "\fLinuxBlockIO\x12\x16\n" + + "\x06Weight\x18\x01 \x01(\rR\x06Weight\x12\x1e\n" + + "\n" + + "LeafWeight\x18\x02 \x01(\rR\n" + + "LeafWeight\x12;\n" + + "\fWeightDevice\x18\x03 \x03(\v2\x17.grpc.LinuxWeightDeviceR\fWeightDevice\x12O\n" + + "\x15ThrottleReadBpsDevice\x18\x04 \x03(\v2\x19.grpc.LinuxThrottleDeviceR\x15ThrottleReadBpsDevice\x12Q\n" + + "\x16ThrottleWriteBpsDevice\x18\x05 \x03(\v2\x19.grpc.LinuxThrottleDeviceR\x16ThrottleWriteBpsDevice\x12Q\n" + + "\x16ThrottleReadIOPSDevice\x18\x06 \x03(\v2\x19.grpc.LinuxThrottleDeviceR\x16ThrottleReadIOPSDevice\x12S\n" + + "\x17ThrottleWriteIOPSDevice\x18\a \x03(\v2\x19.grpc.LinuxThrottleDeviceR\x17ThrottleWriteIOPSDevice\"!\n" + + "\tLinuxPids\x12\x14\n" + + "\x05Limit\x18\x01 \x01(\x03R\x05Limit\"\x81\x01\n" + + "\x11LinuxDeviceCgroup\x12\x14\n" + + "\x05Allow\x18\x01 \x01(\bR\x05Allow\x12\x12\n" + + "\x04Type\x18\x02 \x01(\tR\x04Type\x12\x14\n" + + "\x05Major\x18\x03 \x01(\x03R\x05Major\x12\x14\n" + + "\x05Minor\x18\x04 \x01(\x03R\x05Minor\x12\x16\n" + + "\x06Access\x18\x05 \x01(\tR\x06Access\"f\n" + + "\fLinuxNetwork\x12\x18\n" + + "\aClassID\x18\x01 \x01(\rR\aClassID\x12<\n" + + "\n" + + "Priorities\x18\x02 \x03(\v2\x1c.grpc.LinuxInterfacePriorityR\n" + + "Priorities\"F\n" + + "\x12LinuxHugepageLimit\x12\x1a\n" + + "\bPagesize\x18\x01 \x01(\tR\bPagesize\x12\x14\n" + + "\x05Limit\x18\x02 \x01(\x04R\x05Limit\"H\n" + + "\x16LinuxInterfacePriority\x12\x12\n" + + "\x04Name\x18\x01 \x01(\tR\x04Name\x12\x1a\n" + + "\bPriority\x18\x02 \x01(\rR\bPriority\"\xa0\x01\n" + + "\fLinuxSeccomp\x12$\n" + + "\rDefaultAction\x18\x01 \x01(\tR\rDefaultAction\x12$\n" + + "\rArchitectures\x18\x02 \x03(\tR\rArchitectures\x12\x14\n" + + "\x05Flags\x18\x03 \x03(\tR\x05Flags\x12.\n" + + "\bSyscalls\x18\x04 \x03(\v2\x12.grpc.LinuxSyscallR\bSyscalls\"i\n" + + "\x0fLinuxSeccompArg\x12\x14\n" + + "\x05Index\x18\x01 \x01(\x04R\x05Index\x12\x14\n" + + "\x05Value\x18\x02 \x01(\x04R\x05Value\x12\x1a\n" + + "\bValueTwo\x18\x03 \x01(\x04R\bValueTwo\x12\x0e\n" + + "\x02Op\x18\x04 \x01(\tR\x02Op\"\x91\x01\n" + + "\fLinuxSyscall\x12\x14\n" + + "\x05Names\x18\x01 \x03(\tR\x05Names\x12\x16\n" + + "\x06Action\x18\x02 \x01(\tR\x06Action\x12\x1c\n" + + "\berrnoret\x18\x03 \x01(\rH\x00R\berrnoret\x12)\n" + + "\x04Args\x18\x04 \x03(\v2\x15.grpc.LinuxSeccompArgR\x04ArgsB\n" + + "\n" + + "\bErrnoRet\"5\n" + + "\rLinuxIntelRdt\x12$\n" + + "\rL3CacheSchema\x18\x01 \x01(\tR\rL3CacheSchemaBVZTgithub.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata/agentpb;agentpbb\x06proto3" + +var ( + file_oci_proto_rawDescOnce sync.Once + file_oci_proto_rawDescData []byte +) + +func file_oci_proto_rawDescGZIP() []byte { + file_oci_proto_rawDescOnce.Do(func() { + file_oci_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_oci_proto_rawDesc), len(file_oci_proto_rawDesc))) + }) + return file_oci_proto_rawDescData +} + +var file_oci_proto_msgTypes = make([]protoimpl.MessageInfo, 33) +var file_oci_proto_goTypes = []any{ + (*Spec)(nil), // 0: grpc.Spec + (*Process)(nil), // 1: grpc.Process + (*Box)(nil), // 2: grpc.Box + (*User)(nil), // 3: grpc.User + (*LinuxCapabilities)(nil), // 4: grpc.LinuxCapabilities + (*POSIXRlimit)(nil), // 5: grpc.POSIXRlimit + (*Mount)(nil), // 6: grpc.Mount + (*Root)(nil), // 7: grpc.Root + (*Hooks)(nil), // 8: grpc.Hooks + (*Hook)(nil), // 9: grpc.Hook + (*Linux)(nil), // 10: grpc.Linux + (*Windows)(nil), // 11: grpc.Windows + (*Solaris)(nil), // 12: grpc.Solaris + (*LinuxIDMapping)(nil), // 13: grpc.LinuxIDMapping + (*LinuxNamespace)(nil), // 14: grpc.LinuxNamespace + (*LinuxDevice)(nil), // 15: grpc.LinuxDevice + (*LinuxResources)(nil), // 16: grpc.LinuxResources + (*LinuxMemory)(nil), // 17: grpc.LinuxMemory + (*LinuxCPU)(nil), // 18: grpc.LinuxCPU + (*LinuxWeightDevice)(nil), // 19: grpc.LinuxWeightDevice + (*LinuxThrottleDevice)(nil), // 20: grpc.LinuxThrottleDevice + (*LinuxBlockIO)(nil), // 21: grpc.LinuxBlockIO + (*LinuxPids)(nil), // 22: grpc.LinuxPids + (*LinuxDeviceCgroup)(nil), // 23: grpc.LinuxDeviceCgroup + (*LinuxNetwork)(nil), // 24: grpc.LinuxNetwork + (*LinuxHugepageLimit)(nil), // 25: grpc.LinuxHugepageLimit + (*LinuxInterfacePriority)(nil), // 26: grpc.LinuxInterfacePriority + (*LinuxSeccomp)(nil), // 27: grpc.LinuxSeccomp + (*LinuxSeccompArg)(nil), // 28: grpc.LinuxSeccompArg + (*LinuxSyscall)(nil), // 29: grpc.LinuxSyscall + (*LinuxIntelRdt)(nil), // 30: grpc.LinuxIntelRdt + nil, // 31: grpc.Spec.AnnotationsEntry + nil, // 32: grpc.Linux.SysctlEntry +} +var file_oci_proto_depIdxs = []int32{ + 1, // 0: grpc.Spec.Process:type_name -> grpc.Process + 7, // 1: grpc.Spec.Root:type_name -> grpc.Root + 6, // 2: grpc.Spec.Mounts:type_name -> grpc.Mount + 8, // 3: grpc.Spec.Hooks:type_name -> grpc.Hooks + 31, // 4: grpc.Spec.Annotations:type_name -> grpc.Spec.AnnotationsEntry + 10, // 5: grpc.Spec.Linux:type_name -> grpc.Linux + 12, // 6: grpc.Spec.Solaris:type_name -> grpc.Solaris + 11, // 7: grpc.Spec.Windows:type_name -> grpc.Windows + 2, // 8: grpc.Process.ConsoleSize:type_name -> grpc.Box + 3, // 9: grpc.Process.User:type_name -> grpc.User + 4, // 10: grpc.Process.Capabilities:type_name -> grpc.LinuxCapabilities + 5, // 11: grpc.Process.Rlimits:type_name -> grpc.POSIXRlimit + 9, // 12: grpc.Hooks.Prestart:type_name -> grpc.Hook + 9, // 13: grpc.Hooks.Poststart:type_name -> grpc.Hook + 9, // 14: grpc.Hooks.Poststop:type_name -> grpc.Hook + 9, // 15: grpc.Hooks.CreateRuntime:type_name -> grpc.Hook + 9, // 16: grpc.Hooks.CreateContainer:type_name -> grpc.Hook + 9, // 17: grpc.Hooks.StartContainer:type_name -> grpc.Hook + 13, // 18: grpc.Linux.UIDMappings:type_name -> grpc.LinuxIDMapping + 13, // 19: grpc.Linux.GIDMappings:type_name -> grpc.LinuxIDMapping + 32, // 20: grpc.Linux.Sysctl:type_name -> grpc.Linux.SysctlEntry + 16, // 21: grpc.Linux.Resources:type_name -> grpc.LinuxResources + 14, // 22: grpc.Linux.Namespaces:type_name -> grpc.LinuxNamespace + 15, // 23: grpc.Linux.Devices:type_name -> grpc.LinuxDevice + 27, // 24: grpc.Linux.Seccomp:type_name -> grpc.LinuxSeccomp + 30, // 25: grpc.Linux.IntelRdt:type_name -> grpc.LinuxIntelRdt + 23, // 26: grpc.LinuxResources.Devices:type_name -> grpc.LinuxDeviceCgroup + 17, // 27: grpc.LinuxResources.Memory:type_name -> grpc.LinuxMemory + 18, // 28: grpc.LinuxResources.CPU:type_name -> grpc.LinuxCPU + 22, // 29: grpc.LinuxResources.Pids:type_name -> grpc.LinuxPids + 21, // 30: grpc.LinuxResources.BlockIO:type_name -> grpc.LinuxBlockIO + 25, // 31: grpc.LinuxResources.HugepageLimits:type_name -> grpc.LinuxHugepageLimit + 24, // 32: grpc.LinuxResources.Network:type_name -> grpc.LinuxNetwork + 19, // 33: grpc.LinuxBlockIO.WeightDevice:type_name -> grpc.LinuxWeightDevice + 20, // 34: grpc.LinuxBlockIO.ThrottleReadBpsDevice:type_name -> grpc.LinuxThrottleDevice + 20, // 35: grpc.LinuxBlockIO.ThrottleWriteBpsDevice:type_name -> grpc.LinuxThrottleDevice + 20, // 36: grpc.LinuxBlockIO.ThrottleReadIOPSDevice:type_name -> grpc.LinuxThrottleDevice + 20, // 37: grpc.LinuxBlockIO.ThrottleWriteIOPSDevice:type_name -> grpc.LinuxThrottleDevice + 26, // 38: grpc.LinuxNetwork.Priorities:type_name -> grpc.LinuxInterfacePriority + 29, // 39: grpc.LinuxSeccomp.Syscalls:type_name -> grpc.LinuxSyscall + 28, // 40: grpc.LinuxSyscall.Args:type_name -> grpc.LinuxSeccompArg + 41, // [41:41] is the sub-list for method output_type + 41, // [41:41] is the sub-list for method input_type + 41, // [41:41] is the sub-list for extension type_name + 41, // [41:41] is the sub-list for extension extendee + 0, // [0:41] is the sub-list for field type_name +} + +func init() { file_oci_proto_init() } +func file_oci_proto_init() { + if File_oci_proto != nil { + return + } + file_oci_proto_msgTypes[29].OneofWrappers = []any{ + (*LinuxSyscall_Errnoret)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_oci_proto_rawDesc), len(file_oci_proto_rawDesc)), + NumEnums: 0, + NumMessages: 33, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_oci_proto_goTypes, + DependencyIndexes: file_oci_proto_depIdxs, + MessageInfos: file_oci_proto_msgTypes, + }.Build() + File_oci_proto = out.File + file_oci_proto_goTypes = nil + file_oci_proto_depIdxs = nil +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.proto b/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.proto new file mode 100644 index 000000000..cad5fd83d --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/oci.proto @@ -0,0 +1,476 @@ +// +// Copyright (c) 2017 Intel Corporation +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +syntax = "proto3"; +option go_package = "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb;agentpb"; + +package grpc; + +message Spec { + // Version of the Open Container Initiative Runtime Specification with which the bundle complies. + string Version = 1; + + // Process configures the container process. + Process Process = 2; + + // Root configures the container's root filesystem. + Root Root = 3; + + // Hostname configures the container's hostname. + string Hostname = 4; + + // Mounts configures additional mounts (on top of Root). + repeated Mount Mounts = 5; + + // Hooks configures callbacks for container lifecycle events. + Hooks Hooks = 6; + + // Annotations contains arbitrary metadata for the container. + map Annotations = 7; + + // Linux is platform-specific configuration for Linux based containers. + Linux Linux = 8; + + // Solaris is platform-specific configuration for Solaris based containers. + Solaris Solaris = 9; + // Windows is platform-specific configuration for Windows based containers. + Windows Windows = 10; +} + +message Process { + // Terminal creates an interactive terminal for the container. + bool Terminal = 1; + + // ConsoleSize specifies the size of the console. + Box ConsoleSize = 2; + + // User specifies user information for the process. + User User = 3; + + // Args specifies the binary and arguments for the application to execute. + repeated string Args = 4; + + // Env populates the process environment for the process. + repeated string Env = 5; + + // Cwd is the current working directory for the process and must be + // relative to the container's root. + string Cwd = 6; + + // Capabilities are Linux capabilities that are kept for the process. + LinuxCapabilities Capabilities = 7; + + // Rlimits specifies rlimit options to apply to the process. + repeated POSIXRlimit Rlimits = 8; + + // NoNewPrivileges controls whether additional privileges could be gained by processes in the container. + bool NoNewPrivileges = 9; + + // ApparmorProfile specifies the apparmor profile for the container. + string ApparmorProfile = 10; + + // Specify an oom_score_adj for the container. + int64 OOMScoreAdj = 11; + + // SelinuxLabel specifies the selinux context that the container process is run as. + string SelinuxLabel = 12; +} + +message Box { + // Height is the vertical dimension of a box. + uint32 Height = 1; + + // Width is the horizontal dimension of a box. + uint32 Width = 2; +} + +message User { + // UID is the user id. + uint32 UID = 1; + + // GID is the group id. + uint32 GID = 2; + + // AdditionalGids are additional group ids set for the container's process. + repeated uint32 AdditionalGids = 3; + + // Username is the user name. + string Username = 4; +} + +message LinuxCapabilities { + // Bounding is the set of capabilities checked by the kernel. + repeated string Bounding = 1; + + // Effective is the set of capabilities checked by the kernel. + repeated string Effective = 2; + + // Inheritable is the capabilities preserved across execve. + repeated string Inheritable = 3; + + // Permitted is the limiting superset for effective capabilities. + repeated string Permitted = 4; + + // Ambient is the ambient set of capabilities that are kept. + repeated string Ambient = 5; +} + +message POSIXRlimit { + // Type of the rlimit to set + string Type = 1; + + // Hard is the hard limit for the specified type + uint64 Hard = 2; + + // Soft is the soft limit for the specified type + uint64 Soft = 3; +} + +message Mount { + // destination is the path inside the container expect when it starts with "tmp:/" + string destination = 1; + + // source is the path inside the container expect when it starts with "vm:/dev/" or "tmp:/" + // the path which starts with "vm:/dev/" refers the guest vm's "/dev", + // especially, "vm:/dev/hostfs/" refers to the shared filesystem. + // "tmp:/" is a temporary directory which is used for temporary mounts. + string source = 2; + string type = 3; + repeated string options = 4; +} + +message Root { + // Path is the absolute path to the container's root filesystem. + string Path = 1; + + // Readonly makes the root filesystem for the container readonly before the process is executed. + bool Readonly = 2; +} + +message Hooks { + // Prestart is a list of hooks to be run before the container process is executed. + repeated Hook Prestart = 1; + + // Poststart is a list of hooks to be run after the container process is started. + repeated Hook Poststart = 2; + + // Poststop is a list of hooks to be run after the container process exits. + repeated Hook Poststop = 3; + + // Createruntime is a list of hooks to be run during the creation of runtime(sandbox). + repeated Hook CreateRuntime = 4; + + // CreateContainer is a list of hooks to be run after VM is started, and before container is created. + repeated Hook CreateContainer = 5; + + // StartContainer is a list of hooks to be run after container is created, but before it is started. + repeated Hook StartContainer = 6; +} + +message Hook { + // Path is the absolute path to the container's root filesystem. + string Path = 1; + + // Arguments used for the binary, including the binary name itself. + repeated string Args = 2; + + // Additional `key=value` environment variables. + repeated string Env = 3; + + // Timeout is the number of seconds before aborting the hook. If set, timeout MUST be greater than zero. + int64 Timeout = 4; +} + +message Linux { + // UIDMapping specifies user mappings for supporting user namespaces. + repeated LinuxIDMapping UIDMappings = 1; + + // GIDMapping specifies group mappings for supporting user namespaces. + repeated LinuxIDMapping GIDMappings = 2; + + // Sysctl are a set of key value pairs that are set for the container on start + map Sysctl = 3; + + // Resources contain cgroup information for handling resource constraints + // for the container + LinuxResources Resources = 4; + + // CgroupsPath specifies the path to cgroups that are created and/or joined by the container. + // The path is expected to be relative to the cgroups mountpoint. + // If resources are specified, the cgroups at CgroupsPath will be updated based on resources. + string CgroupsPath = 5; + + // Namespaces contains the namespaces that are created and/or joined by the container + repeated LinuxNamespace Namespaces = 6; + + // Devices are a list of device nodes that are created for the container + repeated LinuxDevice Devices = 7; + + // Seccomp specifies the seccomp security settings for the container. + LinuxSeccomp Seccomp = 8; + + // RootfsPropagation is the rootfs mount propagation mode for the container. + string RootfsPropagation = 9; + + // MaskedPaths masks over the provided paths inside the container. + repeated string MaskedPaths = 10; + + // ReadonlyPaths sets the provided paths as RO inside the container. + repeated string ReadonlyPaths = 11; + + // MountLabel specifies the selinux context for the mounts in the container. + string MountLabel = 12; + + // IntelRdt contains Intel Resource Director Technology (RDT) information + // for handling resource constraints (e.g., L3 cache) for the container + LinuxIntelRdt IntelRdt = 13; +} + +message Windows { + // Dummy string, never used. + string dummy = 1; +} + +message Solaris { + // Dummy string, never used. + string dummy = 1; +} + +message LinuxIDMapping { + // HostID is the starting UID/GID on the host to be mapped to 'ContainerID' + uint32 HostID = 1; + + // ContainerID is the starting UID/GID in the container + uint32 ContainerID = 2; + + // Size is the number of IDs to be mapped + uint32 Size = 3; +} + +message LinuxNamespace { + // Type is the type of namespace + string Type = 1; + + // Path is a path to an existing namespace persisted on disk that can be joined + // and is of the same type + string Path = 2; +} + +message LinuxDevice { + // Path to the device. + string Path = 1; + + // Device type, block, char, etc. + string Type = 2; + + // Major is the device's major number. + int64 Major = 3; + + // Minor is the device's minor number. + int64 Minor = 4; + + // FileMode permission bits for the device. + uint32 FileMode = 5; + + // UID of the device. + uint32 UID = 6; + + // Gid of the device. + uint32 GID = 7; +} + +message LinuxResources { + // Devices configures the device whitelist. + repeated LinuxDeviceCgroup Devices = 1; + + // Memory restriction configuration + LinuxMemory Memory = 2; + + // CPU resource restriction configuration + LinuxCPU CPU = 3; + + // Task resource restriction configuration. + LinuxPids Pids = 4; + + // BlockIO restriction configuration + LinuxBlockIO BlockIO = 5; + + // Hugetlb limit (in bytes) + repeated LinuxHugepageLimit HugepageLimits = 6; + + // Network restriction configuration + LinuxNetwork Network = 7; +} + +message LinuxMemory { + // Memory limit (in bytes). + int64 Limit = 1; + + // Memory reservation or soft_limit (in bytes). + int64 Reservation = 2; + + // Total memory limit (memory + swap). + int64 Swap = 3; + + // Kernel memory limit (in bytes). + int64 Kernel = 4; + + // Kernel memory limit for tcp (in bytes) + int64 KernelTCP = 5; + + // How aggressive the kernel will swap memory pages. + uint64 Swappiness = 6; + + // DisableOOMKiller disables the OOM killer for out of memory conditions + bool DisableOOMKiller = 7; +} + +message LinuxCPU { + // CPU shares (relative weight (ratio) vs. other cgroups with cpu shares). + uint64 Shares = 1; + + // CPU hardcap limit (in usecs). Allowed cpu time in a given period. + int64 Quota = 2; + + // CPU period to be used for hardcapping (in usecs). + uint64 Period = 3; + + // How much time realtime scheduling may use (in usecs). + int64 RealtimeRuntime = 4; + + // CPU period to be used for realtime scheduling (in usecs). + uint64 RealtimePeriod = 5; + + // CPUs to use within the cpuset. Default is to use any CPU available. + string Cpus = 6; + + // List of memory nodes in the cpuset. Default is to use any available memory node. + string Mems = 7; +} + +message LinuxWeightDevice { + // Major is the device's major number. + int64 Major = 1; + + // Minor is the device's minor number. + int64 Minor = 2; + + // Weight is the bandwidth rate for the device. + uint32 Weight = 3; + + // LeafWeight is the bandwidth rate for the device while competing with the cgroup's child cgroups, CFQ scheduler only + uint32 LeafWeight = 4; +} + +message LinuxThrottleDevice { + // Major is the device's major number. + int64 Major = 1; + + // Minor is the device's minor number. + int64 Minor = 2; + + // Rate is the IO rate limit per cgroup per device + uint64 Rate = 3; +} + +message LinuxBlockIO { + // Specifies per cgroup weight + uint32 Weight = 1; + + // Specifies tasks' weight in the given cgroup while competing with the cgroup's child cgroups, CFQ scheduler only + uint32 LeafWeight = 2; + + // Weight per cgroup per device, can override BlkioWeight + repeated LinuxWeightDevice WeightDevice = 3; + + // IO read rate limit per cgroup per device, bytes per second + repeated LinuxThrottleDevice ThrottleReadBpsDevice = 4; + + // IO write rate limit per cgroup per device, bytes per second + repeated LinuxThrottleDevice ThrottleWriteBpsDevice = 5; + + // IO read rate limit per cgroup per device, IO per second + repeated LinuxThrottleDevice ThrottleReadIOPSDevice = 6; + + // IO write rate limit per cgroup per device, IO per second + repeated LinuxThrottleDevice ThrottleWriteIOPSDevice = 7; +} + +message LinuxPids { + // Maximum number of PIDs. Default is "no limit". + int64 Limit = 1; +} + +message LinuxDeviceCgroup { + // Allow or deny + bool Allow = 1; + + // Device type, block, char, etc. + string Type = 2; + + // Major is the device's major number. + int64 Major = 3; + + // Minor is the device's minor number. + int64 Minor = 4; + + // Cgroup access permissions format, rwm. + string Access = 5; +} + +message LinuxNetwork { + // Set class identifier for container's network packets + uint32 ClassID = 1; + + // Set priority of network traffic for container + repeated LinuxInterfacePriority Priorities = 2; +} + +message LinuxHugepageLimit { + // Pagesize is the hugepage size + string Pagesize = 1; + + // Limit is the limit of "hugepagesize" hugetlb usage + uint64 Limit = 2; +} + +message LinuxInterfacePriority { + // Name is the name of the network interface + string Name = 1; + + // Priority for the interface + uint32 Priority = 2; +} + +message LinuxSeccomp { + string DefaultAction = 1; + repeated string Architectures = 2; + repeated string Flags = 3; + repeated LinuxSyscall Syscalls = 4; +} + +message LinuxSeccompArg { + uint64 Index = 1; + uint64 Value = 2; + uint64 ValueTwo = 3; + string Op = 4; +} + +message LinuxSyscall { + repeated string Names = 1; + string Action = 2; + oneof ErrnoRet { + uint32 errnoret = 3; + } + repeated LinuxSeccompArg Args = 4; +} + +message LinuxIntelRdt { + // The schema for L3 cache id and capacity bitmask (CBM) + // Format: "L3:=;=;..." + string L3CacheSchema = 1; +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.pb.go b/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.pb.go new file mode 100644 index 000000000..c2c849ddf --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.pb.go @@ -0,0 +1,570 @@ +// +// Copyright 2018 Intel Corporation. +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11-devel +// protoc v4.25.3 +// source: types.proto + +package agentpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IPFamily int32 + +const ( + IPFamily_v4 IPFamily = 0 + IPFamily_v6 IPFamily = 1 +) + +// Enum value maps for IPFamily. +var ( + IPFamily_name = map[int32]string{ + 0: "v4", + 1: "v6", + } + IPFamily_value = map[string]int32{ + "v4": 0, + "v6": 1, + } +) + +func (x IPFamily) Enum() *IPFamily { + p := new(IPFamily) + *p = x + return p +} + +func (x IPFamily) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (IPFamily) Descriptor() protoreflect.EnumDescriptor { + return file_types_proto_enumTypes[0].Descriptor() +} + +func (IPFamily) Type() protoreflect.EnumType { + return &file_types_proto_enumTypes[0] +} + +func (x IPFamily) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use IPFamily.Descriptor instead. +func (IPFamily) EnumDescriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{0} +} + +// FSGroupChangePolicy defines the policy for applying group id ownership change on a mounted volume. +type FSGroupChangePolicy int32 + +const ( + // Always indicates that the volume ownership will always be changed. + FSGroupChangePolicy_Always FSGroupChangePolicy = 0 + // OnRootMismatch indicates that the volume ownership will be changed only + // when the ownership of the root directory does not match with the expected group id for the volume. + FSGroupChangePolicy_OnRootMismatch FSGroupChangePolicy = 1 +) + +// Enum value maps for FSGroupChangePolicy. +var ( + FSGroupChangePolicy_name = map[int32]string{ + 0: "Always", + 1: "OnRootMismatch", + } + FSGroupChangePolicy_value = map[string]int32{ + "Always": 0, + "OnRootMismatch": 1, + } +) + +func (x FSGroupChangePolicy) Enum() *FSGroupChangePolicy { + p := new(FSGroupChangePolicy) + *p = x + return p +} + +func (x FSGroupChangePolicy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FSGroupChangePolicy) Descriptor() protoreflect.EnumDescriptor { + return file_types_proto_enumTypes[1].Descriptor() +} + +func (FSGroupChangePolicy) Type() protoreflect.EnumType { + return &file_types_proto_enumTypes[1] +} + +func (x FSGroupChangePolicy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FSGroupChangePolicy.Descriptor instead. +func (FSGroupChangePolicy) EnumDescriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{1} +} + +type IPAddress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Family IPFamily `protobuf:"varint,1,opt,name=family,proto3,enum=types.IPFamily" json:"family,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Mask string `protobuf:"bytes,3,opt,name=mask,proto3" json:"mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IPAddress) Reset() { + *x = IPAddress{} + mi := &file_types_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IPAddress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IPAddress) ProtoMessage() {} + +func (x *IPAddress) ProtoReflect() protoreflect.Message { + mi := &file_types_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IPAddress.ProtoReflect.Descriptor instead. +func (*IPAddress) Descriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{0} +} + +func (x *IPAddress) GetFamily() IPFamily { + if x != nil { + return x.Family + } + return IPFamily_v4 +} + +func (x *IPAddress) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *IPAddress) GetMask() string { + if x != nil { + return x.Mask + } + return "" +} + +type Interface struct { + state protoimpl.MessageState `protogen:"open.v1"` + Device string `protobuf:"bytes,1,opt,name=device,proto3" json:"device,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + IPAddresses []*IPAddress `protobuf:"bytes,3,rep,name=IPAddresses,proto3" json:"IPAddresses,omitempty"` + Mtu uint64 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` + HwAddr string `protobuf:"bytes,5,opt,name=hwAddr,proto3" json:"hwAddr,omitempty"` + // Path for the device (see the pci::Path (Rust) and types.PciPath + // (Go) or ccw::Device (Rust) and types.CcwDevice (Go) types for + // format details, depending on architecture) + DevicePath string `protobuf:"bytes,6,opt,name=devicePath,proto3" json:"devicePath,omitempty"` + // Type defines the type of interface described by this structure. + // The expected values are the one that are defined by the netlink + // library, regarding each type of link. Here is a non exhaustive + // list: "veth", "macvtap", "vlan", "macvlan", "tap", ... + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` + RawFlags uint32 `protobuf:"varint,8,opt,name=raw_flags,json=rawFlags,proto3" json:"raw_flags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Interface) Reset() { + *x = Interface{} + mi := &file_types_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Interface) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Interface) ProtoMessage() {} + +func (x *Interface) ProtoReflect() protoreflect.Message { + mi := &file_types_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Interface.ProtoReflect.Descriptor instead. +func (*Interface) Descriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{1} +} + +func (x *Interface) GetDevice() string { + if x != nil { + return x.Device + } + return "" +} + +func (x *Interface) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Interface) GetIPAddresses() []*IPAddress { + if x != nil { + return x.IPAddresses + } + return nil +} + +func (x *Interface) GetMtu() uint64 { + if x != nil { + return x.Mtu + } + return 0 +} + +func (x *Interface) GetHwAddr() string { + if x != nil { + return x.HwAddr + } + return "" +} + +func (x *Interface) GetDevicePath() string { + if x != nil { + return x.DevicePath + } + return "" +} + +func (x *Interface) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Interface) GetRawFlags() uint32 { + if x != nil { + return x.RawFlags + } + return 0 +} + +type Route struct { + state protoimpl.MessageState `protogen:"open.v1"` + Dest string `protobuf:"bytes,1,opt,name=dest,proto3" json:"dest,omitempty"` + Gateway string `protobuf:"bytes,2,opt,name=gateway,proto3" json:"gateway,omitempty"` + Device string `protobuf:"bytes,3,opt,name=device,proto3" json:"device,omitempty"` + Source string `protobuf:"bytes,4,opt,name=source,proto3" json:"source,omitempty"` + Scope uint32 `protobuf:"varint,5,opt,name=scope,proto3" json:"scope,omitempty"` + Family IPFamily `protobuf:"varint,6,opt,name=family,proto3,enum=types.IPFamily" json:"family,omitempty"` + Flags uint32 `protobuf:"varint,7,opt,name=flags,proto3" json:"flags,omitempty"` + Mtu uint32 `protobuf:"varint,8,opt,name=mtu,proto3" json:"mtu,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Route) Reset() { + *x = Route{} + mi := &file_types_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Route) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Route) ProtoMessage() {} + +func (x *Route) ProtoReflect() protoreflect.Message { + mi := &file_types_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Route.ProtoReflect.Descriptor instead. +func (*Route) Descriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{2} +} + +func (x *Route) GetDest() string { + if x != nil { + return x.Dest + } + return "" +} + +func (x *Route) GetGateway() string { + if x != nil { + return x.Gateway + } + return "" +} + +func (x *Route) GetDevice() string { + if x != nil { + return x.Device + } + return "" +} + +func (x *Route) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Route) GetScope() uint32 { + if x != nil { + return x.Scope + } + return 0 +} + +func (x *Route) GetFamily() IPFamily { + if x != nil { + return x.Family + } + return IPFamily_v4 +} + +func (x *Route) GetFlags() uint32 { + if x != nil { + return x.Flags + } + return 0 +} + +func (x *Route) GetMtu() uint32 { + if x != nil { + return x.Mtu + } + return 0 +} + +type ARPNeighbor struct { + state protoimpl.MessageState `protogen:"open.v1"` + ToIPAddress *IPAddress `protobuf:"bytes,1,opt,name=toIPAddress,proto3" json:"toIPAddress,omitempty"` + Device string `protobuf:"bytes,2,opt,name=device,proto3" json:"device,omitempty"` + Lladdr string `protobuf:"bytes,3,opt,name=lladdr,proto3" json:"lladdr,omitempty"` + State int32 `protobuf:"varint,4,opt,name=state,proto3" json:"state,omitempty"` + Flags int32 `protobuf:"varint,5,opt,name=flags,proto3" json:"flags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ARPNeighbor) Reset() { + *x = ARPNeighbor{} + mi := &file_types_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ARPNeighbor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ARPNeighbor) ProtoMessage() {} + +func (x *ARPNeighbor) ProtoReflect() protoreflect.Message { + mi := &file_types_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ARPNeighbor.ProtoReflect.Descriptor instead. +func (*ARPNeighbor) Descriptor() ([]byte, []int) { + return file_types_proto_rawDescGZIP(), []int{3} +} + +func (x *ARPNeighbor) GetToIPAddress() *IPAddress { + if x != nil { + return x.ToIPAddress + } + return nil +} + +func (x *ARPNeighbor) GetDevice() string { + if x != nil { + return x.Device + } + return "" +} + +func (x *ARPNeighbor) GetLladdr() string { + if x != nil { + return x.Lladdr + } + return "" +} + +func (x *ARPNeighbor) GetState() int32 { + if x != nil { + return x.State + } + return 0 +} + +func (x *ARPNeighbor) GetFlags() int32 { + if x != nil { + return x.Flags + } + return 0 +} + +var File_types_proto protoreflect.FileDescriptor + +const file_types_proto_rawDesc = "" + + "\n" + + "\vtypes.proto\x12\x05types\"b\n" + + "\tIPAddress\x12'\n" + + "\x06family\x18\x01 \x01(\x0e2\x0f.types.IPFamilyR\x06family\x12\x18\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\x12\x12\n" + + "\x04mask\x18\x03 \x01(\tR\x04mask\"\xe6\x01\n" + + "\tInterface\x12\x16\n" + + "\x06device\x18\x01 \x01(\tR\x06device\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x122\n" + + "\vIPAddresses\x18\x03 \x03(\v2\x10.types.IPAddressR\vIPAddresses\x12\x10\n" + + "\x03mtu\x18\x04 \x01(\x04R\x03mtu\x12\x16\n" + + "\x06hwAddr\x18\x05 \x01(\tR\x06hwAddr\x12\x1e\n" + + "\n" + + "devicePath\x18\x06 \x01(\tR\n" + + "devicePath\x12\x12\n" + + "\x04type\x18\a \x01(\tR\x04type\x12\x1b\n" + + "\traw_flags\x18\b \x01(\rR\brawFlags\"\xcc\x01\n" + + "\x05Route\x12\x12\n" + + "\x04dest\x18\x01 \x01(\tR\x04dest\x12\x18\n" + + "\agateway\x18\x02 \x01(\tR\agateway\x12\x16\n" + + "\x06device\x18\x03 \x01(\tR\x06device\x12\x16\n" + + "\x06source\x18\x04 \x01(\tR\x06source\x12\x14\n" + + "\x05scope\x18\x05 \x01(\rR\x05scope\x12'\n" + + "\x06family\x18\x06 \x01(\x0e2\x0f.types.IPFamilyR\x06family\x12\x14\n" + + "\x05flags\x18\a \x01(\rR\x05flags\x12\x10\n" + + "\x03mtu\x18\b \x01(\rR\x03mtu\"\x9d\x01\n" + + "\vARPNeighbor\x122\n" + + "\vtoIPAddress\x18\x01 \x01(\v2\x10.types.IPAddressR\vtoIPAddress\x12\x16\n" + + "\x06device\x18\x02 \x01(\tR\x06device\x12\x16\n" + + "\x06lladdr\x18\x03 \x01(\tR\x06lladdr\x12\x14\n" + + "\x05state\x18\x04 \x01(\x05R\x05state\x12\x14\n" + + "\x05flags\x18\x05 \x01(\x05R\x05flags*\x1a\n" + + "\bIPFamily\x12\x06\n" + + "\x02v4\x10\x00\x12\x06\n" + + "\x02v6\x10\x01*5\n" + + "\x13FSGroupChangePolicy\x12\n" + + "\n" + + "\x06Always\x10\x00\x12\x12\n" + + "\x0eOnRootMismatch\x10\x01BVZTgithub.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata/agentpb;agentpbb\x06proto3" + +var ( + file_types_proto_rawDescOnce sync.Once + file_types_proto_rawDescData []byte +) + +func file_types_proto_rawDescGZIP() []byte { + file_types_proto_rawDescOnce.Do(func() { + file_types_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc))) + }) + return file_types_proto_rawDescData +} + +var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_types_proto_goTypes = []any{ + (IPFamily)(0), // 0: types.IPFamily + (FSGroupChangePolicy)(0), // 1: types.FSGroupChangePolicy + (*IPAddress)(nil), // 2: types.IPAddress + (*Interface)(nil), // 3: types.Interface + (*Route)(nil), // 4: types.Route + (*ARPNeighbor)(nil), // 5: types.ARPNeighbor +} +var file_types_proto_depIdxs = []int32{ + 0, // 0: types.IPAddress.family:type_name -> types.IPFamily + 2, // 1: types.Interface.IPAddresses:type_name -> types.IPAddress + 0, // 2: types.Route.family:type_name -> types.IPFamily + 2, // 3: types.ARPNeighbor.toIPAddress:type_name -> types.IPAddress + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_types_proto_init() } +func file_types_proto_init() { + if File_types_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_types_proto_rawDesc), len(file_types_proto_rawDesc)), + NumEnums: 2, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_types_proto_goTypes, + DependencyIndexes: file_types_proto_depIdxs, + EnumInfos: file_types_proto_enumTypes, + MessageInfos: file_types_proto_msgTypes, + }.Build() + File_types_proto = out.File + file_types_proto_goTypes = nil + file_types_proto_depIdxs = nil +} diff --git a/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.proto b/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.proto new file mode 100644 index 000000000..1d10b1845 --- /dev/null +++ b/cmd/ateom-microvm/internal/third_party/kata/agentpb/types.proto @@ -0,0 +1,71 @@ +// +// Copyright 2018 Intel Corporation. +// Copyright (c) 2019-2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +syntax = "proto3"; + +option go_package = "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb;agentpb"; + +package types; + +enum IPFamily { + v4 = 0; + v6 = 1; +} + +// FSGroupChangePolicy defines the policy for applying group id ownership change on a mounted volume. +enum FSGroupChangePolicy { + // Always indicates that the volume ownership will always be changed. + Always = 0; + // OnRootMismatch indicates that the volume ownership will be changed only + // when the ownership of the root directory does not match with the expected group id for the volume. + OnRootMismatch = 1; +} + +message IPAddress { + IPFamily family = 1; + string address = 2; + string mask = 3; +} + +message Interface { + string device = 1; + string name = 2; + repeated IPAddress IPAddresses = 3; + uint64 mtu = 4; + string hwAddr = 5; + + // Path for the device (see the pci::Path (Rust) and types.PciPath + // (Go) or ccw::Device (Rust) and types.CcwDevice (Go) types for + // format details, depending on architecture) + string devicePath = 6; + + // Type defines the type of interface described by this structure. + // The expected values are the one that are defined by the netlink + // library, regarding each type of link. Here is a non exhaustive + // list: "veth", "macvtap", "vlan", "macvlan", "tap", ... + string type = 7; + uint32 raw_flags = 8; +} + +message Route { + string dest = 1; + string gateway = 2; + string device = 3; + string source = 4; + uint32 scope = 5; + IPFamily family = 6; + uint32 flags = 7; + uint32 mtu = 8; +} + +message ARPNeighbor { + IPAddress toIPAddress = 1; + string device = 2; + string lladdr = 3; + int32 state = 4; + int32 flags = 5; +} diff --git a/cmd/ateom-microvm/main.go b/cmd/ateom-microvm/main.go new file mode 100644 index 000000000..1bb77cfd4 --- /dev/null +++ b/cmd/ateom-microvm/main.go @@ -0,0 +1,227 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Command ateom-microvm is the kata + cloud-hypervisor micro-VM +// implementation of the ateompb.Ateom service, a peer to cmd/ateom-gvisor. +// +// It runs a substrate actor as a cloud-hypervisor micro-VM (launched via the +// kata guest model) and supports full suspend/resume by driving CH's native +// snapshot/restore underneath (see internal/ch). +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "net" + "os" + "strings" + "sync" + + "cloud.google.com/go/compute/metadata" + "github.com/agent-substrate/substrate/internal/actorlog" + "github.com/agent-substrate/substrate/internal/ateinterceptors" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/agent-substrate/substrate/internal/serverboot" + "github.com/agent-substrate/substrate/internal/version" + "github.com/hashicorp/go-reap" + "github.com/vishvananda/netns" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "golang.org/x/sys/unix" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var ( + podUID = flag.String("pod-uid", "", "The UID of the current pod") + chBinary = flag.String("cloud-hypervisor-binary", "cloud-hypervisor", "Path to the cloud-hypervisor binary (used to relaunch on restore).") + kataConfig = flag.String("kata-config", "", "Path to a kata configuration.toml (passed to the shim as KATA_CONF_FILE). Empty uses kata's default. atelet generates one pointing at runtime-fetched assets.") + kataDebug = flag.Bool("kata-debug", false, "Verbose kata-agent debugging: raise the guest agent log level and forward the guest console (incl. agent logs) into the pod logs.") + showVersion = flag.Bool("version", false, "Print version and exit.") + + // reapLock guards subprocess exec against the child reaper: ateom-microvm + // spawns the cloud-hypervisor process under it. + reapLock sync.RWMutex +) + +func main() { + flag.Parse() + if *showVersion { + fmt.Println(version.String()) + return + } + ctx := context.Background() + + if err := do(ctx); err != nil { + slog.ErrorContext(ctx, "Error while executing", slog.Any("err", err)) + os.Exit(1) + } +} + +func do(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Share one synchronized writer between the runtime logger and the actor-log + // forwarder (created below) so the two log streams to the pod's stdout don't + // interleave-corrupt each other's lines. + logWriter := actorlog.NewSyncedWriter(os.Stdout) + serverboot.InitLoggerWithWriter(logWriter) + slog.InfoContext(ctx, "ateom-microvm booting", slog.String("version", version.String())) + + tp, err := serverboot.InitTracing(ctx, serverboot.TracingOptions{ + ServiceName: "ateom-microvm", + Sampler: sdktrace.ParentBased(sdktrace.NeverSample()), + }) + if err != nil { + serverboot.Fatal(ctx, "Failed to initialize tracing", err) + } + defer serverboot.ShutdownProvider("TracerProvider", tp.Shutdown) + + // Create ateom dir. + ateomDir := ateompath.AteomPath(*podUID) + if err := os.MkdirAll(ateomDir, 0o700); err != nil { + return fmt.Errorf("in os.MkdirAll(%q): %w", ateomDir, err) + } + + // Reap children reparented to us (cloud-hypervisor), guarded so our own + // exec.Cmd calls can take the wait. + go reap.ReapChildren(nil, nil, nil, &reapLock) + slog.InfoContext(ctx, "Child process reaper launched") + + // kata's virtio-fs sharing depends on mount propagation: it slave-binds + // .../shared (served by virtiofsd) from .../mounts and expects the later + // per-container rootfs bind under mounts/ to propagate across. That only + // works if the underlying mount is SHARED. On a host systemd makes / + // rshared, but a container rootfs is rprivate (runc default), so the + // propagation silently never happens: the guest sees an empty rootfs and + // createContainer fails ENOENT. Self-bind /run/kata-containers and mark it + // rshared so kata's propagation chain works inside the pod. + if err := ensureSharedPropagation(ctx, "/run/kata-containers"); err != nil { + return fmt.Errorf("while making /run/kata-containers a shared mount: %w", err) + } + + // Clean up any old socket. + sockPath := ateompath.AteomSocketPath(*podUID) + if err := os.RemoveAll(sockPath); err != nil { + return fmt.Errorf("while removing %q: %w", sockPath, err) + } + + lis, err := net.Listen("unix", sockPath) + if err != nil { + return fmt.Errorf("while opening unix socket: %w", err) + } + + // Networking: create a named interior netns; each activation builds a fresh + // veth pair into it (see net.go) and points kata at it. + interiorNetNS, err := createNetNSWithoutSwitching(ateompath.AteomNetNSName(*podUID)) + if err != nil { + return fmt.Errorf("while creating interior netns: %w", err) + } + + // Forward the actor container's stdout/stderr to the worker pod's stdout as + // JSON with ate.dev/* labels (logging parity with ateom-gvisor). It shares + // logWriter with the runtime logger so the two streams to os.Stdout are + // serialized through one SyncedWriter and never interleave-corrupt lines. + actorLogger := actorlog.NewActorLogger(logWriter, metadata.OnGCE()) + + svr := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.UnaryInterceptor(ateinterceptors.ServerUnaryInterceptor), + ) + ateompb.RegisterAteomServer(svr, NewService(*podUID, *chBinary, *kataConfig, *kataDebug, interiorNetNS, actorLogger)) + reflection.Register(svr) + + slog.InfoContext(ctx, "ateom-microvm serving", slog.String("socket", sockPath)) + if err := svr.Serve(lis); err != nil { + return fmt.Errorf("while serving: %w", err) + } + return nil +} + +// ensureSharedPropagation makes path a mount point with rshared propagation +// (self-bind + MS_SHARED|MS_REC), so mounts created beneath it propagate to +// slave binds (kata's mounts/ -> shared/ chain). Idempotent: skips if path is +// already a shared mount point. +func ensureSharedPropagation(ctx context.Context, path string) error { + if err := os.MkdirAll(path, 0o750); err != nil { + return fmt.Errorf("creating %q: %w", path, err) + } + if b, err := os.ReadFile("/proc/self/mountinfo"); err == nil { + for _, line := range strings.Split(string(b), "\n") { + // mountinfo: ID parentID major:minor root mountpoint opts optional... - fstype ... + fields := strings.Fields(line) + if len(fields) >= 7 && fields[4] == path && strings.Contains(line, "shared:") { + slog.InfoContext(ctx, "Mount already shared", slog.String("path", path)) + return nil + } + } + } + if err := unix.Mount(path, path, "", unix.MS_BIND, ""); err != nil { + return fmt.Errorf("self-binding %q: %w", path, err) + } + if err := unix.Mount("", path, "", unix.MS_SHARED|unix.MS_REC, ""); err != nil { + return fmt.Errorf("marking %q rshared: %w", path, err) + } + slog.InfoContext(ctx, "Made mount rshared for kata virtio-fs propagation", slog.String("path", path)) + return nil +} + +// AteomService is the cloud-hypervisor implementation of ateompb.AteomServer. +type AteomService struct { + ateompb.UnimplementedAteomServer + + // lock serializes RPCs; like ateom-gvisor, the run/checkpoint/restore + // lifecycle is not safe to drive concurrently. + lock sync.Mutex + + podUID string + chBinary string + kataConfig string + kataDebug bool + + // interiorNetNS hosts the per-activation actor veth peer (see net.go); + // kata is pointed at it. + interiorNetNS netns.NsHandle + + // actorLogger forwards the actor container's stdout/stderr to the worker pod's + // stdout as ate.dev/*-labeled JSON and emits actor lifecycle events (parity + // with ateom-gvisor). + actorLogger *actorlog.ActorLogger + + // running maps actor id -> the live micro-VM, kept so CheckpointWorkload can + // pause+snapshot+teardown the same sandbox (and RestoreWorkload can track the + // CH it relaunched). + running map[string]*runningActor +} + +var _ ateompb.AteomServer = (*AteomService)(nil) + +// NewService creates a new AteomService. +func NewService(podUID, chBinary, kataConfig string, kataDebug bool, interiorNetNS netns.NsHandle, actorLogger *actorlog.ActorLogger) *AteomService { + return &AteomService{ + podUID: podUID, + chBinary: chBinary, + kataConfig: kataConfig, + kataDebug: kataDebug, + interiorNetNS: interiorNetNS, + actorLogger: actorLogger, + running: map[string]*runningActor{}, + } +} diff --git a/cmd/ateom-microvm/main_unsupported.go b/cmd/ateom-microvm/main_unsupported.go new file mode 100644 index 000000000..a0416ac57 --- /dev/null +++ b/cmd/ateom-microvm/main_unsupported.go @@ -0,0 +1,27 @@ +//go:build !linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stderr, "ateom-microvm is only supported on Linux") + os.Exit(1) +} diff --git a/cmd/ateom-microvm/net.go b/cmd/ateom-microvm/net.go new file mode 100644 index 000000000..e74db2bd3 --- /dev/null +++ b/cmd/ateom-microvm/net.go @@ -0,0 +1,617 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 + +// Actor networking mirrors cmd/ateom-gvisor's veth model: a fresh +// point-to-point veth pair per activation, with the worker side (ateom0, +// 169.254.17.1/30) staying in the pod netns next to the pod's real eth0, and +// the peer moved into the interior netns, renamed eth0, and given the stable +// actor address 169.254.17.2/30. nftables rules in the pod netns masquerade +// actor egress behind the pod IP and DNAT inbound pod-IP:80 to the actor. +// +// kata consumes the interior netns exactly like a CNI-provisioned container +// netns: its tcfilter network model builds a tap cross-connected to eth0 (the +// veth peer) and gives the guest eth0's address. Because that address is a +// CONSTANT, a restored guest's frozen network config stays valid on any pod — +// no in-guest reconfiguration needed. +// +// (Copied with light adaptation from cmd/ateom-gvisor; expected to be +// de-duplicated into a shared package later.) + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "os" + "runtime" + + "github.com/google/nftables" + "github.com/google/nftables/binaryutil" + "github.com/google/nftables/expr" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" + "golang.org/x/sys/unix" + + "github.com/agent-substrate/substrate/internal/serverboot" +) + +const ( + hostVethName = "ateom0" + actorVethName = "eth0" + actorVethTempName = "ateom1" + hostVethCIDR = "169.254.17.1/30" + actorVethCIDR = "169.254.17.2/30" + actorVethGateway = "169.254.17.1" + actorVethIP = "169.254.17.2" + actorNftTableName = "ateom_actor" + + // hostVethMAC is deliberately FIXED (locally administered), unlike + // ateom-gvisor where the kernel's random veth MAC is fine. A CH snapshot + // freezes the guest kernel's ARP cache, including the entry for the + // gateway 169.254.17.1; restoring against a new veth pair with a random + // MAC would blackhole guest egress until that entry expires. A constant + // gateway MAC keeps the frozen entry valid on every pod. + hostVethMAC = "02:a8:1e:00:00:01" + + // actorGuestMAC is the FIXED MAC for the guest's eth0 (the CH virtio-net) on + // the ateom-owned-boot path. Fixed for the same reason as hostVethMAC: a cold + // boot freezes this MAC into the guest+snapshot, and restore re-adds the + // virtio-net under the same MAC (SnapshotNetDevices reads it back), so the + // guest's frozen interface config stays valid across pods. Distinct from the + // gateway MAC (…:01). + actorGuestMAC = "02:a8:1e:00:00:02" + + // actorVethSubnet is the point-to-point /30 the actor veth lives on; the guest + // needs the connected (scope-link) route to it so the gateway is reachable. + actorVethSubnet = "169.254.17.0/30" +) + +// Parsed forms of the fixed network constants above, cooked once at package init +// (a malformed constant is a programmer error, so these panic). Callers use them +// directly instead of re-parsing on every activation. +var ( + hostVethAddr = mustParseAddr(hostVethCIDR) + hostVethHWAddr = mustParseMAC(hostVethMAC) + actorVethAddr = mustParseAddr(actorVethCIDR) + actorVethGwIP = mustParseIP(actorVethGateway) +) + +func mustParseAddr(cidr string) *netlink.Addr { + a, err := parseAddr(cidr) + if err != nil { + panic(fmt.Sprintf("parsing constant CIDR %q: %v", cidr, err)) + } + return a +} + +func mustParseMAC(s string) net.HardwareAddr { + m, err := net.ParseMAC(s) + if err != nil { + panic(fmt.Sprintf("parsing constant MAC %q: %v", s, err)) + } + return m +} + +func mustParseIP(s string) net.IP { + ip := net.ParseIP(s).To4() + if ip == nil { + panic(fmt.Sprintf("parsing constant IPv4 %q", s)) + } + return ip +} + +// setupActorNetwork builds a fresh point-to-point network between the worker +// pod netns and the kata interior netns (see the package comment). Idempotent +// via cleanup-before-setup; also sweeps stale kata taps out of the interior +// netns so the sandbox always builds on a clean slate. +func (s *AteomService) setupActorNetwork(ctx context.Context) (retErr error) { + s.cleanupActorNetworkOrExit(ctx, "Failed to clean up stale actor network before setup") + defer func() { + if retErr != nil { + s.cleanupActorNetworkOrExit(ctx, "Failed to clean up partially configured actor network") + } + }() + + podIP, err := podIPv4() + if err != nil { + return fmt.Errorf("while resolving pod IPv4 address: %w", err) + } + + // Sweep leftover links (kata taps from torn-down runs, restore taps) from + // the persistent interior netns before the new veth peer arrives. + if err := netNSDo(ctx, s.interiorNetNS, func(ctx context.Context) error { + links, err := netlink.LinkList() + if err != nil { + return fmt.Errorf("while listing interior netns links: %w", err) + } + for _, l := range links { + if l.Attrs().Name == "lo" { + continue + } + if err := netlink.LinkDel(l); err != nil { + slog.WarnContext(ctx, "Failed to delete leftover interior link", slog.String("link", l.Attrs().Name), slog.Any("err", err)) + } + } + return nil + }); err != nil { + return err + } + + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: hostVethName, + HardwareAddr: hostVethHWAddr, + }, + PeerName: actorVethTempName, + } + if err := netlink.LinkAdd(veth); err != nil { + return fmt.Errorf("while creating actor veth pair: %w", err) + } + + hostLink, err := netlink.LinkByName(hostVethName) + if err != nil { + return fmt.Errorf("while getting host veth: %w", err) + } + if err := netlink.AddrReplace(hostLink, hostVethAddr); err != nil { + return fmt.Errorf("while assigning host veth address: %w", err) + } + if err := netlink.LinkSetUp(hostLink); err != nil { + return fmt.Errorf("while bringing up host veth: %w", err) + } + + actorLink, err := netlink.LinkByName(actorVethTempName) + if err != nil { + return fmt.Errorf("while getting actor veth peer: %w", err) + } + if err := netlink.LinkSetNsFd(actorLink, int(s.interiorNetNS)); err != nil { + return fmt.Errorf("while moving actor veth peer into interior netns: %w", err) + } + + if err := netNSDo(ctx, s.interiorNetNS, configureActorVeth); err != nil { + return fmt.Errorf("while configuring actor veth in interior netns: %w", err) + } + + if err := enableIPv4Forwarding(); err != nil { + return err + } + if err := installActorNftablesRules(podIP); err != nil { + return err + } + + return nil +} + +func configureActorVeth(ctx context.Context) error { + // Run inside the interior netns after setupActorNetwork moves the veth peer + // there. kata reads link names, addresses, and routes from this namespace + // when the sandbox starts, so the peer is renamed to eth0 and configured + // like a normal container interface; the guest is configured identically by + // the kata agent. + loLink, err := netlink.LinkByName("lo") + if err != nil { + return fmt.Errorf("while acquiring lo in interior netns: %w", err) + } + if err := netlink.LinkSetUp(loLink); err != nil { + return fmt.Errorf("while bringing up lo in interior netns: %w", err) + } + + actorLink, err := netlink.LinkByName(actorVethTempName) + if err != nil { + return fmt.Errorf("while acquiring actor veth in interior netns: %w", err) + } + if err := netlink.LinkSetName(actorLink, actorVethName); err != nil { + return fmt.Errorf("while renaming actor veth to %q: %w", actorVethName, err) + } + actorLink, err = netlink.LinkByName(actorVethName) + if err != nil { + return fmt.Errorf("while reacquiring actor veth in interior netns: %w", err) + } + + if err := netlink.AddrReplace(actorLink, actorVethAddr); err != nil { + return fmt.Errorf("while assigning actor veth address: %w", err) + } + if err := netlink.LinkSetUp(actorLink); err != nil { + return fmt.Errorf("while bringing up actor veth: %w", err) + } + + if err := netlink.RouteReplace(&netlink.Route{ + LinkIndex: actorLink.Attrs().Index, + Gw: actorVethGwIP, + }); err != nil { + return fmt.Errorf("while installing actor default route: %w", err) + } + + return nil +} + +// cleanupActorNetwork removes all per-activation network state owned by ateom. +// Intentionally idempotent: runs before setup, after checkpoint, and from +// setup-failure cleanup. +func (s *AteomService) cleanupActorNetwork(ctx context.Context) error { + var cleanupErr error + if err := removeActorNftablesRules(); err != nil { + cleanupErr = errors.Join(cleanupErr, fmt.Errorf("while removing actor nftables rules: %w", err)) + slog.WarnContext(ctx, "Failed to remove actor nftables rules; continuing actor netns cleanup", slog.Any("err", err)) + } + + if link, err := netlink.LinkByName(hostVethName); err == nil { + if err := netlink.LinkDel(link); err != nil { + cleanupErr = errors.Join(cleanupErr, fmt.Errorf("while deleting host veth: %w", err)) + slog.WarnContext(ctx, "Failed to delete host veth; continuing actor netns cleanup", slog.Any("err", err)) + } + } else if _, ok := err.(netlink.LinkNotFoundError); !ok { + cleanupErr = errors.Join(cleanupErr, fmt.Errorf("while looking up host veth: %w", err)) + slog.WarnContext(ctx, "Failed to look up host veth; continuing actor netns cleanup", slog.Any("err", err)) + } + + if err := netNSDo(ctx, s.interiorNetNS, func(_ context.Context) error { + for _, name := range []string{actorVethName, actorVethTempName} { + link, err := netlink.LinkByName(name) + if err == nil { + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("while deleting interior veth %q: %w", name, err) + } + continue + } + if _, ok := err.(netlink.LinkNotFoundError); !ok { + return fmt.Errorf("while looking up interior veth %q: %w", name, err) + } + } + return nil + }); err != nil { + cleanupErr = errors.Join(cleanupErr, fmt.Errorf("while cleaning interior netns links: %w", err)) + } + + return cleanupErr +} + +func (s *AteomService) cleanupActorNetworkOrExit(ctx context.Context, msg string) { + if err := s.cleanupActorNetwork(ctx); err != nil { + serverboot.Fatal(ctx, msg, err) + } +} + +// podIPv4 resolves the worker pod IPv4 address from the pod namespace's real +// eth0 (which stays in the pod namespace in the veth model). +func podIPv4() (net.IP, error) { + eth0Link, err := netlink.LinkByName("eth0") + if err != nil { + return nil, fmt.Errorf("while getting pod eth0: %w", err) + } + addrs, err := netlink.AddrList(eth0Link, netlink.FAMILY_V4) + if err != nil { + return nil, fmt.Errorf("while listing pod eth0 addresses: %w", err) + } + for _, addr := range addrs { + if addr.IP == nil { + continue + } + if ip := addr.IP.To4(); ip != nil { + return ip, nil + } + } + return nil, fmt.Errorf("pod eth0 has no IPv4 address") +} + +func parseAddr(cidr string) (*netlink.Addr, error) { + addr, err := netlink.ParseAddr(cidr) + if err != nil { + return nil, fmt.Errorf("while parsing address %q: %w", cidr, err) + } + return addr, nil +} + +func enableIPv4Forwarding() error { + // Actor packets enter the worker pod via the host-side veth and leave + // through the pod's eth0; the kernel will not route between them otherwise. + // Open the existing sysctl O_WRONLY (not os.WriteFile, which would create a + // regular file if the knob were missing) so an absent knob is a clear error. + f, err := os.OpenFile("/proc/sys/net/ipv4/ip_forward", os.O_WRONLY, 0) + if err != nil { + return fmt.Errorf("while opening ip_forward sysctl: %w", err) + } + defer f.Close() + if _, err := f.Write([]byte("1\n")); err != nil { + return fmt.Errorf("while enabling IPv4 forwarding in worker pod netns: %w", err) + } + return nil +} + +func installActorNftablesRules(podIP net.IP) error { + // Dedicated ateom-owned IPv4 table (cheap cleanup, no CNI chain mutation): + // * postrouting: masquerade actor egress (169.254.17.2) behind the pod IP. + // * prerouting: DNAT pod-IP:80/tcp to the actor veth IP. + // * forward: accept forwarded packets between the actor veth and pod eth0. + // Mirrors cmd/ateom-gvisor (same compatibility-bridge caveats and TODOs). + if err := removeActorNftablesRules(); err != nil { + return err + } + + c := &nftables.Conn{} + table := &nftables.Table{ + Family: nftables.TableFamilyIPv4, + Name: actorNftTableName, + } + c.AddTable(table) + + prerouting := c.AddChain(&nftables.Chain{ + Name: "prerouting", + Table: table, + Type: nftables.ChainTypeNAT, + Hooknum: nftables.ChainHookPrerouting, + Priority: nftables.ChainPriorityNATDest, + }) + preroutingExprs := append(ipDestinationEqual(podIP.String()), tcpDestinationPortEqual(80)...) + preroutingExprs = append(preroutingExprs, + &expr.Immediate{ + Register: 1, + Data: net.ParseIP(actorVethIP).To4(), + }, + &expr.Immediate{ + Register: 2, + Data: binaryutil.BigEndian.PutUint16(80), + }, + &expr.NAT{ + Type: expr.NATTypeDestNAT, + Family: unix.NFPROTO_IPV4, + RegAddrMin: 1, + RegProtoMin: 2, + }, + ) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: prerouting, + Exprs: preroutingExprs, + }) + + postrouting := c.AddChain(&nftables.Chain{ + Name: "postrouting", + Table: table, + Type: nftables.ChainTypeNAT, + Hooknum: nftables.ChainHookPostrouting, + Priority: nftables.ChainPriorityNATSource, + }) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: postrouting, + Exprs: append(ipSourceEqual(actorVethIP), &expr.Masq{}), + }) + + acceptPolicy := nftables.ChainPolicyAccept + forward := c.AddChain(&nftables.Chain{ + Name: "forward", + Table: table, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityFilter, + Policy: &acceptPolicy, + }) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: forward, + Exprs: []expr.Any{ + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + }) + + if err := c.Flush(); err != nil { + return fmt.Errorf("while installing actor nftables rules: %w", err) + } + return nil +} + +func removeActorNftablesRules() error { + c := &nftables.Conn{} + tables, err := c.ListTablesOfFamily(nftables.TableFamilyIPv4) + if err != nil { + return fmt.Errorf("while listing nftables tables: %w", err) + } + for _, table := range tables { + if table.Name != actorNftTableName { + continue + } + c.DelTable(table) + if err := c.Flush(); err != nil { + return fmt.Errorf("while deleting actor nftables table: %w", err) + } + return nil + } + return nil +} + +func ipSourceEqual(ip string) []expr.Any { + return ipPayloadEqual(12, ip) +} + +func ipDestinationEqual(ip string) []expr.Any { + return ipPayloadEqual(16, ip) +} + +func ipPayloadEqual(offset uint32, ip string) []expr.Any { + return []expr.Any{ + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: offset, + Len: 4, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: net.ParseIP(ip).To4(), + }, + } +} + +func tcpDestinationPortEqual(port uint16) []expr.Any { + return []expr.Any{ + &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 2, + Len: 2, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.BigEndian.PutUint16(port), + }, + } +} + +// createNetNSWithoutSwitching creates a named netns and returns its handle, +// restoring the caller's current netns before returning. +func createNetNSWithoutSwitching(name string) (netns.NsHandle, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + curNetNS, err := netns.Get() + if err != nil { + return -1, fmt.Errorf("while getting current netns: %w", err) + } + defer func() { + if err := netns.Set(curNetNS); err != nil { + panic(fmt.Sprintf("Failed to restore original netns: %v", err)) + } + }() + + interiorNetNS, err := netns.NewNamed(name) + if err != nil { + return -1, fmt.Errorf("while creating interior network namespace: %w", err) + } + return interiorNetNS, nil +} + +// netNSDo runs do() with the OS thread switched into targetNS, then restores it. +func netNSDo(ctx context.Context, targetNS netns.NsHandle, do func(context.Context) error) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + curNetNS, err := netns.Get() + if err != nil { + return fmt.Errorf("while getting current netns: %w", err) + } + defer func() { + if err := netns.Set(curNetNS); err != nil { + panic(fmt.Sprintf("Failed to restore original netns: %v", err)) + } + }() + + if err := netns.Set(targetNS); err != nil { + return fmt.Errorf("setting target netns: %w", err) + } + if err := do(ctx); err != nil { + return fmt.Errorf("while executing function in target netns: %w", err) + } + return nil +} + +// setupRestoreTap recreates, in the interior netns, the tap + TC-mirror wiring +// kata's tcfilter network model builds at boot: a tap device cross-connected to +// eth0 (the actor veth peer) with mirred-redirect ingress filters in both +// directions. Returns the open tap FDs (one per queue pair) for +// cloud-hypervisor to adopt via vm.restore net_fds (the snapshot's virtio-net +// device is fd-backed, so CH requires fresh FDs on restore). Call after +// setupActorNetwork. +func (s *AteomService) setupRestoreTap(ctx context.Context, name string, queuePairs int) ([]*os.File, error) { + var fds []*os.File + err := netNSDo(ctx, s.interiorNetNS, func(ctx context.Context) error { + eth0, err := netlink.LinkByName(actorVethName) + if err != nil { + return fmt.Errorf("acquiring actor veth in interior netns: %w", err) + } + if old, lerr := netlink.LinkByName(name); lerr == nil { + _ = netlink.LinkDel(old) + } + flags := netlink.TUNTAP_NO_PI | netlink.TUNTAP_VNET_HDR + if queuePairs > 1 { + flags |= netlink.TUNTAP_MULTI_QUEUE + } + tap := &netlink.Tuntap{ + LinkAttrs: netlink.LinkAttrs{Name: name, MTU: eth0.Attrs().MTU}, + Mode: netlink.TUNTAP_MODE_TAP, + Flags: flags, + Queues: queuePairs, + } + if err := netlink.LinkAdd(tap); err != nil { + return fmt.Errorf("creating tap %q: %w", name, err) + } + fds = tap.Fds + if err := netlink.LinkSetUp(tap); err != nil { + return fmt.Errorf("bringing up tap %q: %w", name, err) + } + // Cross-connect: everything arriving on the veth peer redirects out the + // tap and vice versa (kata's TCFilterModel: ingress qdisc + match-all u32 + // with a mirred egress-redirect action, here via U32.RedirIndex). + for _, pair := range [][2]netlink.Link{{eth0, tap}, {tap, eth0}} { + qdisc := &netlink.Ingress{QdiscAttrs: netlink.QdiscAttrs{ + LinkIndex: pair[0].Attrs().Index, + Parent: netlink.HANDLE_INGRESS, + Handle: netlink.MakeHandle(0xffff, 0), + }} + if err := netlink.QdiscReplace(qdisc); err != nil { + return fmt.Errorf("adding ingress qdisc to %q: %w", pair[0].Attrs().Name, err) + } + filter := &netlink.U32{ + FilterAttrs: netlink.FilterAttrs{ + LinkIndex: pair[0].Attrs().Index, + Parent: netlink.MakeHandle(0xffff, 0), + Priority: 1, + Protocol: unix.ETH_P_ALL, + }, + ClassId: netlink.MakeHandle(1, 1), + RedirIndex: pair[1].Attrs().Index, + } + if err := netlink.FilterAdd(filter); err != nil { + return fmt.Errorf("adding mirred filter %s -> %s: %w", pair[0].Attrs().Name, pair[1].Attrs().Name, err) + } + } + return nil + }) + if err != nil { + for _, f := range fds { + _ = f.Close() + } + return nil, err + } + return fds, nil +} + +// actorVethMTU reads the MTU of the actor veth (eth0 in the interior netns) so the +// owned-boot path can configure the guest eth0 with a matching MTU via the agent +// (UpdateInterface). Defaults to 1500 if the link can't be read. +func (s *AteomService) actorVethMTU(ctx context.Context) int { + mtu := 1500 + _ = netNSDo(ctx, s.interiorNetNS, func(ctx context.Context) error { + if l, err := netlink.LinkByName(actorVethName); err == nil { + mtu = l.Attrs().MTU + } else { + slog.WarnContext(ctx, "Failed to read actor veth MTU; using default", + slog.String("link", actorVethName), slog.Int("default_mtu", mtu), slog.Any("err", err)) + } + return nil + }) + return mtu +} diff --git a/cmd/ateom-microvm/restore.go b/cmd/ateom-microvm/restore.go new file mode 100644 index 000000000..cb3271c10 --- /dev/null +++ b/cmd/ateom-microvm/restore.go @@ -0,0 +1,282 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// RestoreWorkload restores the actor on a (possibly different) pod by relaunching +// cloud-hypervisor directly from the downloaded snapshot and resuming. +// +// Contract with atelet: the memory-only snapshot dir (config.json + state.json + +// memory-ranges + base-id) has been downloaded to RestoreStateDir. +// +// There is NO virtiofsd and NO shared-dir to reconstruct — the rootfs is the +// writable /dev/vdb disk, which CH reopens from the path recorded in the snapshot +// config.json. Steps: rewrite the vsock socket path to this actor's VMDir, +// reset /dev/vdb to the golden disk template (or rebuild it from the OCI image), +// rebuild the tap (the snapshot's virtio-net is fd-backed → fresh net_fds), +// relaunch CH with --restore, and resume. Guest RAM (incl. the actor's in-memory +// state and frozen network config) comes back from the memory-only snapshot. +func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.RestoreWorkloadRequest) (resp *ateompb.RestoreWorkloadResponse, retErr error) { + s.lock.Lock() + defer s.lock.Unlock() + + ns := req.GetActorTemplateNamespace() + name := req.GetActorTemplateName() + id := req.GetActorId() + restoreDir := ateompath.RestoreStateDir(ns, name, id) + tStart := time.Now() + + s.actorLogger.EmitLifecycleLog("Actor restoring", id, name, ns) + + rr := s.resolveRuntime(req.GetRuntimeAssetPaths()) + kata.CleanupSandboxState(ctx, id) + + // Repoint the snapshot's vsock socket to this actor's VMDir (the disk + kernel + // paths are content-addressed/per-actor and already line up on the same node). + if err := rewriteSnapshotSocketPaths(restoreDir, id); err != nil { + return nil, fmt.Errorf("while rewriting snapshot socket paths: %w", err) + } + srcID := id + if b, rerr := os.ReadFile(filepath.Join(restoreDir, baseIDFile)); rerr == nil { + if v := strings.TrimSpace(string(b)); v != "" { + srcID = v + } + } + if err := os.MkdirAll(kata.VMDir(id), 0o700); err != nil { + return nil, fmt.Errorf("while creating VM dir: %w", err) + } + + // Recreate the /dev/vdb backing file the snapshot references (the actor dir), + // reset-to-golden. Two ways, both byte-consistent with the golden snapshot's + // guest ext4 cache: + // - same-node: a verbatim golden template (copyDiskFile) — guaranteed identical. + // - cross-node: rebuild from the OCI image atelet unpacked to the bundle at + // restore (mkfs.ext4 -d is LAYOUT-deterministic for identical inputs, so the + // data blocks land at the same offsets the guest cache expects; only the + // superblock UUID/timestamps differ, which are cached in RAM and not re-read). + // Either way the actor's prior rootfs writes are discarded (gVisor semantics). + containers := req.GetSpec().GetContainers() + if len(containers) != 1 { + return nil, status.Errorf(codes.Unimplemented, "ateom-microvm supports exactly one container, got %d", len(containers)) + } + actorDir := ateompath.ActorPath(ns, name, id) + diskPath := filepath.Join(actorDir, actorRootfsDiskName) + if tmpl := filepath.Join(actorDir, goldenRootfsDiskName); !fileMissing(tmpl) { + if err := copyDiskFile(ctx, tmpl, diskPath); err != nil { + return nil, fmt.Errorf("while resetting rootfs disk to golden (template): %w", err) + } + slog.InfoContext(ctx, "Reset actor rootfs disk to golden (template)", slog.String("id", id)) + } else { + bundleRootfs := filepath.Join(ateompath.OCIBundlePath(ns, name, id, containers[0].GetName()), "rootfs") + if err := kata.BuildExt4Image(ctx, bundleRootfs, diskPath); err != nil { + return nil, fmt.Errorf("while reconstructing rootfs disk from image: %w", err) + } + slog.InfoContext(ctx, "Reconstructed actor rootfs disk from image", slog.String("id", id)) + } + + // Repoint the snapshot config's writable /dev/vdb disk at THIS actor's + // reconstructed backing file. The golden snapshot recorded the golden actor's + // per-actor disk path, which is stale on any pod restoring a different actor + // (and absent on any node that never ran the golden) — unlike /dev/vda, the + // content-addressed kata image whose path is identical on every node. + if err := repointActorRootfsDisk(restoreDir, diskPath); err != nil { + return nil, fmt.Errorf("while repointing actor rootfs disk in snapshot config: %w", err) + } + + // Networking: rebuild the per-activation veth + tap; the snapshot's virtio-net + // is fd-backed, so CH needs fresh tap FDs (net_fds) on restore. + if err := s.setupActorNetwork(ctx); err != nil { + return nil, fmt.Errorf("while setting up actor network: %w", err) + } + defer func() { + if retErr != nil { + if cleanupErr := s.cleanupActorNetwork(ctx); cleanupErr != nil { + slog.WarnContext(ctx, "Failed to clean up actor network after Restore failure", slog.Any("err", cleanupErr)) + } + } + }() + netDevs, err := ch.SnapshotNetDevices(restoreDir) + if err != nil { + return nil, fmt.Errorf("while reading snapshot net devices: %w", err) + } + var restoredNets []ch.RestoredNet + var tapFiles []*os.File + defer func() { + for _, f := range tapFiles { + _ = f.Close() + } + }() + for i, nd := range netDevs { + files, terr := s.setupRestoreTap(ctx, fmt.Sprintf("tap%d_kata", i), nd.QueuePairs) + if terr != nil { + return nil, fmt.Errorf("while building restore tap for %s: %w", nd.ID, terr) + } + tapFiles = append(tapFiles, files...) + rn := ch.RestoredNet{ID: nd.ID} + for _, f := range files { + rn.FDs = append(rn.FDs, int(f.Fd())) + } + restoredNets = append(restoredNets, rn) + } + + // Relaunch CH and restore with the tap FDs attached (SCM_RIGHTS). CH reopens + // /dev/vda (image) + /dev/vdb (actor rootfs) from the snapshot config paths. + apiSocket := filepath.Join(kata.VMDir(id), "clh-api-restore.sock") + chCmd, client, err := ch.LaunchVMM(ctx, ch.LaunchVMMOptions{ + Binary: rr.chBinary, APISocket: apiSocket, Stdout: slogWriter{ctx}, Stderr: slogWriter{ctx}, + }) + if err != nil { + return nil, fmt.Errorf("while launching VMM for restore: %w", err) + } + defer func() { + if retErr != nil && chCmd.Process != nil { + _ = chCmd.Process.Kill() + } + }() + // OnDemand (userfaultfd) memory restore: ~75ms vs ~1.8s eager, and it keeps the + // memfd SPARSE so the next suspend isn't the eager-copy-densified full-RAM scan. + // CH's OnDemand snapshot alone would be INCOMPLETE (it writes only faulted pages, + // dropping the un-faulted ones it demand-pages from this source) — so + // CheckpointWorkload overlays CH's delta onto this source (restoreSourceDir) to + // rebuild a complete snapshot. CH demand-pages from restoreDir for the VM's whole + // lifetime, so it must persist until teardown (atelet keeps it until reset). + if err := client.RestoreWithNetFDs(ctx, restoreDir, restoredNets, "OnDemand"); err != nil { + return nil, fmt.Errorf("while restoring VM with net FDs: %w", err) + } + if err := client.Resume(ctx); err != nil { + return nil, fmt.Errorf("while resuming restored guest: %w", err) + } + + ra := &runningActor{chCmd: chCmd, apiSocket: apiSocket, baseID: srcID, restoreSourceDir: restoreDir} + + // Re-attach stdout/stderr forwarding: the restored guest's container + kata-agent + // are alive, so a fresh dial over this actor's vsock resumes ReadStdout/ReadStderr + // (same containerID==execID==id as the cold run). Best-effort — a failed dial must + // not fail the restore (the actor is already running); forwarding is just skipped. + vsockPath := kata.VsockSocketPath(id) + logAC, dialErr := dialAgentRetry(ctx, vsockPath, 15*time.Second) + if dialErr != nil { + slog.WarnContext(ctx, "post-restore agent dial failed; actor log forwarding disabled for this restore", + slog.String("id", id), slog.Any("err", dialErr)) + } else { + ra.logAgent = logAC + s.startActorLogForwarding(logAC, id, name, ns, containers[0].GetName()) + } + + s.running[id] = ra + s.actorLogger.EmitLifecycleLog("Actor restored", id, name, ns) + slog.InfoContext(ctx, "Actor restored (owned-boot, virtio-blk rootfs)", + slog.String("id", id), slog.Duration("total", time.Since(tStart))) + return &ateompb.RestoreWorkloadResponse{}, nil +} + +// rewriteSnapshotSocketPaths repoints the snapshot config.json's per-sandbox +// hybrid-vsock socket from the source actor's VMDir to the restoring actor's +// VMDir, so the socket we create is the one CH reopens. The kernel and /dev/vda +// kata image are content-addressed static files with identical paths on every +// node, so they need no rewrite; the writable /dev/vdb actor rootfs disk is +// per-actor and is repointed separately (see repointActorRootfsDisk). +func rewriteSnapshotSocketPaths(snapshotDir, id string) error { + cfgPath := filepath.Join(snapshotDir, "config.json") + b, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + var cfg map[string]any + if err := json.Unmarshal(b, &cfg); err != nil { + return fmt.Errorf("parsing %q: %w", cfgPath, err) + } + if vsock, ok := cfg["vsock"].(map[string]any); ok { + vsock["socket"] = kata.VsockSocketPath(id) + } + // The owned-boot path captures the guest serial console to a file under the + // source actor's VMDir (Serial{Mode:"File"}). On restore that path is stale + // (points at the golden/source pod's VMDir), so CH's CreateConsoleDevice fails + // (No such file or directory). Repoint it at this actor's VMDir. + if serial, ok := cfg["serial"].(map[string]any); ok { + if mode, _ := serial["mode"].(string); mode == "File" { + serial["file"] = filepath.Join(kata.VMDir(id), "serial.log") + } + } + out, err := json.Marshal(cfg) + if err != nil { + return err + } + if err := os.WriteFile(cfgPath, out, 0o600); err != nil { + return err + } + return nil +} + +// repointActorRootfsDisk rewrites the snapshot config.json so the writable +// /dev/vdb actor rootfs disk points at this actor's reconstructed backing file +// (diskPath). The actor rootfs disk lives under the actor's per-actor directory +// (keyed by actor id), so the golden snapshot's recorded path is the GOLDEN +// actor's — stale on any pod restoring a different actor, and absent on any node +// that never ran the golden. (This is the disk analogue of the serial.file +// repoint in rewriteSnapshotSocketPaths.) The disk is identified by basename so +// the read-only /dev/vda kata image (a content-addressed static file) is left +// untouched; it is an error if no actor rootfs disk is present to repoint. +func repointActorRootfsDisk(snapshotDir, diskPath string) error { + cfgPath := filepath.Join(snapshotDir, "config.json") + b, err := os.ReadFile(cfgPath) + if err != nil { + return err + } + var cfg map[string]any + if err := json.Unmarshal(b, &cfg); err != nil { + return fmt.Errorf("parsing %q: %w", cfgPath, err) + } + rewrote := false + if disks, ok := cfg["disks"].([]any); ok { + for _, d := range disks { + dm, ok := d.(map[string]any) + if !ok { + continue + } + if p, _ := dm["path"].(string); filepath.Base(p) == actorRootfsDiskName { + dm["path"] = diskPath + rewrote = true + } + } + } + if !rewrote { + return fmt.Errorf("no %q disk found in %q to repoint", actorRootfsDiskName, cfgPath) + } + out, err := json.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(cfgPath, out, 0o600) +} diff --git a/cmd/ateom-microvm/run.go b/cmd/ateom-microvm/run.go new file mode 100644 index 000000000..f32b829d5 --- /dev/null +++ b/cmd/ateom-microvm/run.go @@ -0,0 +1,529 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + specs "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// runningActor holds the live state for one actor's micro-VM. ateom owns the +// cloud-hypervisor process directly (booted by RunWorkload or relaunched by +// RestoreWorkload), so it tracks that process and its api-socket for teardown. +type runningActor struct { + containerName string + + // baseID is the FROZEN base sandbox id propagated across this actor's restore + // lineage. For a cold-run actor this is the actor's own id; for a restored + // actor it is the id read from the snapshot's base-id file (the golden id, + // propagated). CheckpointWorkload writes it back into the next snapshot's + // base-id file so the chain survives suspend->resume->suspend. + baseID string + + // ateom owns this CH process (booted at Run or relaunched at Restore). + chCmd *exec.Cmd + // apiSocket is the CH api-socket for this ateom-owned VMM. + apiSocket string + + // restoreSourceDir is the snapshot dir this actor was OnDemand-restored from + // (the base CH is demand-paging from). Set only on the owned-boot virtio-blk + // path when restored via OnDemand. CheckpointWorkload overlays CH's new (sparse, + // faulted-only) snapshot onto this base to produce a COMPLETE snapshot (CH's + // OnDemand snapshot alone drops the un-faulted pages). Empty for cold-run actors + // (their snapshot is already complete). + restoreSourceDir string + + // logAgent is the kata-agent ttrpc client kept open for the lifetime of the + // stdout/stderr forwarding goroutines (they pump the container's output via + // ReadStdout/ReadStderr on this connection). It is NOT closed when RunWorkload / + // RestoreWorkload return — teardownActor closes it, which makes the in-flight + // ReadStdout/ReadStderr calls fail and the forwarding goroutines exit (io.EOF). + // nil if forwarding was not started (e.g. a best-effort post-restore dial failed). + logAgent *kata.AgentClient +} + +// baseIDFile is a tiny snapshot file (under the checkpoint/restore dir) holding +// the FROZEN base sandbox id — the id the guest's virtio-fs find-paths are pinned +// to (/rootfs). It is the id the RO base was FIRST shared under (the golden +// actor's cold-run id) and is INVARIANT across every restore of that actor's +// lineage: the guest memory keeps referencing /rootfs, while the snapshot +// config.json's socket paths get rewritten to the current actor id on each restore. +// RestoreWorkload reads this to lay the reconstructed-from-image base at the path +// the guest expects. (The config.json socket id is the WRONG source — it equals the +// current id, not the frozen golden id, for any restored-then-checkpointed actor.) +const baseIDFile = "base-id" + +// Asset names in RunWorkloadRequest.runtime_asset_paths (set by atelet's +// fetchRuntimeAssets, keyed by the ActorTemplate runtime asset names). +const ( + assetCH = "cloud-hypervisor" + assetKernel = "kata-kernel" + assetImage = "kata-image" + assetConfig = "kata-config" +) + +// actorRootfsDiskName is the actor's writable rootfs disk file under the actor +// dir; it is the /dev/vdb backing path recorded in the snapshot config.json and +// reopened verbatim on restore. +const actorRootfsDiskName = "actor-rootfs.ext4" + +// goldenRootfsDiskName is the verbatim copy of the actor's /dev/vdb disk AS-OF the +// golden snapshot, kept under the actor dir. reset-to-golden recreates /dev/vdb +// from it on restore (byte-identical to what the snapshot's guest RAM/ext4 cache +// expects), discarding the actor's later rootfs writes — gVisor semantics. +const goldenRootfsDiskName = "golden-rootfs.ext4" + +// fileMissing reports whether path does not exist. +func fileMissing(path string) bool { + _, err := os.Stat(path) + return os.IsNotExist(err) +} + +// copyDiskFile copies a (sparse) disk image verbatim, preserving holes so the +// (mostly-empty) ext4 image doesn't materialize its scratch blocks. Used to +// save/restore the golden rootfs disk template. +func copyDiskFile(ctx context.Context, src, dst string) error { + tmp := dst + ".tmp" + _ = os.Remove(tmp) + if out, err := exec.CommandContext(ctx, "cp", "--sparse=always", src, tmp).CombinedOutput(); err != nil { + return fmt.Errorf("cp %s -> %s: %w: %s", src, tmp, err, out) + } + if err := os.Rename(tmp, dst); err != nil { + return fmt.Errorf("rename %s -> %s: %w", tmp, dst, err) + } + return nil +} + +// resolvedRuntime holds the concrete binary/config paths for a request, taken +// from fetched runtime assets when present, else the process flags. +type resolvedRuntime struct { + chBinary string // path to the cloud-hypervisor binary + configFile string // path to the kata configuration.toml +} + +// firstNonEmpty returns the first non-empty string, or "" if all are empty. +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} + +// resolveRuntime resolves the cloud-hypervisor binary + the kata config path from +// fetched assets, falling back to flags. +func (s *AteomService) resolveRuntime(paths map[string]string) resolvedRuntime { + return resolvedRuntime{ + chBinary: firstNonEmpty(paths[assetCH], s.chBinary), + configFile: firstNonEmpty(paths[assetConfig], s.kataConfig), + } +} + +// RunWorkload boots the actor as a cloud-hypervisor micro-VM that ateom owns. +// +// ateom boots cloud-hypervisor itself — no kata shim — and gives the actor a +// writable boot-time virtio-blk disk (/dev/vdb, built from the OCI bundle rootfs) +// as its container rootfs. Rootfs data lives on that host-backed disk rather than +// a guest tmpfs overlay-upper, so the CH snapshot is memory-only with no balloon +// needed to reclaim a RAM-backed upper. It replicates the kata clh boot (vm.create +// kernel+image, add-net, vm.boot) and the shim's post-boot work (agent +// CreateSandbox + guest network config) before driving the kata-agent to start the +// blk-rootfs container. +// +// Contract with atelet (mirrors ateom-gvisor): +// - The runtime assets (guest kernel, guest OS image, cloud-hypervisor, base +// kata config) are on disk and passed as runtime asset paths. +// - The OCI bundle (config.json + populated rootfs/) is prepared per container. +func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkloadRequest) (resp *ateompb.RunWorkloadResponse, retErr error) { + s.lock.Lock() + defer s.lock.Unlock() + + ns := req.GetActorTemplateNamespace() + name := req.GetActorTemplateName() + id := req.GetActorId() + + s.actorLogger.EmitLifecycleLog("Actor starting", id, name, ns) + + // KNOWN GAP vs the gVisor runtime: it runs multiple containers per actor; this + // runtime is single-container for now. Multi-container is a mechanical extension + // (one boot-time virtio-blk rootfs disk + agent CreateContainer per container, + // sharing the one guest/sandbox) and is tracked as follow-up work. + containers := req.GetSpec().GetContainers() + if len(containers) != 1 { + return nil, status.Errorf(codes.Unimplemented, "ateom-microvm supports exactly one container, got %d", len(containers)) + } + containerName := containers[0].GetName() + + // Owned-boot builds the CH vm.create itself, so it needs the guest kernel + + // image paths directly. + paths := req.GetRuntimeAssetPaths() + kernel, image := paths[assetKernel], paths[assetImage] + if kernel == "" || image == "" { + return nil, fmt.Errorf("owned-boot requires %q and %q asset paths", assetKernel, assetImage) + } + actorDir := ateompath.ActorPath(ns, name, id) + rr := s.resolveRuntime(paths) + + // Networking (host side): per-activation veth into the interior netns. The + // tap + TC mirror is built below (after the VM exists) so its FDs are fresh. + if err := s.setupActorNetwork(ctx); err != nil { + return nil, fmt.Errorf("while setting up actor network: %w", err) + } + defer func() { + if retErr != nil { + if cleanupErr := s.cleanupActorNetwork(ctx); cleanupErr != nil { + slog.WarnContext(ctx, "Failed to clean up actor network after Run failure", slog.Any("err", cleanupErr)) + } + } + }() + + bundle := ateompath.OCIBundlePath(ns, name, id, containerName) + spec, err := ensureKataCompatibleSpec(bundle, id, ateompath.AteomNetNSPath(s.podUID)) + if err != nil { + return nil, fmt.Errorf("while preparing kata OCI spec: %w", err) + } + + // Build the actor's writable rootfs as a raw ext4 virtio-blk disk from the + // atelet-populated OCI bundle rootfs. This becomes /dev/vdb. + diskPath := filepath.Join(actorDir, actorRootfsDiskName) + if err := kata.BuildExt4Image(ctx, filepath.Join(bundle, "rootfs"), diskPath); err != nil { + return nil, fmt.Errorf("while building actor rootfs disk: %w", err) + } + + // Guest sizing + agent kernel params from the kata config. + memMiB, vcpus, kparams, err := s.guestConfig(rr) + if err != nil { + return nil, err + } + + // Clean stale per-sandbox state + create the runtime dir for the sockets. + kata.CleanupSandboxState(ctx, id) + if err := os.MkdirAll(kata.VMDir(id), 0o700); err != nil { + return nil, fmt.Errorf("while creating VM dir: %w", err) + } + + // Launch a bare VMM (CH + api-socket); ateom owns this process for teardown. + apiSocket := filepath.Join(kata.VMDir(id), "clh-api.sock") + chCmd, client, err := ch.LaunchVMM(ctx, ch.LaunchVMMOptions{ + Binary: rr.chBinary, + APISocket: apiSocket, + Stdout: slogWriter{ctx}, + Stderr: slogWriter{ctx}, + }) + if err != nil { + return nil, fmt.Errorf("while launching VMM: %w", err) + } + defer func() { + if retErr != nil && chCmd.Process != nil { + _ = chCmd.Process.Kill() + _, _ = chCmd.Process.Wait() + } + }() + + // Assemble the CH VmConfig (kata-compatible cmdline, RO image on /dev/vda + + // writable rootfs on /dev/vdb). serialLog is also read on a failed agent dial + // below, so keep it here. + serialLog := filepath.Join(kata.VMDir(id), "serial.log") + vmCfg := buildVMConfig(id, kernel, image, diskPath, kparams, serialLog, memMiB, vcpus) + if err := client.CreateVM(ctx, vmCfg); err != nil { + return nil, fmt.Errorf("while creating VM: %w", err) + } + + // Network device: build the tap + TC mirror against the actor veth and add a + // virtio-net to the created (pre-boot) VM with the tap FDs (SCM_RIGHTS). + tapFiles, err := s.setupRestoreTap(ctx, "tap0_kata", 1) + if err != nil { + return nil, fmt.Errorf("while building tap: %w", err) + } + defer func() { + for _, f := range tapFiles { + _ = f.Close() // CH dups adopted FDs; ours always close. + } + }() + var fds []int + for _, f := range tapFiles { + fds = append(fds, int(f.Fd())) + } + if err := client.AddNetWithFDs(ctx, actorGuestMAC, 2*len(tapFiles), fds); err != nil { + return nil, fmt.Errorf("while adding net device: %w", err) + } + + // Boot. + if err := client.BootVM(ctx); err != nil { + return nil, fmt.Errorf("while booting VM: %w", err) + } + slog.InfoContext(ctx, "Micro-VM booted (owned-boot)", slog.String("id", id), slog.String("api", apiSocket)) + + // Dial the kata-agent over hybrid-vsock. The agent only starts listening once + // the guest's init reaches kata-containers.target — well after CH creates the + // vsock socket file — so poll the CONNECT until it answers (as the kata shim + // does), rather than dialing once. + vsockPath := kata.VsockSocketPath(id) + if !waitForFile(vsockPath, 15*time.Second) { + return nil, fmt.Errorf("kata-agent vsock socket %q did not appear", vsockPath) + } + ac, err := dialAgentRetry(ctx, vsockPath, 60*time.Second) + if err != nil { + if b, rerr := os.ReadFile(serialLog); rerr == nil { + slog.ErrorContext(ctx, "agent dial failed; guest serial tail", slog.String("serial", tailString(string(b), 3000))) + } + return nil, fmt.Errorf("while dialing kata-agent: %w", err) + } + // The agent client must stay open past this RPC: the stdout/stderr forwarding + // goroutines (started below) read over it for the actor's lifetime. It is stored + // on the runningActor and closed by teardownActor. Close it here only if Run + // fails after this point (no runningActor recorded). + defer func() { + if retErr != nil { + _ = ac.Close() + } + }() + + // Post-boot kata-agent setup: sandbox, guest networking, start the container. + if err := s.startActorContainer(ctx, ac, id, vsockPath, spec); err != nil { + return nil, err + } + + ra := &runningActor{chCmd: chCmd, apiSocket: apiSocket, containerName: containerName, baseID: id, logAgent: ac} + s.running[id] = ra + + // Forward the actor container's stdout/stderr into the pod logs (parity with + // ateom-gvisor). StartBlkWorkload uses containerID==execID==id, so the agent + // keys the streams by id. The goroutines read over ac for the actor's lifetime + // and exit (io.EOF) when teardownActor closes ac. + s.startActorLogForwarding(ac, id, name, ns, containerName) + + s.actorLogger.EmitLifecycleLog("Actor started", id, name, ns) + slog.InfoContext(ctx, "Actor started (owned-boot, virtio-blk rootfs)", slog.String("id", id)) + return &ateompb.RunWorkloadResponse{}, nil +} + +// guestConfig reads guest sizing + agent kernel params from the resolved kata +// config, enabling the debug console (vsock 1026) for in-guest diagnostics and, +// with kataDebug, raising the agent log level. +func (s *AteomService) guestConfig(rr resolvedRuntime) (memMiB, vcpus int, kparams string, err error) { + var cfgBytes []byte + if rr.configFile != "" { + cfgBytes, _ = os.ReadFile(rr.configFile) + } + cfg, err := kata.ParseConfig(cfgBytes, 2048, 1) + if err != nil { + return 0, 0, "", fmt.Errorf("while parsing kata config: %w", err) + } + kparams = kata.WithDebugConsole(cfg.KernelParams) + if s.kataDebug { + kparams = kata.WithAgentDebug(kparams) + } + return cfg.MemoryMiB, cfg.VCPUs, kparams, nil +} + +// buildVMConfig assembles the cloud-hypervisor VmConfig for the owned boot. The +// kernel cmdline replicates kata's clh boot cmdline (verified against a live kata +// snapshot's payload.cmdline): beyond the root/clh base params it MUST include +// systemd.unit=kata-containers.target (else systemd boots the default target and +// powers off — the guest exits ~6s in) and mask systemd-networkd (the agent owns +// eth0). The console is ARCH-SPECIFIC: ttyAMA0 (PL011) on arm64, ttyS0 (8250) on +// amd64 — the wrong one => "unable to open an initial console". The config's +// kernel_params are appended; serial is captured to serialLog for boot debugging. +// The RO guest image is /dev/vda, the writable rootfs /dev/vdb. +func buildVMConfig(id, kernel, image, diskPath, kparams, serialLog string, memMiB, vcpus int) ch.VmConfig { + console := "ttyS0" + if runtime.GOARCH == "arm64" { + console = "ttyAMA0" + } + cmdline := "root=/dev/vda1 rootflags=data=ordered,errors=remount-ro ro rootfstype=ext4 " + + "panic=1 no_timer_check noreplace-smp console=" + console + ",115200n8 " + + "systemd.unit=kata-containers.target systemd.mask=systemd-networkd.service systemd.mask=systemd-networkd.socket" + if kparams != "" { + cmdline += " " + kparams + } + return ch.VmConfig{ + Cpus: ch.CpusConfig{BootVcpus: int32(vcpus), MaxVcpus: int32(vcpus)}, + Memory: ch.MemoryConfig{Size: int64(memMiB) * 1024 * 1024, Shared: true}, + Payload: ch.PayloadConfig{Kernel: kernel, Cmdline: cmdline}, + Disks: []ch.DiskConfig{ + {Path: image, Readonly: true, ImageType: "Raw", NumQueues: int32(vcpus), QueueSize: 1024}, + {Path: diskPath, Readonly: false, ImageType: "Raw", NumQueues: int32(vcpus), QueueSize: 1024}, + }, + Rng: &ch.RngConfig{Src: "/dev/urandom"}, + Serial: &ch.ConsoleConfig{Mode: "File", File: serialLog}, + Vsock: &ch.VsockConfig{Cid: 3, Socket: kata.VsockSocketPath(id)}, + } +} + +// startActorContainer performs the post-boot kata-agent setup the shim normally +// does at boot: establish the sandbox, configure guest networking (eth0 +// IP/MAC/MTU + routes), and start the actor container on its /dev/vdb rootfs. On +// failure it dumps guest diagnostics over the debug console. +func (s *AteomService) startActorContainer(ctx context.Context, ac *kata.AgentClient, id, vsockPath string, spec *specs.Spec) error { + // Establish the agent sandbox (the shim normally does this at boot). + sbCtx, sbCancel := context.WithTimeout(ctx, 20*time.Second) + err := ac.CreateSandbox(sbCtx, &agentpb.CreateSandboxRequest{Hostname: spec.Hostname, SandboxId: id}) + sbCancel() + if err != nil { + return fmt.Errorf("while creating agent sandbox: %w", err) + } + + // Configure guest networking (the shim's job): eth0 IP/MAC/MTU, routes, ARP. + mtu := uint64(s.actorVethMTU(ctx)) + netCtx, netCancel := context.WithTimeout(ctx, 20*time.Second) + err = s.configureGuestNetwork(netCtx, ac, mtu) + netCancel() + if err != nil { + dump := kata.DebugConsoleDump(ctx, vsockPath, "ip addr 2>&1; echo '== route =='; ip route 2>&1; echo '== neigh =='; ip neigh 2>&1") + slog.ErrorContext(ctx, "guest network config failed; dump", slog.String("dump", dump)) + return fmt.Errorf("while configuring guest network: %w", err) + } + + // Start the actor with its rootfs on /dev/vdb (single blk storage). + wlCtx, wlCancel := context.WithTimeout(ctx, 30*time.Second) + err = ac.StartBlkWorkload(wlCtx, id, "/dev/vdb", spec) + wlCancel() + if err != nil { + dump := kata.DebugConsoleDump(ctx, vsockPath, + "echo '== /dev/vdb =='; ls -l /dev/vdb 2>&1; blkid /dev/vdb 2>&1; "+ + "echo '== mounts =='; grep kata /proc/mounts 2>&1") + slog.ErrorContext(ctx, "blk workload failed; dump", slog.String("dump", dump)) + return fmt.Errorf("while starting blk workload: %w", err) + } + return nil +} + +// startActorLogForwarding spawns two goroutines that pump the actor container's +// stdout and stderr (read over the kata-agent ttrpc client ac via repeated +// ReadStdout/ReadStderr) through the shared actorlog forwarder, which annotates +// each line with the actor's ate.dev/* labels and writes it to the pod's stdout. +// +// The streams are keyed by containerID==execID==id (the value StartBlkWorkload +// passed); lines are tagged with the container name (ate.dev/container_name). The +// reader contexts are context.Background() — the goroutines are NOT bound to the RPC +// that started them; they terminate when ac is closed (by teardownActor), which +// makes the in-flight ReadStdout/ReadStderr fail and the StreamReader return +// io.EOF, ending WrapContainerLogs. This keeps the agent connection (which ttrpc +// allows concurrent Calls on) alive for forwarding while guaranteeing no goroutine +// outlives the connection. +func (s *AteomService) startActorLogForwarding(ac *kata.AgentClient, id, name, ns, containerName string) { + go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, id, id, false), id, name, ns, containerName) + go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, id, id, true), id, name, ns, containerName) +} + +// dialAgentRetry polls DialAgent until the kata-agent answers the hybrid-vsock +// CONNECT (the socket file exists at boot, but the agent only listens once the +// guest reaches kata-containers.target) or the overall timeout elapses. Each +// attempt is capped at 5s (usually it fails fast with connection-refused while +// the agent isn't listening yet; the cap only bounds a rare hung dial), then +// waits 500ms before retrying — so steady-state polling is ~every 500ms, not 5s. +func dialAgentRetry(ctx context.Context, vsockPath string, timeout time.Duration) (*kata.AgentClient, error) { + deadline := time.Now().Add(timeout) + var lastErr error + for { + dctx, cancel := context.WithTimeout(ctx, 5*time.Second) + ac, err := kata.DialAgent(dctx, vsockPath) + cancel() + if err == nil { + return ac, nil + } + lastErr = err + if time.Now().After(deadline) { + return nil, lastErr + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(500 * time.Millisecond): + } + } +} + +// tailString returns the last n bytes of s (for logging a serial-console tail). +func tailString(s string, n int) string { + if len(s) <= n { + return s + } + return s[len(s)-n:] +} + +// configureGuestNetwork replicates the kata shim's guest network setup over the +// agent: configure eth0 (IP/MAC/MTU), install the connected + default routes, and +// pin the gateway's ARP entry to its fixed MAC (so a restored guest's frozen +// neighbor entry stays valid). +func (s *AteomService) configureGuestNetwork(ctx context.Context, ac *kata.AgentClient, mtu uint64) error { + if err := ac.UpdateInterface(ctx, &agentpb.Interface{ + Device: actorVethName, + Name: actorVethName, + HwAddr: actorGuestMAC, + Mtu: mtu, + IPAddresses: []*agentpb.IPAddress{ + {Family: agentpb.IPFamily_v4, Address: actorVethIP, Mask: "30"}, + }, + }); err != nil { + return err + } + if err := ac.UpdateRoutes(ctx, []*agentpb.Route{ + {Dest: actorVethSubnet, Device: actorVethName, Scope: uint32(unix.RT_SCOPE_LINK), Family: agentpb.IPFamily_v4}, + {Dest: "", Gateway: actorVethGateway, Device: actorVethName, Family: agentpb.IPFamily_v4}, + }); err != nil { + return err + } + return ac.AddARPNeighbors(ctx, []*agentpb.ARPNeighbor{{ + ToIPAddress: &agentpb.IPAddress{Family: agentpb.IPFamily_v4, Address: actorVethGateway}, + Device: actorVethName, + Lladdr: hostVethMAC, + State: 0x80, // NUD_PERMANENT + }}) +} + +// waitForFile polls for path to exist, up to d. Used to wait for the kata-agent +// hybrid-vsock socket the shim creates during VM boot before dialing it. +func waitForFile(path string, d time.Duration) bool { + deadline := time.Now().Add(d) + for { + if _, err := os.Stat(path); err == nil { + return true + } + if time.Now().After(deadline) { + return false + } + time.Sleep(50 * time.Millisecond) + } +} + +// slogWriter adapts an io.Writer to slog at info level, capturing the +// cloud-hypervisor process's stdout/stderr into the worker logs. +type slogWriter struct{ ctx context.Context } + +func (w slogWriter) Write(p []byte) (int, error) { + slog.InfoContext(w.ctx, "cloud-hypervisor", slog.String("out", string(p))) + return len(p), nil +} diff --git a/cmd/ateom-microvm/service_integration_test.go b/cmd/ateom-microvm/service_integration_test.go new file mode 100644 index 000000000..ab79446ef --- /dev/null +++ b/cmd/ateom-microvm/service_integration_test.go @@ -0,0 +1,473 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/internal/actorlog" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/vishvananda/netns" +) + +// TestServiceRunBlkRootfs covers the owned-boot cold-run path: ateom boots +// cloud-hypervisor itself and gives the actor a writable boot-time virtio-blk +// rootfs (/dev/vdb), then drives the kata-agent to start the container. It +// exercises only run (no checkpoint/restore). Unlike TestServiceE2E it MUST pass +// the guest kernel + image + base-config asset paths, because owned-boot builds +// the CH vm.create itself rather than reading configuration.toml. +// +// Gated behind KATA_INTEGRATION=1. Required env: +// +// KATA_ROOTFS_SRC= a populated actor rootfs (becomes /dev/vdb) +// KATA_KERNEL= guest kernel (vmlinux.container) +// KATA_IMAGE= guest OS image (kata-containers.img, /dev/vda) +// KATA_CONFIG= a stock kata clh configuration.toml (for kernel_params + sizing) +// +// Optional: KATA_CH / KATA_VIRTIOFSD (defaults provided). Run as root on a host +// with kata + /dev/kvm + mkfs.ext4 (e2fsprogs): +// +// sudo KATA_INTEGRATION=1 KATA_ROOTFS_SRC=/path/to/rootfs KATA_KERNEL=... KATA_IMAGE=... \ +// KATA_CONFIG=... ./ateom-microvm.test -test.v -test.run BlkRootfs +func TestServiceRunBlkRootfs(t *testing.T) { + if os.Getenv("KATA_INTEGRATION") != "1" { + t.Skip("set KATA_INTEGRATION=1 to run (requires kata + /dev/kvm + root + e2fsprogs)") + } + rootfsSrc := os.Getenv("KATA_ROOTFS_SRC") + if rootfsSrc == "" { + t.Fatal("KATA_ROOTFS_SRC is required") + } + kernel, image, cfg := os.Getenv("KATA_KERNEL"), os.Getenv("KATA_IMAGE"), os.Getenv("KATA_CONFIG") + if kernel == "" || image == "" || cfg == "" { + t.Fatal("KATA_KERNEL, KATA_IMAGE, and KATA_CONFIG are required for the owned-boot path") + } + chBin := envOrTest("KATA_CH", "/usr/local/bin/cloud-hypervisor") + + ns, name := "default", "e2e-blk" + id := fmt.Sprintf("ateomchv-blk-%d", os.Getpid()) + container := "app" + + bundle := ateompath.OCIBundlePath(ns, name, id, container) + rootfs := filepath.Join(bundle, "rootfs") + if err := os.MkdirAll(rootfs, 0o755); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("cp", "-a", rootfsSrc+"/.", rootfs+"/").CombinedOutput(); err != nil { + t.Fatalf("copying rootfs: %v: %s", err, out) + } + writeMinimalGvisorStyleSpec(t, bundle) + + podUID := "testpod-blk" + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + interiorNetNS, err := createNetNSWithoutSwitching(ateompath.AteomNetNSName(podUID)) + if err != nil { + t.Fatalf("creating interior netns: %v", err) + } + svc := NewService(podUID, chBin, "", true, interiorNetNS, actorlog.NewActorLogger(actorlog.NewSyncedWriter(os.Stdout), false)) + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + t.Cleanup(func() { + cctx, c := context.WithTimeout(context.Background(), 20*time.Second) + svc.teardownActor(cctx, id, svc.running[id], nil) + c() + _ = os.RemoveAll(ateompath.ActorPath(ns, name, id)) + _ = os.RemoveAll(kata.VMDir(id)) + _ = interiorNetNS.Close() + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + }) + + if _, err := svc.RunWorkload(ctx, &ateompb.RunWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: map[string]string{ + assetKernel: kernel, + assetImage: image, + assetConfig: cfg, + assetCH: chBin, + }, + }); err != nil { + // Best-effort: dump the guest serial console (captured to VMDir/serial.log) + // so a boot failure shows the kernel/agent output. + if b, rerr := os.ReadFile(filepath.Join(kata.VMDir(id), "serial.log")); rerr == nil { + t.Logf("[serial.log tail]\n%s", lastLines(string(b), 60)) + } + t.Fatalf("RunWorkload (owned-boot): %v", err) + } + t.Log("RunWorkload OK (owned-boot: CH booted by ateom, actor rootfs on /dev/vdb)") + + // Liveness: the ateom-owned CH must be up and the VM Running. + client := ch.NewClient(filepath.Join(kata.VMDir(id), "clh-api.sock")) + if err := client.WaitReady(ctx, 10*time.Second); err != nil { + t.Fatalf("owned CH not ready: %v", err) + } + // Confirm the actor's rootfs really came from /dev/vdb (a marker visible via + // the guest debug console — the actor's own files live on the blk disk). + dump := kata.DebugConsoleDump(ctx, kata.VsockSocketPath(id), + "echo '== vdb =='; blkid /dev/vdb 2>&1; echo '== rootfs mount =='; grep vdb /proc/mounts 2>&1; echo '== ip =='; ip -4 addr show eth0 2>&1") + t.Logf("[guest] %s", dump) +} + +// TestServiceCheckpointRestoreBlkRootfs exercises memory-only snapshot + restore +// with in-RAM continuity: the owned-boot actor snapshots MEMORY-ONLY (no +// shared-dir.tar, no balloon) and restores with its guest RAM intact. It writes a +// sentinel into guest tmpfs (/run = RAM), checkpoints, +// ships the snapshot dir, restores on a fresh CH process, and reads the sentinel +// back — if RAM continuity holds it survives. Same gating/env as +// TestServiceRunBlkRootfs. +func TestServiceCheckpointRestoreBlkRootfs(t *testing.T) { + if os.Getenv("KATA_INTEGRATION") != "1" { + t.Skip("set KATA_INTEGRATION=1 to run (requires kata + /dev/kvm + root + e2fsprogs)") + } + rootfsSrc := os.Getenv("KATA_ROOTFS_SRC") + kernel, image, cfg := os.Getenv("KATA_KERNEL"), os.Getenv("KATA_IMAGE"), os.Getenv("KATA_CONFIG") + if rootfsSrc == "" || kernel == "" || image == "" || cfg == "" { + t.Fatal("KATA_ROOTFS_SRC, KATA_KERNEL, KATA_IMAGE, KATA_CONFIG are required") + } + chBin := envOrTest("KATA_CH", "/usr/local/bin/cloud-hypervisor") + + ns, name := "default", "e2e-blkcr" + id := fmt.Sprintf("ateomchv-blkcr-%d", os.Getpid()) + container := "app" + + bundle := ateompath.OCIBundlePath(ns, name, id, container) + rootfs := filepath.Join(bundle, "rootfs") + if err := os.MkdirAll(rootfs, 0o755); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("cp", "-a", rootfsSrc+"/.", rootfs+"/").CombinedOutput(); err != nil { + t.Fatalf("copying rootfs: %v: %s", err, out) + } + writeMinimalGvisorStyleSpec(t, bundle) + + podUID := "testpod-blkcr" + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + interiorNetNS, err := createNetNSWithoutSwitching(ateompath.AteomNetNSName(podUID)) + if err != nil { + t.Fatalf("creating interior netns: %v", err) + } + svc := NewService(podUID, chBin, "", true, interiorNetNS, actorlog.NewActorLogger(actorlog.NewSyncedWriter(os.Stdout), false)) + ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second) + defer cancel() + t.Cleanup(func() { + cctx, c := context.WithTimeout(context.Background(), 20*time.Second) + svc.teardownActor(cctx, id, svc.running[id], nil) + c() + _ = os.RemoveAll(ateompath.ActorPath(ns, name, id)) + _ = os.RemoveAll(kata.VMDir(id)) + _ = interiorNetNS.Close() + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + }) + + assets := map[string]string{assetKernel: kernel, assetImage: image, assetConfig: cfg, assetCH: chBin} + if _, err := svc.RunWorkload(ctx, &ateompb.RunWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: assets, + }); err != nil { + t.Fatalf("RunWorkload: %v", err) + } + t.Log("RunWorkload OK") + + // Write an in-RAM (tmpfs /run) sentinel via the guest debug console. + const sentinel = "BLKROOT_CONTINUITY_OK_4242" + vsock := kata.VsockSocketPath(id) + _ = kata.DebugConsoleDump(ctx, vsock, "echo "+sentinel+" > /run/blkroot-sentinel; sync; echo wrote") + if got := kata.DebugConsoleDump(ctx, vsock, "cat /run/blkroot-sentinel"); !strings.Contains(got, sentinel) { + t.Fatalf("sentinel not readable pre-checkpoint: %q", got) + } + t.Log("wrote in-RAM sentinel") + + // CheckpointWorkload — memory-only, no balloon/wipe. + if _, err := svc.CheckpointWorkload(ctx, &ateompb.CheckpointWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + }); err != nil { + t.Fatalf("CheckpointWorkload: %v", err) + } + checkpointDir := ateompath.CheckpointStateDir(ns, name, id) + for _, f := range []string{"config.json", "state.json", "memory-ranges", "base-id"} { + if _, err := os.Stat(filepath.Join(checkpointDir, f)); err != nil { + t.Fatalf("checkpoint missing %q: %v", f, err) + } + } + if _, err := os.Stat(filepath.Join(checkpointDir, "shared-dir.tar")); err == nil { + t.Error("snapshot has shared-dir.tar — owned-boot must be MEMORY-ONLY (no virtio-fs base)") + } + t.Log("CheckpointWorkload OK (memory-only: config/state/memory-ranges/base-id, no shared-dir.tar)") + + // Ship snapshot dir -> restore dir (simulating atelet object-storage round trip). + restoreDir := ateompath.RestoreStateDir(ns, name, id) + if err := os.MkdirAll(restoreDir, 0o700); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("cp", "-a", checkpointDir+"/.", restoreDir+"/").CombinedOutput(); err != nil { + t.Fatalf("shipping snapshot: %v: %s", err, out) + } + + // RestoreWorkload — reopen /dev/vdb, no virtiofsd/reconstruct. + if _, err := svc.RestoreWorkload(ctx, &ateompb.RestoreWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: assets, + }); err != nil { + t.Fatalf("RestoreWorkload: %v", err) + } + client := ch.NewClient(filepath.Join(kata.VMDir(id), "clh-api-restore.sock")) + if err := client.WaitReady(ctx, 10*time.Second); err != nil { + t.Fatalf("restored CH not ready: %v", err) + } + t.Log("RestoreWorkload OK") + + // In-RAM continuity: the sentinel written before checkpoint must survive. + got := kata.DebugConsoleDump(ctx, vsock, "cat /run/blkroot-sentinel") + if !strings.Contains(got, sentinel) { + t.Fatalf("RAM continuity FAILED: sentinel gone after restore #1: %q", got) + } + t.Logf("cycle1 OK: memory-only snapshot + restore, in-RAM continuity (%q)", strings.TrimSpace(got)) + + // --- SECOND cycle: checkpoint-AFTER-restore. This is the OnDemand diff-snapshot + // case — CH writes only the faulted delta and CheckpointWorkload overlays it onto + // the restore source to rebuild a COMPLETE snapshot. If the merge is wrong the + // snapshot is incomplete and restore #2 boots a corrupt guest (sentinel gone / + // unreachable). Write a SECOND sentinel first so we also prove pages dirtied in + // THIS activation are captured by the merge. --- + const sentinel2 = "BLKROOT_CYCLE2_OK_8888" + _ = kata.DebugConsoleDump(ctx, vsock, "echo "+sentinel2+" > /run/blkroot-sentinel2; sync") + if _, err := svc.CheckpointWorkload(ctx, &ateompb.CheckpointWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + }); err != nil { + t.Fatalf("CheckpointWorkload #2 (merge): %v", err) + } + // Ship the merged snapshot (overwrites restoreDir AFTER the merge read it). + if out, err := exec.Command("cp", "-a", checkpointDir+"/.", restoreDir+"/").CombinedOutput(); err != nil { + t.Fatalf("shipping snapshot #2: %v: %s", err, out) + } + if _, err := svc.RestoreWorkload(ctx, &ateompb.RestoreWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: assets, + }); err != nil { + t.Fatalf("RestoreWorkload #2: %v", err) + } + client2 := ch.NewClient(filepath.Join(kata.VMDir(id), "clh-api-restore.sock")) + if err := client2.WaitReady(ctx, 10*time.Second); err != nil { + t.Fatalf("restored CH #2 not ready: %v", err) + } + // BOTH sentinels must survive: sentinel (from cycle 1, an un-faulted source page + // recovered by the overlay) AND sentinel2 (dirtied this cycle, in CH's delta). + g1 := kata.DebugConsoleDump(ctx, vsock, "cat /run/blkroot-sentinel") + g2 := kata.DebugConsoleDump(ctx, vsock, "cat /run/blkroot-sentinel2") + if !strings.Contains(g1, sentinel) { + t.Fatalf("merge INCOMPLETE: cycle-1 sentinel lost after restore #2 (un-faulted source page dropped): %q", g1) + } + if !strings.Contains(g2, sentinel2) { + t.Fatalf("merge lost the cycle-2 delta: sentinel2 gone after restore #2: %q", g2) + } + t.Logf("OnDemand-merge OK: 2-cycle suspend/resume, both sentinels survived (%q | %q)", + strings.TrimSpace(g1), strings.TrimSpace(g2)) +} + +// TestServiceResetToGoldenBlkRootfs exercises reset-to-golden. From the +// golden snapshot, each restore recreates /dev/vdb byte-identical to the golden +// disk template, so an actor's rootfs writes do NOT persist into the next +// activation, while in-RAM state from the golden snapshot DOES. Two restores from +// the same golden snapshot: restore#1 writes a disk sentinel (runtime); restore#2 +// must NOT see it (disk reset), while the RAM sentinel survives both. +func TestServiceResetToGoldenBlkRootfs(t *testing.T) { + if os.Getenv("KATA_INTEGRATION") != "1" { + t.Skip("set KATA_INTEGRATION=1 to run (requires kata + /dev/kvm + root + e2fsprogs)") + } + rootfsSrc := os.Getenv("KATA_ROOTFS_SRC") + kernel, image, cfg := os.Getenv("KATA_KERNEL"), os.Getenv("KATA_IMAGE"), os.Getenv("KATA_CONFIG") + if rootfsSrc == "" || kernel == "" || image == "" || cfg == "" { + t.Fatal("KATA_ROOTFS_SRC, KATA_KERNEL, KATA_IMAGE, KATA_CONFIG are required") + } + chBin := envOrTest("KATA_CH", "/usr/local/bin/cloud-hypervisor") + + ns, name := "default", "e2e-blkrtg" + id := fmt.Sprintf("ateomchv-blkrtg-%d", os.Getpid()) + container := "app" + + bundle := ateompath.OCIBundlePath(ns, name, id, container) + if err := os.MkdirAll(filepath.Join(bundle, "rootfs"), 0o755); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("cp", "-a", rootfsSrc+"/.", filepath.Join(bundle, "rootfs")+"/").CombinedOutput(); err != nil { + t.Fatalf("copying rootfs: %v: %s", err, out) + } + writeMinimalGvisorStyleSpec(t, bundle) + + podUID := "testpod-blkrtg" + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + interiorNetNS, err := createNetNSWithoutSwitching(ateompath.AteomNetNSName(podUID)) + if err != nil { + t.Fatalf("creating interior netns: %v", err) + } + svc := NewService(podUID, chBin, "", true, interiorNetNS, actorlog.NewActorLogger(actorlog.NewSyncedWriter(os.Stdout), false)) + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) + defer cancel() + t.Cleanup(func() { + cctx, c := context.WithTimeout(context.Background(), 20*time.Second) + svc.teardownActor(cctx, id, svc.running[id], nil) + c() + _ = os.RemoveAll(ateompath.ActorPath(ns, name, id)) + _ = os.RemoveAll(kata.VMDir(id)) + _ = interiorNetNS.Close() + _ = netns.DeleteNamed(ateompath.AteomNetNSName(podUID)) + }) + + assets := map[string]string{assetKernel: kernel, assetImage: image, assetConfig: cfg, assetCH: chBin} + runReq := &ateompb.RunWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: assets, + } + restoreReq := &ateompb.RestoreWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + RuntimeAssetPaths: assets, + } + vsock := kata.VsockSocketPath(id) + const ramSentinel = "RAM_GOLDEN_OK_7777" + rootfsDir := "/run/kata-containers/" + id + "/rootfs" + const diskSentinel = "DISK_WRITE_SHOULD_RESET_9999" + + // --- Golden: run, plant an in-RAM sentinel, checkpoint (saves golden snapshot + // + golden disk template), tear down. --- + if _, err := svc.RunWorkload(ctx, runReq); err != nil { + t.Fatalf("RunWorkload: %v", err) + } + _ = kata.DebugConsoleDump(ctx, vsock, "echo "+ramSentinel+" > /run/ram-sentinel; sync") + if _, err := svc.CheckpointWorkload(ctx, &ateompb.CheckpointWorkloadRequest{ + ActorTemplateNamespace: ns, ActorTemplateName: name, ActorId: id, + Spec: &ateompb.WorkloadSpec{Containers: []*ateompb.Container{{Name: container}}}, + }); err != nil { + t.Fatalf("CheckpointWorkload: %v", err) + } + // golden disk template must have been saved. + if _, err := os.Stat(filepath.Join(ateompath.ActorPath(ns, name, id), "golden-rootfs.ext4")); err != nil { + t.Fatalf("golden rootfs template not saved: %v", err) + } + checkpointDir := ateompath.CheckpointStateDir(ns, name, id) + restoreDir := ateompath.RestoreStateDir(ns, name, id) + if err := os.MkdirAll(restoreDir, 0o700); err != nil { + t.Fatal(err) + } + if out, err := exec.Command("cp", "-a", checkpointDir+"/.", restoreDir+"/").CombinedOutput(); err != nil { + t.Fatalf("shipping snapshot: %v: %s", err, out) + } + t.Log("golden checkpoint OK (snapshot + golden disk template saved)") + + // --- Restore #1: disk reset from golden template; write a disk sentinel at + // runtime, confirm it lands, then tear down (discard). --- + if _, err := svc.RestoreWorkload(ctx, restoreReq); err != nil { + t.Fatalf("RestoreWorkload #1: %v", err) + } + if got := kata.DebugConsoleDump(ctx, vsock, "cat /run/ram-sentinel"); !strings.Contains(got, ramSentinel) { + t.Fatalf("restore#1 RAM continuity failed: %q", got) + } + _ = kata.DebugConsoleDump(ctx, vsock, "echo "+diskSentinel+" > "+rootfsDir+"/disk-sentinel; sync") + if got := kata.DebugConsoleDump(ctx, vsock, "cat "+rootfsDir+"/disk-sentinel"); !strings.Contains(got, diskSentinel) { + t.Fatalf("restore#1 disk sentinel did not land: %q", got) + } + t.Log("restore#1 OK: RAM sentinel present, disk sentinel written") + tdCtx, tdCancel := context.WithTimeout(ctx, 20*time.Second) + svc.teardownActor(tdCtx, id, svc.running[id], ch.NewClient(filepath.Join(kata.VMDir(id), "clh-api-restore.sock"))) + tdCancel() + delete(svc.running, id) + + // --- Restore #2: disk reset AGAIN from golden template — the disk sentinel + // from restore#1 must be GONE, while the golden RAM sentinel still survives. --- + if _, err := svc.RestoreWorkload(ctx, restoreReq); err != nil { + t.Fatalf("RestoreWorkload #2: %v", err) + } + if got := kata.DebugConsoleDump(ctx, vsock, "cat /run/ram-sentinel"); !strings.Contains(got, ramSentinel) { + t.Fatalf("restore#2 RAM continuity failed: %q", got) + } + got := kata.DebugConsoleDump(ctx, vsock, "cat "+rootfsDir+"/disk-sentinel 2>&1; echo END") + if strings.Contains(got, diskSentinel) { + t.Fatalf("reset-to-golden FAILED: disk sentinel persisted after restore#2: %q", got) + } + t.Logf("reset-to-golden OK: discarded the rootfs write (disk sentinel gone) while RAM continuity held: %q", strings.TrimSpace(got)) +} + +func lastLines(s string, n int) string { + lines := strings.Split(strings.TrimRight(s, "\n"), "\n") + if len(lines) > n { + lines = lines[len(lines)-n:] + } + return strings.Join(lines, "\n") + "\n" +} + +func envOrTest(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// writeMinimalGvisorStyleSpec writes a deliberately minimal OCI spec (no +// linux.resources / cgroupsPath) so the test exercises ensureKataCompatibleSpec. +func writeMinimalGvisorStyleSpec(t *testing.T, bundle string) { + t.Helper() + spec := map[string]any{ + "ociVersion": "1.0.2", + "process": map[string]any{ + "user": map[string]any{"uid": 0, "gid": 0}, + "args": []string{"sleep", "3600"}, + "env": []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + "cwd": "/", + "capabilities": map[string]any{ + "bounding": []string{"CAP_KILL", "CAP_AUDIT_WRITE", "CAP_NET_BIND_SERVICE"}, + "effective": []string{"CAP_KILL", "CAP_AUDIT_WRITE", "CAP_NET_BIND_SERVICE"}, + "permitted": []string{"CAP_KILL", "CAP_AUDIT_WRITE", "CAP_NET_BIND_SERVICE"}, + }, + }, + "root": map[string]any{"path": "rootfs", "readonly": false}, + "hostname": "ateomchv", + "mounts": []map[string]any{ + {"destination": "/proc", "type": "proc", "source": "proc"}, + {"destination": "/dev", "type": "tmpfs", "source": "tmpfs"}, + {"destination": "/sys", "type": "sysfs", "source": "sysfs", "options": []string{"nosuid", "noexec", "nodev", "ro"}}, + }, + "linux": map[string]any{ + "namespaces": []map[string]any{ + {"type": "pid"}, {"type": "network"}, {"type": "ipc"}, {"type": "uts"}, {"type": "mount"}, + }, + }, + } + b, err := json.MarshalIndent(spec, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(bundle, "config.json"), b, 0o600); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/ateom-microvm/spec.go b/cmd/ateom-microvm/spec.go new file mode 100644 index 000000000..8b9d5ca08 --- /dev/null +++ b/cmd/ateom-microvm/spec.go @@ -0,0 +1,153 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// ensureKataCompatibleSpec augments the bundle's config.json with the fields +// kata's OCI conversion requires but atelet's (gVisor-oriented) spec omits. +// Without linux.resources, kata's ContainerConfig nil-derefs and the shim +// crashes. This shaper is a bridge; a future atelet change should emit +// runtime-appropriate specs so it can retire. +func ensureKataCompatibleSpec(bundle, id, netnsPath string) (*specs.Spec, error) { + specPath := filepath.Join(bundle, "config.json") + b, err := os.ReadFile(specPath) + if err != nil { + return nil, fmt.Errorf("reading %q: %w", specPath, err) + } + var spec specs.Spec + if err := json.Unmarshal(b, &spec); err != nil { + return nil, fmt.Errorf("parsing %q: %w", specPath, err) + } + + if spec.Linux == nil { + spec.Linux = &specs.Linux{} + } + if spec.Linux.Resources == nil { + spec.Linux.Resources = defaultKataResources() + } + if spec.Linux.CgroupsPath == "" { + spec.Linux.CgroupsPath = "/ateomchv/" + id + } + + // atelet's spec carries gVisor pause-model CRI annotations + // (container-type=container, sandbox-id=pause). kata reads those and waits + // for a separate "pause" sandbox that we never create, failing with "the + // sandbox hasn't been created". Strip them so kata treats this single + // container as its own sandbox (creates the VM), as in the integration tests. + for k := range spec.Annotations { + if strings.HasPrefix(k, "io.kubernetes.cri.") { + delete(spec.Annotations, k) + } + } + + // NB: no virtio-fs-overlay annotation here. With the STOCK shim, this spec is + // for the "carrier" container that only boots the VM + shares the RO base over + // virtio-fs. ateom assembles the actual overlay rootfs itself by driving the + // kata-agent CreateContainer over ttrpc (see RunWorkload) — no patched shim. + + // Point the network namespace at our interior netns (which holds the pod's + // eth0); kata finds eth0 there and wires it to the VM's virtio-net. + netnsSet := false + for i := range spec.Linux.Namespaces { + if spec.Linux.Namespaces[i].Type == specs.NetworkNamespace { + spec.Linux.Namespaces[i].Path = netnsPath + netnsSet = true + } + } + if !netnsSet { + spec.Linux.Namespaces = append(spec.Linux.Namespaces, specs.LinuxNamespace{ + Type: specs.NetworkNamespace, Path: netnsPath, + }) + } + + // Replace atelet's gVisor-oriented mounts (minimal /dev tmpfs, a + // /etc/resolv.conf host bind that ENOENTs against the distroless rootfs) with + // the exact set `ctr run --runtime io.containerd.kata.v2` emits, which kata's + // agent accepts. (Static shaper; pod DNS integration is future work.) + // + // KNOWN GAP vs the gVisor runtime: this also drops atelet's read-only actor + // identity bind mount (/run/ate/actor-id). The micro-VM guest can't see host + // paths (the rootfs is a virtio-blk disk, not a shared filesystem), and + // reset-to-golden restores guest RAM + rootfs from the golden snapshot, so a + // per-actor file written into the rootfs would be shadowed/incorrect on restore. + // Exposing the identity needs a per-actor volume injected from OUTSIDE the golden + // state; not yet implemented. No micro-VM workload depends on it today. + spec.Mounts = defaultKataMounts() + + out, err := json.MarshalIndent(&spec, "", " ") + if err != nil { + return nil, fmt.Errorf("marshaling spec: %w", err) + } + if err := os.WriteFile(specPath, out, 0o600); err != nil { + return nil, fmt.Errorf("writing %q: %w", specPath, err) + } + return &spec, nil +} + +// defaultKataMounts mirrors the mount set `ctr run --runtime io.containerd.kata.v2` +// produces (the proven-good shape for the kata agent). +func defaultKataMounts() []specs.Mount { + return []specs.Mount{ + {Destination: "/proc", Type: "proc", Source: "proc", Options: []string{"nosuid", "noexec", "nodev"}}, + {Destination: "/dev", Type: "tmpfs", Source: "tmpfs", Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}}, + {Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}}, + {Destination: "/dev/shm", Type: "tmpfs", Source: "shm", Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"}}, + {Destination: "/dev/mqueue", Type: "mqueue", Source: "mqueue", Options: []string{"nosuid", "noexec", "nodev"}}, + {Destination: "/sys", Type: "sysfs", Source: "sysfs", Options: []string{"nosuid", "noexec", "nodev", "ro"}}, + {Destination: "/run", Type: "tmpfs", Source: "tmpfs", Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}}, + } +} + +// defaultKataResources mirrors the device allowlist + cpu shares that +// `ctr run --runtime io.containerd.kata.v2` emits (the proven-good shape). +func defaultKataResources() *specs.LinuxResources { + dev := func(t string, major, minor int64, access string) specs.LinuxDeviceCgroup { + d := specs.LinuxDeviceCgroup{Allow: true, Type: t, Access: access} + if major != 0 { + d.Major = &major + } + if minor >= 0 { + d.Minor = &minor + } + return d + } + shares := uint64(1024) + return &specs.LinuxResources{ + Devices: []specs.LinuxDeviceCgroup{ + {Allow: false, Access: "rwm"}, + dev("c", 1, 3, "rwm"), // /dev/null + dev("c", 1, 8, "rwm"), // /dev/random + dev("c", 1, 7, "rwm"), // /dev/full + dev("c", 5, 0, "rwm"), // /dev/tty + dev("c", 1, 5, "rwm"), // /dev/zero + dev("c", 1, 9, "rwm"), // /dev/urandom + dev("c", 5, 1, "rwm"), // /dev/console + dev("c", 136, -1, "rwm"), // pts + dev("c", 5, 2, "rwm"), // /dev/ptmx + }, + CPU: &specs.LinuxCPU{Shares: &shares}, + } +} diff --git a/demos/counter/README.md b/demos/counter/README.md index 9cfe56eed..0c2b74fca 100644 --- a/demos/counter/README.md +++ b/demos/counter/README.md @@ -74,6 +74,34 @@ kubectl ate suspend actor my-counter-1 kubectl ate delete actor my-counter-1 ``` +## Micro-VM variant + +The same in-RAM-counter suspend/resume-continuity demo also runs on the micro-VM +sandbox class (`ateom-microvm`: a Kata guest on Cloud Hypervisor), proving that +the guest-memory snapshot round-trips just as gVisor's process snapshot does. + +- [`demos/counter/counter-microvm.yaml.tmpl`](counter-microvm.yaml.tmpl) — the + `WorkerPool` + `ActorTemplate` for the micro-VM sandbox class. +- [`hack/run-microvm-demo.sh`](../../hack/run-microvm-demo.sh) — one-shot bring-up + that builds the micro-VM worker image, stages the guest assets, deploys the + control plane, and applies the manifest above. Like the other hack scripts it + reads `.ate-dev-env.sh` for GKE; use the kind wrapper for a local cluster. + +Run it and follow the printed next steps: + +```bash +# GKE (uses .ate-dev-env.sh, uploads assets to GCS): +./hack/run-microvm-demo.sh + +# local kind (local registry + in-cluster rustfs): +KIND_CLUSTER_NAME= ./hack/run-microvm-demo-kind.sh +``` + +Then create an actor, increment the counter, suspend it, resume it (even on a +different worker), and confirm the count continues — the actor's counter lives in +guest RAM, so a continuing count proves the guest-memory snapshot survived the +round trip. + ## How to Uninstall To remove the counter demo resources from your cluster, run: diff --git a/demos/counter/counter-microvm.yaml.tmpl b/demos/counter/counter-microvm.yaml.tmpl new file mode 100644 index 000000000..e9f4e31e2 --- /dev/null +++ b/demos/counter/counter-microvm.yaml.tmpl @@ -0,0 +1,106 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Micro-VM (kata + cloud-hypervisor) variant of the counter demo. The counter is +# in-RAM (atomic uint64), so a successful suspend/resume across pods shows the +# count continuing — proving the guest memory snapshot round-tripped. +# +# The sandbox binaries (cloud-hypervisor, guest kernel, guest rootfs, base +# configuration.toml) live on a cluster-scoped SandboxConfig, FETCHED at runtime +# from the cluster object store bucket ${BUCKET_NAME} under kata-assets/ (rustfs on +# kind, GCS on GKE) — NOT baked into the worker image. ateom owns the +# cloud-hypervisor boot itself and gives the actor a writable virtio-blk rootfs, so +# neither the kata shim nor virtiofsd is needed. The per-arch sha256 values below +# are the asset sets produced by hack/microvm-assets/assemble.sh; atelet selects +# the block matching the node's architecture, and each cluster's bucket holds that +# arch's binaries at these paths (staged by run-microvm-demo[-kind].sh). + +apiVersion: v1 +kind: Namespace +metadata: + name: ate-demo-counter-microvm + +--- + +apiVersion: ate.dev/v1alpha1 +kind: SandboxConfig +metadata: + name: counter-microvm +spec: + sandboxClass: microvm + assets: + arm64: + cloud-hypervisor: + url: "gs://${BUCKET_NAME}/kata-assets/cloud-hypervisor" + sha256: "bf004ddc1a148f47caa87ac49a783b8dbd6bf9bc27abe522ed197df7b982d3b1" + kata-kernel: + url: "gs://${BUCKET_NAME}/kata-assets/vmlinux" + sha256: "a44d663f4ddad20a35527a3578fadef9beb23c1e5cb720e85d6928d6de70d3a1" + kata-image: + url: "gs://${BUCKET_NAME}/kata-assets/rootfs.img" + sha256: "7ebd652760c881374c0a761d34addcb76d9a650e35c10c01b780ebcdd9a1f2aa" + kata-config: + url: "gs://${BUCKET_NAME}/kata-assets/configuration-clh.toml" + sha256: "df504d9be0ed01765fdc8a9467955e1e671eb97724443f65a524bf914ccb818b" + amd64: + cloud-hypervisor: + url: "gs://${BUCKET_NAME}/kata-assets/cloud-hypervisor" + sha256: "829af01ff075bb96c4f183905134c453a88d68cbabdc6b87df21098842581ee9" + kata-kernel: + url: "gs://${BUCKET_NAME}/kata-assets/vmlinux" + sha256: "a5f0af5fe536cd52c3ca214d15d81c577e5c5dc672947ab7980b91ddcb7c9d71" + kata-image: + url: "gs://${BUCKET_NAME}/kata-assets/rootfs.img" + sha256: "ca9e06621b7edd2e056607c04db8bcebd92ad37ad4e37d18b8247d851feb0fae" + kata-config: + url: "gs://${BUCKET_NAME}/kata-assets/configuration-clh.toml" + sha256: "8cce580e5abf78c05c8e9b929c24a524b9a81fc47be4e2e4f38dcae5ef052be6" + +--- + +apiVersion: ate.dev/v1alpha1 +kind: WorkerPool +metadata: + name: counter-microvm + namespace: ate-demo-counter-microvm + labels: + workload: counter-microvm +spec: + replicas: 2 + sandboxClass: microvm + sandboxConfigName: counter-microvm + ateomImage: ko://github.com/agent-substrate/substrate/cmd/ateom-microvm + +--- + +apiVersion: ate.dev/v1alpha1 +kind: ActorTemplate +metadata: + name: counter-microvm + namespace: ate-demo-counter-microvm +spec: + # Must match the WorkerPool's sandboxClass: a snapshot is not portable across + # sandbox classes, so only pools of the same class are eligible to run this + # template's actors. + sandboxClass: microvm + pauseImage: "registry.k8s.io/pause:3.10.2@sha256:f548e0e8e3dc1896ca956272154dde3314e8cc4fde0a57577ee9fa1c63f5baf4" + containers: + - name: counter + image: ko://github.com/agent-substrate/substrate/demos/counter + command: ["/ko-app/counter"] + workerSelector: + matchLabels: + workload: counter-microvm + snapshotsConfig: + location: gs://${BUCKET_NAME}/ate-demo-counter-microvm/ diff --git a/docs/api-guide.md b/docs/api-guide.md index cdfa2c9c8..b4fbe6fca 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -176,6 +176,12 @@ spec: sha256: "1ba2366ae2efceba166046f51a4104f9261c9cb72c6db8f5b3fe2dc57dea86b9" ``` +### Micro-VM SandboxConfig + +A `microvm` `SandboxConfig` supplies the [Kata Containers](https://katacontainers.io/) + [Cloud Hypervisor](https://www.cloudhypervisor.org/) toolchain instead of `runsc`. Each architecture must define the full asset set — `kata-shim`, `cloud-hypervisor`, `virtiofsd`, `kata-kernel`, `kata-image`, and `kata-config` — which a `ValidatingAdmissionPolicy` enforces at apply time. Worker pods for a micro-VM pool require `/dev/kvm` and nested-virtualization-capable nodes labeled `ate.dev/sandboxClass=microvm` (the controller adds the device mount and node placement automatically). + +See [`hack/microvm-assets/`](../hack/microvm-assets/) for scripts that assemble and stage these assets, plus a worked counter demo (`demos/counter/counter-microvm.yaml.tmpl`) that suspends and resumes an in-RAM counter across worker pods. + --- ## 4. Operational Workflow diff --git a/docs/architecture.md b/docs/architecture.md index 52cc63da9..f13665f95 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -284,12 +284,20 @@ The node-level subsystem manages the physical execution of sandboxes and the mov * **atelet**: A lightweight supervisor running on each node as a DaemonSet. It acts as the "Herder," managing a pool of physical pods and communicating with the Control Plane. - * **ateom**: A specialized "interior gVisor" container image that runs inside the physical worker pods. It provides a gRPC interface for `atelet` to trigger `RunWorkload`, `CheckpointWorkload`, and `RestoreWorkload` operations. This separation ensures that the physical pod lifecycle remains decoupled from the sandboxed agent process. + * **ateom**: A specialized sandbox-herder container image — one per sandbox class (`ateom-gvisor`, `ateom-microvm`) — that runs inside the physical worker pods. It provides a gRPC interface for `atelet` to trigger `RunWorkload`, `CheckpointWorkload`, and `RestoreWorkload` operations. This separation ensures that the physical pod lifecycle remains decoupled from the sandboxed agent process. - * **Lifecycle Management**: The `ateom` process invokes the sandbox runtime (e.g., `runsc` for gVisor) to checkpoint or restore processes within the physical pod boundaries. (Note: Substrate currently requires a `runsc` version with the `--allow-connected-on-save` flag to work around a bug in networking resumption during checkpointing). + * **Lifecycle Management**: The `ateom` process invokes the sandbox runtime to checkpoint or restore processes within the physical pod boundaries — `runsc` for gVisor, or the Kata + Cloud Hypervisor stack for micro-VMs. (Note: the gVisor backend currently requires a `runsc` version with the `--allow-connected-on-save` flag to work around a bug in networking resumption during checkpointing.) * **Storage Mover**: The `atelet` streams snapshots to and from GCS/S3, ensuring process state is persistent and portable across the cluster. +### Sandbox Classes + +A `WorkerPool` selects a **sandbox class** (`spec.sandboxClass`), and each class has a matching `ateom` herder image. The sandbox binaries themselves are not baked into the worker image — they are fetched at runtime from a cluster-scoped [`SandboxConfig`](api-guide.md#3-sandboxconfig-sandbox-binaries) and pinned into each snapshot's manifest so restores stay reproducible across runtime upgrades. + + * **gVisor** (`ateom-gvisor`, the default): runs the workload under `runsc`. Suspend/resume uses gVisor's checkpoint/restore of the sandboxed process tree. + + * **micro-VM** (`ateom-microvm`): runs the workload inside a [Kata Containers](https://katacontainers.io/) guest (Kata 3.31 guest assets) on the [Cloud Hypervisor](https://www.cloudhypervisor.org/) VMM. `ateom` owns the Cloud Hypervisor boot directly — there is **no Kata shim and no containerd daemon**: it launches Cloud Hypervisor, boots the guest kernel + OS image, and then drives the Kata agent over its hybrid-vsock ttrpc API itself (creating the sandbox, configuring guest networking, and starting the container). The actor's container rootfs is a writable boot-time virtio-blk disk (`/dev/vdb`) that `ateom` builds with `mkfs.ext4` from the OCI bundle, so rootfs writes land off guest RAM on a host-backed disk. Suspend captures a Cloud Hypervisor **memory-only snapshot** of the running guest (no memory balloon); resume relaunches Cloud Hypervisor with its **OnDemand** (userfaultfd) memory restore — demand-paging from the snapshot while a diff-merge folds newly-faulted pages back in to keep the snapshot complete — so full in-RAM state comes back on any worker, including a different node. On each restore `/dev/vdb` is recreated byte-identical to the golden image (**reset-to-golden**), so rootfs writes are discarded across suspend/resume (matching gVisor's semantics) while in-RAM state persists. The actor container's stdout/stderr is forwarded to the pod log with `ate.dev/*` labels (parity with `ateom-gvisor`). Micro-VM workers require `/dev/kvm` and nested-virtualization-capable nodes; the controller adds the KVM device mount and pins these pods to nodes labeled `ate.dev/sandboxClass=microvm`. See [`hack/microvm-assets/`](../hack/microvm-assets/) for assembling the asset set. + ### Networking Stack (`atenet` + Envoy) Handles session-aware routing and automatic re-animation. diff --git a/docs/dev/code-layout.md b/docs/dev/code-layout.md index 96e2dc5df..1a6c7b8de 100644 --- a/docs/dev/code-layout.md +++ b/docs/dev/code-layout.md @@ -52,6 +52,7 @@ Each subdirectory of `cmd/` corresponds to one compiled binary: | `cmd/atelet` | Node supervisor (DaemonSet) | | `cmd/atenet` | Network proxy / Envoy external-processing server | | `cmd/ateom-gvisor` | In-pod gVisor container image entry point | +| `cmd/ateom-microvm` | In-pod kata + cloud-hypervisor micro-VM container image entry point | | `cmd/kubectl-ate` | `kubectl` plugin for interacting with Substrate | | `cmd/podcertcontroller` | Controller that issues pod TLS certificates | diff --git a/docs/roadmap.md b/docs/roadmap.md index 5043c39a3..b4e7bbee1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -68,7 +68,6 @@ Below is a collection of finer-grained efforts which we believe align with the a * Audit logging on API and lifecycle operations. * Sandbox integrations for threat detection telemetry. * Harden actor networking to further isolate from the surrounding node (e.g. with current networking -* Support for additional sandboxing technologies beyond gVisor, including at least one flavor of microVM. ### Observability diff --git a/go.mod b/go.mod index 16b41313f..1a7180ed2 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.17 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 + github.com/containerd/ttrpc v1.2.8 github.com/envoyproxy/go-control-plane v0.14.0 github.com/envoyproxy/go-control-plane/envoy v1.37.0 github.com/google/go-cmp v0.7.0 @@ -23,6 +24,7 @@ require ( github.com/hashicorp/go-reap v0.0.0-20260220095743-4e27870b4f51 github.com/klauspost/compress v1.18.5 github.com/opencontainers/runtime-spec v1.3.0 + github.com/pelletier/go-toml/v2 v2.4.0 github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.18.0 github.com/spf13/cobra v1.10.2 @@ -81,6 +83,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect diff --git a/go.sum b/go.sum index 2151f0ae3..ba6480951 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +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/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= +github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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= @@ -237,6 +241,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/pelletier/go-toml/v2 v2.4.0 h1:Mwu0mAkUKbittDs3/ADDWXqMmq3EOK2VHiuCkV00Row= +github.com/pelletier/go-toml/v2 v2.4.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= diff --git a/hack/ateom-base/Dockerfile b/hack/ateom-base/Dockerfile new file mode 100644 index 000000000..9ac88ca0d --- /dev/null +++ b/hack/ateom-base/Dockerfile @@ -0,0 +1,22 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM debian:stable-slim +# e2fsprogs provides mkfs.ext4, which the ateom-microvm worker uses to build the +# actor's writable /dev/vdb ext4 rootfs from the OCI bundle. debian-slim also +# provides coreutils (cp --sparse) and glibc for the fetched cloud-hypervisor binary. +RUN apt-get update \ + && apt-get install -y --no-install-recommends e2fsprogs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/hack/create-kind-cluster.sh b/hack/create-kind-cluster.sh index 2915a95a2..26548f2b4 100755 --- a/hack/create-kind-cluster.sh +++ b/hack/create-kind-cluster.sh @@ -42,13 +42,38 @@ if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true registry:3 fi -# 2. Create kind configuration with containerdConfigPatches and feature gates +# 2. Create kind configuration with containerdConfigPatches and feature gates. +# +# Probe for /dev/kvm where the kind nodes will actually run — inside the Docker +# provider VM on macOS (Lima/Colima/Docker Desktop), the host itself on Linux — +# and only wire up micro-VM (kata + cloud-hypervisor) support when present. +# Without KVM the cluster still works for gVisor. +echo "Probing for /dev/kvm in the Docker environment..." +HAS_KVM=0 +if docker run --rm --device /dev/kvm busybox true >/dev/null 2>&1; then + HAS_KVM=1 + echo "/dev/kvm found: micro-VM (kata + cloud-hypervisor) support will be enabled." +else + echo "/dev/kvm not available: micro-VM support disabled (gVisor still works)." +fi + echo "Creating kind configuration for cluster '${KIND_CLUSTER_NAME}'..." cat < "${ROOT}/bin/kind-config.yaml" kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane +EOF +if [ "${HAS_KVM}" = "1" ]; then + cat <> "${ROOT}/bin/kind-config.yaml" + # Bind-mount /dev/kvm into the node so micro-VM (kata + cloud-hypervisor) + # worker pods can use KVM. + extraMounts: + - hostPath: /dev/kvm + containerPath: /dev/kvm +EOF +fi +cat <> "${ROOT}/bin/kind-config.yaml" # cmd/podcertcontroller depends on ClusterTrustBundle & PodCertificateRequest. # They are not enabled by default as of Kubernetes v1.36 # https://github.com/kubernetes/kubernetes/blob/master/test/compatibility_lifecycle/reference/versioned_feature_list.yaml @@ -72,6 +97,16 @@ for node in $("${ROOT}"/hack/kind.sh get nodes --name "${KIND_CLUSTER_NAME}"); d docker exec "${node}" sysctl net.ipv4.conf.all.proxy_arp=1 done +# 2.6 When KVM is available: make /dev/kvm usable inside the node and label +# nodes so micro-VM WorkerPools (nodeSelector ate.dev/sandboxClass=microvm) schedule. +if [ "${HAS_KVM}" = "1" ]; then + echo "Preparing kind nodes for micro-VM (kata + cloud-hypervisor) runtime..." + for node in $("${ROOT}"/hack/kind.sh get nodes --name "${KIND_CLUSTER_NAME}"); do + docker exec "${node}" chmod 666 /dev/kvm + kubectl label node "${node}" ate.dev/sandboxClass=microvm --overwrite + done +fi + # 3. Add the registry config to the nodes echo "Adding registry config to kind nodes..." REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}" diff --git a/hack/microvm-assets/README.md b/hack/microvm-assets/README.md new file mode 100644 index 000000000..5a43e095c --- /dev/null +++ b/hack/microvm-assets/README.md @@ -0,0 +1,52 @@ +# Micro-VM runtime assets + counter demo (kind, fetch-not-bake) + +The `microvm` runtime (`cmd/ateom-microvm`, kata + cloud-hypervisor) fetches its +toolchain at runtime — nothing kata-specific is baked into the worker image. ateom owns +the cloud-hypervisor boot and gives the actor a writable virtio-blk rootfs, so neither the +kata shim nor virtiofsd is needed. The asset set is just four files: + +- `cloud-hypervisor` — the VMM binary (fetched from its release) +- `vmlinux` — the guest kernel (from kata-static) +- `rootfs.img` — the guest rootfs image (from kata-static) +- `configuration-clh.toml` — the base kata config (from kata-static) + +These helpers assemble the asset set for your node arch, stage it into the cluster's rustfs +S3 bucket, and the demo manifest's `SandboxConfig` points at it. When `/dev/kvm` is +available, `hack/create-kind-cluster.sh` mounts it into the node and labels the node +`ate.dev/sandboxClass=microvm`. + +> [!TIP] +> `hack/run-microvm-demo.sh` automates the full bring-up below (ateom-base image, ko base +> override, assets, control plane, demo apply) for kind OR GKE without editing committed +> files. The steps here are the manual equivalent. + +## Steps (run on a KVM-capable Linux host matching the node arch) + +1. **Assemble assets for your arch:** + ```sh + ARCH=arm64 hack/microvm-assets/assemble.sh + ``` + Copy the printed sha256 sums into the `SandboxConfig` `spec.assets` in + `demos/counter/counter-microvm.yaml.tmpl` (the committed values are arm64; other arches differ). + +2. **Bring up the cluster + control plane:** + ```sh + hack/create-kind-cluster.sh # mounts /dev/kvm, labels node ate.dev/sandboxClass=microvm + hack/install-ate-kind.sh # control plane + rustfs (bucket: ate-snapshots) + ``` + +3. **Stage assets into rustfs:** + ```sh + OUT="$PWD/microvm-assets-arm64" hack/microvm-assets/stage-to-rustfs.sh + ``` + +4. **Apply the demo + drive it:** + ```sh + BUCKET_NAME=ate-snapshots envsubst < demos/counter/counter-microvm.yaml.tmpl | kubectl apply -f - + ``` + Create an actor from `counter-microvm`, hit the in-RAM counter to increment it, suspend + (checkpoint), resume on a different worker pod, and confirm the count continues — proving the + guest-memory snapshot round-tripped across pods. + +## Notes +- `assets` is single-arch (unlike runsc's amd64/arm64): stage assets matching the node arch. diff --git a/hack/microvm-assets/assemble.sh b/hack/microvm-assets/assemble.sh new file mode 100755 index 000000000..e12a4d1b3 --- /dev/null +++ b/hack/microvm-assets/assemble.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Assemble the micro-VM (kata + cloud-hypervisor) runtime asset set that +# ateom-microvm fetches at runtime (fetch-not-bake). Run this on a Linux +# host of the TARGET arch. +# +# Produces, under $OUT, the four assets named as the SandboxConfig expects, plus +# their sha256 sums (paste into demos/counter/counter-microvm.yaml.tmpl): +# cloud-hypervisor vmlinux rootfs.img configuration-clh.toml +# +# ateom owns the cloud-hypervisor boot and gives the actor a writable virtio-blk +# rootfs, so neither the kata shim nor virtiofsd is part of the asset set. +# +# Env: ARCH (arm64|amd64, default arm64), KATA_VER (3.31.0), CH_VER (v52.0), +# OUT (default ./bin/microvm-assets/$ARCH, under the gitignored bin/). + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" + +ARCH="${ARCH:-arm64}" +KATA_VER="${KATA_VER:-3.31.0}" +CH_VER="${CH_VER:-v52.0}" +OUT="${OUT:-${ROOT}/bin/microvm-assets/$ARCH}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +case "$ARCH" in + arm64) CH_ASSET="cloud-hypervisor-static-aarch64" ;; + amd64) CH_ASSET="cloud-hypervisor-static" ;; + *) echo "unsupported ARCH=$ARCH" >&2; exit 1 ;; +esac + +mkdir -p "$OUT" +cd "$WORK" + +echo ">> Downloading kata-static ${KATA_VER} (${ARCH})..." +curl -fSL -o kata-static.tar.zst \ + "https://github.com/kata-containers/kata-containers/releases/download/${KATA_VER}/kata-static-${KATA_VER}-${ARCH}.tar.zst" +mkdir -p kata +tar --zstd -xf kata-static.tar.zst -C kata +KROOT="kata/opt/kata" + +cp "$(readlink -f "${KROOT}/share/kata-containers/vmlinux.container")" "${OUT}/vmlinux" +cp "$(readlink -f "${KROOT}/share/kata-containers/kata-containers.img")" "${OUT}/rootfs.img" +cp "${KROOT}/share/defaults/kata-containers/configuration-clh.toml" "${OUT}/configuration-clh.toml" + +echo ">> Downloading cloud-hypervisor ${CH_VER} (${CH_ASSET})..." +curl -fSL -o "${OUT}/cloud-hypervisor" \ + "https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/${CH_VER}/${CH_ASSET}" +chmod +x "${OUT}/cloud-hypervisor" + +echo +echo ">> Assets assembled in ${OUT}:" +cd "${OUT}" +for f in cloud-hypervisor vmlinux rootfs.img configuration-clh.toml; do + [ -f "$f" ] || { echo "MISSING: $f" >&2; exit 1; } +done +echo +echo ">> sha256 (paste into demos/counter/counter-microvm.yaml.tmpl runtime.assets):" +sha256sum cloud-hypervisor vmlinux rootfs.img configuration-clh.toml diff --git a/hack/microvm-assets/stage-to-gcs.sh b/hack/microvm-assets/stage-to-gcs.sh new file mode 100755 index 000000000..6ec9e5c12 --- /dev/null +++ b/hack/microvm-assets/stage-to-gcs.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stage the assembled micro-VM asset set into the GCS snapshot bucket under +# kata-assets/, where atelet fetches it (per demos/counter/counter-microvm.yaml.tmpl). +# The GKE counterpart of stage-to-rustfs.sh. Run after assemble.sh has produced $OUT. +# +# Requires the `gcloud` CLI authenticated for the bucket's project. Env: OUT (asset +# dir, default ./bin/microvm-assets/amd64), BUCKET (default ate-snapshots), +# PROJECT_ID (optional; passed to gcloud as --project when set). + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" + +OUT="${OUT:-${ROOT}/bin/microvm-assets/amd64}" +BUCKET="${BUCKET:-ate-snapshots}" + +# Pass --project only when PROJECT_ID is set (mirrors hack/teardown.sh); otherwise +# gcloud uses its active config project. ${PROJECT_ID:+...} elides the flag entirely +# when unset (same idiom as KUBECTL_CONTEXT in hack/run-microvm-demo.sh). +echo ">> Uploading assets to gs://${BUCKET}/kata-assets/ ..." +for f in cloud-hypervisor vmlinux rootfs.img configuration-clh.toml; do + echo " $f" + gcloud storage cp ${PROJECT_ID:+--project="${PROJECT_ID}"} "${OUT}/${f}" "gs://${BUCKET}/kata-assets/${f}" +done + +echo ">> Done. Verify:" +gcloud storage ls ${PROJECT_ID:+--project="${PROJECT_ID}"} "gs://${BUCKET}/kata-assets/" diff --git a/hack/microvm-assets/stage-to-rustfs.sh b/hack/microvm-assets/stage-to-rustfs.sh new file mode 100755 index 000000000..3f705dcfc --- /dev/null +++ b/hack/microvm-assets/stage-to-rustfs.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stage the assembled micro-VM asset set into the kind cluster's rustfs S3 bucket +# under kata-assets/, where atelet fetches it (per demos/counter/counter-microvm.yaml.tmpl). +# Run after the cluster is up (hack/install-ate-kind.sh) and assemble.sh has produced $OUT. +# +# Requires the `aws` CLI. Env: OUT (asset dir, default ./bin/microvm-assets/arm64), +# BUCKET (default ate-snapshots), NAMESPACE (rustfs namespace, default ate-system), +# KUBECTL_CONTEXT (optional; kube context for port-forward). + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" + +OUT="${OUT:-${ROOT}/bin/microvm-assets/arm64}" +BUCKET="${BUCKET:-ate-snapshots}" +NAMESPACE="${NAMESPACE:-ate-system}" +KUBECTL_CONTEXT="${KUBECTL_CONTEXT:-}" + +if ! command -v aws >/dev/null 2>&1; then + echo "error: the 'aws' CLI is required but was not found in PATH" >&2 + exit 1 +fi + +export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-rustfsadmin}" +export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-rustfsadmin}" +export AWS_REGION="${AWS_REGION:-us-east-1}" + +echo ">> Port-forwarding svc/rustfs 9000 in namespace ${NAMESPACE}..." +kubectl ${KUBECTL_CONTEXT:+--context="${KUBECTL_CONTEXT}"} -n "${NAMESPACE}" port-forward svc/rustfs 9000:9000 >/tmp/rustfs-pf.log 2>&1 & +PF_PID=$! +trap 'kill "$PF_PID" 2>/dev/null || true' EXIT +sleep 3 + +ENDPOINT="http://localhost:9000" +echo ">> Uploading assets to s3://${BUCKET}/kata-assets/ via ${ENDPOINT}..." +for f in cloud-hypervisor vmlinux rootfs.img configuration-clh.toml; do + echo " $f" + aws --endpoint-url "${ENDPOINT}" s3 cp "${OUT}/${f}" "s3://${BUCKET}/kata-assets/${f}" +done + +echo ">> Done. Verify:" +aws --endpoint-url "${ENDPOINT}" s3 ls "s3://${BUCKET}/kata-assets/" diff --git a/hack/run-microvm-demo-kind.sh b/hack/run-microvm-demo-kind.sh new file mode 100755 index 000000000..683e6d8d0 --- /dev/null +++ b/hack/run-microvm-demo-kind.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "${ROOT}" + +# Bring up the counter-microvm demo on a local kind cluster. Wraps +# hack/run-microvm-demo.sh with the same kind-specific environment that +# hack/install-ate-kind.sh uses, so the image repo and snapshot bucket match what +# gets installed in the cluster. All arguments are forwarded to run-microvm-demo.sh. + +# shellcheck disable=SC2155 # safe initialization +goarch=$(go env GOARCH) + +# override reading dev env -- otherwise .ate-dev-env.sh would point images/assets +# at GKE/GCS instead of the local registry and the in-cluster rustfs. +export NO_DEV_ENV="true" +# images are pushed to the local registry +export KO_DOCKER_REPO="${KO_DOCKER_REPO:-localhost:5001}" +# build for the host architecture +export KO_DEFAULTPLATFORMS="linux/${goarch}" +# use the kind control-plane path (install-ate-kind.sh) + stage assets to rustfs +export ATE_INSTALL_KIND="true" +# default bucket name for local deployment (served by the in-cluster rustfs) +export BUCKET_NAME="${BUCKET_NAME:-ate-snapshots}" +# target the local kind cluster's context +KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}" +export KUBECTL_CONTEXT="${KUBECTL_CONTEXT:-kind-${KIND_CLUSTER_NAME}}" +# unset other env from ate-dev-env.sh in case the developer already sourced them +unset GCE_REGION CLUSTER_LOCATION NETWORK SUBNETWORK MEMORYSTORE_INSTANCE PROJECT_ID + +exec "${ROOT}/hack/run-microvm-demo.sh" "$@" diff --git a/hack/run-microvm-demo.sh b/hack/run-microvm-demo.sh new file mode 100755 index 000000000..2ad3009c1 --- /dev/null +++ b/hack/run-microvm-demo.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# One-shot bring-up of the counter-microvm demo. GKE/dev-env by default; for a +# local kind cluster use hack/run-microvm-demo-kind.sh (which sets the kind env +# and calls this script), mirroring install-ate.sh / install-ate-kind.sh. +# +# Like the other hack scripts, this sources .ate-dev-env.sh for the cluster / +# registry / bucket settings unless NO_DEV_ENV is set. +# +# The committed .ko.yaml base for cmd/ateom-microvm is debian:stable-slim, which +# lacks mkfs.ext4 (e2fsprogs). The worker needs mkfs.ext4 at runtime to build the +# actor's virtio-blk rootfs, so this script builds hack/ateom-base (debian-slim + +# e2fsprogs) and overrides ONLY that base at build time via a throwaway ko config +# pointed at by KO_CONFIG_PATH — the committed .ko.yaml is never touched. +# +# Env (most come from .ate-dev-env.sh): +# KO_DOCKER_REPO (required) image registry, e.g. gcr.io/PROJECT/ate-images for +# GKE or localhost:5001 for kind. +# BUCKET_NAME object store bucket for assets/snapshots (default: ate-snapshots). +# KUBECTL_CONTEXT (optional) kube context; threaded into install + ko apply + kubectl. +# PROJECT_ID (optional) GCP project for the GCS asset upload (GKE path). +# ARCH target arch (default: from KO_DEFAULTPLATFORMS, else host arch). +# ATEOM_BASE_TAG tag for the built ateom-base image (default: e2fsprogs). +# OUT asset dir (default: $PWD/bin/microvm-assets/$ARCH, gitignored). +# ATE_INSTALL_KIND "true" for the kind path (stage assets to rustfs + install-ate-kind.sh); +# default false uploads assets to GCS + uses install-ate.sh. + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "${ROOT}" + +# Source the environment (cluster, registry, bucket) like the other hack scripts; +# hack/run-microvm-demo-kind.sh sets NO_DEV_ENV to skip this and use kind defaults. +if [[ -r .ate-dev-env.sh ]] && [[ -z "${NO_DEV_ENV:-}" ]]; then + source .ate-dev-env.sh +fi + +# --- env / defaults --------------------------------------------------------- +KO_DOCKER_REPO="${KO_DOCKER_REPO:-}" +KUBECTL_CONTEXT="${KUBECTL_CONTEXT:-}" +BUCKET_NAME="${BUCKET_NAME:-ate-snapshots}" +ATEOM_BASE_TAG="${ATEOM_BASE_TAG:-e2fsprogs}" +ATE_INSTALL_KIND="${ATE_INSTALL_KIND:-false}" + +# Target arch: match the images' platform (KO_DEFAULTPLATFORMS is set by +# .ate-dev-env.sh on GKE and by the kind wrapper); fall back to the host arch. +if [[ -z "${ARCH:-}" ]]; then + if [[ -n "${KO_DEFAULTPLATFORMS:-}" ]]; then + ARCH="${KO_DEFAULTPLATFORMS##*/}" + else + ARCH="$(go env GOARCH)" + fi +fi +OUT="${OUT:-${ROOT}/bin/microvm-assets/$ARCH}" + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Error: KO_DOCKER_REPO is required (set it in .ate-dev-env.sh for GKE," >&2 + echo " or use hack/run-microvm-demo-kind.sh for a local kind cluster)." >&2 + exit 1 +fi +export KO_DOCKER_REPO + +# ANSI color codes for prettier output (mirrors hack/install-ate.sh). +COLOR_CYAN='\033[1;36m' +COLOR_RESET='\033[0m' +log() { + echo -e "${COLOR_CYAN}[run-microvm-demo]: $*${COLOR_RESET}" +} + +ATEOM_BASE_IMAGE="${KO_DOCKER_REPO}/ateom-base:${ATEOM_BASE_TAG}" + +# --- 2. build + push ateom-base (debian-slim + e2fsprogs) for the target arch - +# We build with buildx --load (import into the local docker daemon) and then +# `docker push`, NOT buildx --push: the buildkit builder runs in its own container +# and cannot reach a localhost/kind registry, whereas the docker daemon can. --load +# imports a single-platform image fine even when ARCH != the host arch. For a real +# remote registry (e.g. gcr.io) the same daemon `docker push` works with its creds. +log "Building ateom-base ${ATEOM_BASE_IMAGE} (linux/${ARCH})..." +if docker buildx version >/dev/null 2>&1; then + log " using: docker buildx build --load + docker push" + docker buildx build --platform "linux/${ARCH}" -t "${ATEOM_BASE_IMAGE}" --load hack/ateom-base +else + log " using: docker build + docker push (buildx unavailable)" + docker build -t "${ATEOM_BASE_IMAGE}" hack/ateom-base +fi +docker push "${ATEOM_BASE_IMAGE}" + +# --- 3. throwaway ko config overriding ONLY the ateom-microvm base ----------- +# KO_CONFIG_PATH points at a FILE that ko parses by extension, so it MUST end in +# .yaml (a bare mktemp file is rejected: "Unsupported Config Type"). Use a temp dir +# with a .yaml-named copy of the repo .ko.yaml and swap the one base line. +KO_CONFIG_DIR="$(mktemp -d)" +KO_CONFIG_TMP="${KO_CONFIG_DIR}/ko-override.yaml" +trap 'rm -rf "${KO_CONFIG_DIR}"' EXIT +cp "${ROOT}/.ko.yaml" "${KO_CONFIG_TMP}" + +OVERRIDE_KEY="github.com/agent-substrate/substrate/cmd/ateom-microvm" +if ! grep -q "^ ${OVERRIDE_KEY}:" "${KO_CONFIG_TMP}"; then + echo "Error: could not find the cmd/ateom-microvm baseImageOverride line in .ko.yaml" >&2 + exit 1 +fi +# Replace only the value after the key (use | as the sed delimiter; the value has /). +sed -i.bak "s|^ ${OVERRIDE_KEY}:.*| ${OVERRIDE_KEY}: ${ATEOM_BASE_IMAGE}|" "${KO_CONFIG_TMP}" +rm -f "${KO_CONFIG_TMP}.bak" +export KO_CONFIG_PATH="${KO_CONFIG_TMP}" +log "Using throwaway KO_CONFIG_PATH=${KO_CONFIG_PATH} (ateom-microvm base -> ${ATEOM_BASE_IMAGE})" + +# --- 4. assets: assemble (if missing) then stage to rustfs (kind) / GCS (GKE) -- +need_assemble=false +for f in cloud-hypervisor vmlinux rootfs.img configuration-clh.toml; do + if [[ ! -f "${OUT}/${f}" ]]; then + need_assemble=true + break + fi +done +if [[ "${need_assemble}" == "true" ]]; then + log "Assembling micro-VM assets into ${OUT} (ARCH=${ARCH})..." + ARCH="${ARCH}" OUT="${OUT}" hack/microvm-assets/assemble.sh +else + log "Assets already present in ${OUT}; skipping assemble." +fi + +# Upload the four assets under kata-assets/, where atelet fetches them: the +# in-cluster rustfs (port-forwarded, S3 API) on kind, or the GCS bucket on GKE. +if [[ "${ATE_INSTALL_KIND}" == "true" ]]; then + log "Staging assets to in-cluster rustfs bucket ${BUCKET_NAME} (kata-assets/)..." + OUT="${OUT}" BUCKET="${BUCKET_NAME}" KUBECTL_CONTEXT="${KUBECTL_CONTEXT}" hack/microvm-assets/stage-to-rustfs.sh +else + log "Uploading assets to gs://${BUCKET_NAME}/kata-assets/ ..." + OUT="${OUT}" BUCKET="${BUCKET_NAME}" hack/microvm-assets/stage-to-gcs.sh +fi + +# --- 5. deploy the control plane -------------------------------------------- +log "Deploying the ate control plane (--deploy-ate-system)..." +if [[ "${ATE_INSTALL_KIND}" == "true" ]]; then + # install-ate-kind.sh sets NO_DEV_ENV/KO_DOCKER_REPO/ARCH/ATE_INSTALL_KIND itself. + KUBECTL_CONTEXT="${KUBECTL_CONTEXT}" hack/install-ate-kind.sh --deploy-ate-system +else + # GKE path: pass KO_DOCKER_REPO/BUCKET_NAME/KUBECTL_CONTEXT through the env. + KUBECTL_CONTEXT="${KUBECTL_CONTEXT}" hack/install-ate.sh --deploy-ate-system +fi + +# --- 6. apply the demo ------------------------------------------------------ +# Use ./hack/run-tool.sh ko so ko honors KO_CONFIG_PATH + KO_DOCKER_REPO. Only +# ko apply/create/delete/run accept args after `--`; thread --context there +# (mirrors the run_ko helper in hack/install-ate.sh). +log "Applying the counter-microvm demo manifest..." +sed "s|\${BUCKET_NAME}|${BUCKET_NAME}|g" demos/counter/counter-microvm.yaml.tmpl \ + | ./hack/run-tool.sh ko apply -f - ${KUBECTL_CONTEXT:+-- --context="${KUBECTL_CONTEXT}"} + +# --- 7. next steps ---------------------------------------------------------- +KCTX_FLAG="" +if [[ -n "${KUBECTL_CONTEXT}" ]]; then + KCTX_FLAG=" --context=${KUBECTL_CONTEXT}" +fi +log "Demo applied. Next steps:" +cat < ateom.WorkloadSpec - 2, // 1: ateom.WorkloadSpec.containers:type_name -> ateom.Container - 1, // 2: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 1, // 3: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 0, // 4: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 4, // 5: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 6, // 6: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 3, // 7: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 5, // 8: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 7, // 9: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 1, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 8, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + 2, // 2: ateom.WorkloadSpec.containers:type_name -> ateom.Container + 1, // 3: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 9, // 4: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + 1, // 5: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 10, // 6: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + 0, // 7: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest + 4, // 8: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest + 6, // 9: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest + 3, // 10: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse + 5, // 11: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse + 7, // 12: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse + 10, // [10:13] is the sub-list for method output_type + 7, // [7:10] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_ateom_proto_init() } @@ -579,7 +641,7 @@ func file_ateom_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateom_proto_rawDesc), len(file_ateom_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index 1da19e5a3..b367bec78 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -55,6 +55,12 @@ message RunWorkloadRequest { string runsc_path = 4; WorkloadSpec spec = 5; + + // runtime_asset_paths maps a runtime asset name (e.g. "cloud-hypervisor", + // "virtiofsd", "kata-kernel", "kata-image", "kata-config") + // to the local on-disk path atelet fetched it to (content-addressed, like + // runsc_path). Empty for the gVisor runtime, which uses runsc_path. + map runtime_asset_paths = 7; } // WorkloadSpec parallels Pod, but with far fewer configurable fields. @@ -87,10 +93,17 @@ message CheckpointWorkloadRequest { // // For example: "gs://bucket/actors/1234/snapshots/5678/" string snapshot_uri_prefix = 6; + + // runtime_asset_paths maps a runtime asset name to the local on-disk path + // atelet fetched it to (see RunWorkloadRequest). Empty for gVisor. + map runtime_asset_paths = 7; } message CheckpointWorkloadResponse { - + // snapshot_files lists the files ateom wrote into the checkpoint directory + // (relative names) for atelet to ship to object storage. Each runtime reports + // its own set (gVisor's image files, cloud-hypervisor's snapshot set, ...). + repeated string snapshot_files = 1; } message RestoreWorkloadRequest { @@ -104,6 +117,10 @@ message RestoreWorkloadRequest { // The object storage URI prefix of the snapshot to restore. string snapshot_uri_prefix = 6; + + // runtime_asset_paths maps a runtime asset name to the local on-disk path + // atelet fetched it to (see RunWorkloadRequest). Empty for gVisor. + map runtime_asset_paths = 7; } message RestoreWorkloadResponse { diff --git a/internal/serverboot/serverboot.go b/internal/serverboot/serverboot.go index ad42ec4d5..5c46b8fb4 100644 --- a/internal/serverboot/serverboot.go +++ b/internal/serverboot/serverboot.go @@ -22,6 +22,7 @@ import ( "context" "errors" "fmt" + "io" "log/slog" "net/http" "os" @@ -41,9 +42,16 @@ import ( ) // InitLogger sets the global slog logger to a JSON handler wrapped in -// contextlogging.NewHandler. Call once at process start. +// contextlogging.NewHandler, writing to os.Stdout. Call once at process start. func InitLogger() { - slog.SetDefault(slog.New(contextlogging.NewHandler(slog.NewJSONHandler(os.Stdout, nil)))) + InitLoggerWithWriter(os.Stdout) +} + +// InitLoggerWithWriter is InitLogger with an explicit destination. Use it to share +// one synchronized writer between the runtime logger and a separate writer (e.g. +// ateom's actor-log forwarder) so their lines don't interleave. +func InitLoggerWithWriter(w io.Writer) { + slog.SetDefault(slog.New(contextlogging.NewHandler(slog.NewJSONHandler(w, nil)))) } // serviceInstanceID is generated once so the tracer and meter resources share it. diff --git a/manifests/ate-install/sandboxconfig-validation.yaml b/manifests/ate-install/sandboxconfig-validation.yaml index 9613a9eae..7e96857f8 100644 --- a/manifests/ate-install/sandboxconfig-validation.yaml +++ b/manifests/ate-install/sandboxconfig-validation.yaml @@ -30,12 +30,20 @@ spec: resources: ["sandboxconfigs"] validations: # gVisor needs a "runsc" asset for every architecture it advertises. - # microvm requirements are enforced separately by the micro-VM runtime. - expression: >- object.spec.sandboxClass != 'gvisor' || (has(object.spec.assets) && size(object.spec.assets) > 0 && object.spec.assets.all(arch, 'runsc' in object.spec.assets[arch])) message: "a gvisor SandboxConfig must define a 'runsc' asset for every architecture under spec.assets" + # The micro-VM (cloud-hypervisor) runtime needs its asset set for every + # architecture it advertises. + - expression: >- + object.spec.sandboxClass != 'microvm' || + (has(object.spec.assets) && size(object.spec.assets) > 0 && + object.spec.assets.all(arch, + ['cloud-hypervisor', 'kata-kernel', 'kata-image', 'kata-config'] + .all(name, name in object.spec.assets[arch]))) + message: "a microvm SandboxConfig must define cloud-hypervisor, kata-kernel, kata-image, and kata-config assets for every architecture under spec.assets" --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding diff --git a/pkg/api/v1alpha1/sandboxconfig_validation_test.go b/pkg/api/v1alpha1/sandboxconfig_validation_test.go index 632c55263..3bb28dbba 100644 --- a/pkg/api/v1alpha1/sandboxconfig_validation_test.go +++ b/pkg/api/v1alpha1/sandboxconfig_validation_test.go @@ -47,6 +47,20 @@ func sandboxConfig(name string, class SandboxClass, assets map[string]map[string func runscAsset() AssetFile { return AssetFile{URL: "gs://bucket/runsc", SHA256: validSHA256} } +// microVMAssets returns a full, valid micro-VM asset set for one architecture: +// the four assets the policy requires. ateom owns the cloud-hypervisor boot and +// gives the actor a writable virtio-blk rootfs, so the set has no kata-shim or +// virtiofsd. +func microVMAssets() map[string]AssetFile { + a := AssetFile{URL: "gs://bucket/asset", SHA256: validSHA256} + return map[string]AssetFile{ + "cloud-hypervisor": a, + "kata-kernel": a, + "kata-image": a, + "kata-config": a, + } +} + // applyVAP installs the shipped ValidatingAdmissionPolicy + binding into the // envtest API server and waits for the apiserver to actually enforce it (policy // activation is asynchronous), confirmed by a sentinel that must be denied. @@ -103,9 +117,22 @@ func TestSandboxConfigValidation(t *testing.T) { sc: sandboxConfig("ok-gvisor", SandboxClassGvisor, map[string]map[string]AssetFile{"amd64": {"runsc": runscAsset()}, "arm64": {"runsc": runscAsset()}}), wantErr: false, }, { - name: "microvm is unconstrained by the gvisor rule", - sc: sandboxConfig("ok-microvm", "microvm", map[string]map[string]AssetFile{"amd64": {"cloud-hypervisor": runscAsset()}}), + name: "valid microvm with full asset set", + sc: sandboxConfig("ok-microvm", "microvm", map[string]map[string]AssetFile{"amd64": microVMAssets()}), wantErr: false, + }, { + name: "valid microvm arm64 asset set", + sc: sandboxConfig("ok-microvm-arm64", "microvm", map[string]map[string]AssetFile{"arm64": microVMAssets()}), + wantErr: false, + }, { + name: "microvm missing an asset", + sc: sandboxConfig("bad-microvm", "microvm", map[string]map[string]AssetFile{"amd64": func() map[string]AssetFile { + m := microVMAssets() + delete(m, "kata-image") + return m + }()}), + wantErr: true, + errMsg: "microvm SandboxConfig must define", }, { name: "gvisor arch missing runsc", sc: sandboxConfig("bad-no-runsc", SandboxClassGvisor, map[string]map[string]AssetFile{"amd64": {"notrunsc": runscAsset()}}), diff --git a/vendor/github.com/containerd/log/.golangci.yml b/vendor/github.com/containerd/log/.golangci.yml new file mode 100644 index 000000000..a695775df --- /dev/null +++ b/vendor/github.com/containerd/log/.golangci.yml @@ -0,0 +1,30 @@ +linters: + enable: + - exportloopref # Checks for pointers to enclosing loop variables + - gofmt + - goimports + - gosec + - ineffassign + - misspell + - nolintlint + - revive + - staticcheck + - tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17 + - unconvert + - unused + - vet + - dupword # Checks for duplicate words in the source code + disable: + - errcheck + +run: + timeout: 5m + skip-dirs: + - api + - cluster + - design + - docs + - docs/man + - releases + - reports + - test # e2e scripts diff --git a/vendor/github.com/containerd/log/LICENSE b/vendor/github.com/containerd/log/LICENSE new file mode 100644 index 000000000..584149b6e --- /dev/null +++ b/vendor/github.com/containerd/log/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright The containerd Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/containerd/log/README.md b/vendor/github.com/containerd/log/README.md new file mode 100644 index 000000000..00e084988 --- /dev/null +++ b/vendor/github.com/containerd/log/README.md @@ -0,0 +1,17 @@ +# log + +A Go package providing a common logging interface across containerd repositories and a way for clients to use and configure logging in containerd packages. + +This package is not intended to be used as a standalone logging package outside of the containerd ecosystem and is intended as an interface wrapper around a logging implementation. +In the future this package may be replaced with a common go logging interface. + +## Project details + +**log** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). +As a containerd sub-project, you will find the: + * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), + * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), + * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) + +information in our [`containerd/project`](https://github.com/containerd/project) repository. + diff --git a/vendor/github.com/containerd/log/context.go b/vendor/github.com/containerd/log/context.go new file mode 100644 index 000000000..20153066f --- /dev/null +++ b/vendor/github.com/containerd/log/context.go @@ -0,0 +1,182 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package log provides types and functions related to logging, passing +// loggers through a context, and attaching context to the logger. +// +// # Transitional types +// +// This package contains various types that are aliases for types in [logrus]. +// These aliases are intended for transitioning away from hard-coding logrus +// as logging implementation. Consumers of this package are encouraged to use +// the type-aliases from this package instead of directly using their logrus +// equivalent. +// +// The intent is to replace these aliases with locally defined types and +// interfaces once all consumers are no longer directly importing logrus +// types. +// +// IMPORTANT: due to the transitional purpose of this package, it is not +// guaranteed for the full logrus API to be provided in the future. As +// outlined, these aliases are provided as a step to transition away from +// a specific implementation which, as a result, exposes the full logrus API. +// While no decisions have been made on the ultimate design and interface +// provided by this package, we do not expect carrying "less common" features. +package log + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" +) + +// G is a shorthand for [GetLogger]. +// +// We may want to define this locally to a package to get package tagged log +// messages. +var G = GetLogger + +// L is an alias for the standard logger. +var L = &Entry{ + Logger: logrus.StandardLogger(), + // Default is three fields plus a little extra room. + Data: make(Fields, 6), +} + +type loggerKey struct{} + +// Fields type to pass to "WithFields". +type Fields = map[string]any + +// Entry is a logging entry. It contains all the fields passed with +// [Entry.WithFields]. It's finally logged when Trace, Debug, Info, Warn, +// Error, Fatal or Panic is called on it. These objects can be reused and +// passed around as much as you wish to avoid field duplication. +// +// Entry is a transitional type, and currently an alias for [logrus.Entry]. +type Entry = logrus.Entry + +// RFC3339NanoFixed is [time.RFC3339Nano] with nanoseconds padded using +// zeros to ensure the formatted time is always the same number of +// characters. +const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" + +// Level is a logging level. +type Level = logrus.Level + +// Supported log levels. +const ( + // TraceLevel level. Designates finer-grained informational events + // than [DebugLevel]. + TraceLevel Level = logrus.TraceLevel + + // DebugLevel level. Usually only enabled when debugging. Very verbose + // logging. + DebugLevel Level = logrus.DebugLevel + + // InfoLevel level. General operational entries about what's going on + // inside the application. + InfoLevel Level = logrus.InfoLevel + + // WarnLevel level. Non-critical entries that deserve eyes. + WarnLevel Level = logrus.WarnLevel + + // ErrorLevel level. Logs errors that should definitely be noted. + // Commonly used for hooks to send errors to an error tracking service. + ErrorLevel Level = logrus.ErrorLevel + + // FatalLevel level. Logs and then calls "logger.Exit(1)". It exits + // even if the logging level is set to Panic. + FatalLevel Level = logrus.FatalLevel + + // PanicLevel level. This is the highest level of severity. Logs and + // then calls panic with the message passed to Debug, Info, ... + PanicLevel Level = logrus.PanicLevel +) + +// SetLevel sets log level globally. It returns an error if the given +// level is not supported. +// +// level can be one of: +// +// - "trace" ([TraceLevel]) +// - "debug" ([DebugLevel]) +// - "info" ([InfoLevel]) +// - "warn" ([WarnLevel]) +// - "error" ([ErrorLevel]) +// - "fatal" ([FatalLevel]) +// - "panic" ([PanicLevel]) +func SetLevel(level string) error { + lvl, err := logrus.ParseLevel(level) + if err != nil { + return err + } + + L.Logger.SetLevel(lvl) + return nil +} + +// GetLevel returns the current log level. +func GetLevel() Level { + return L.Logger.GetLevel() +} + +// OutputFormat specifies a log output format. +type OutputFormat string + +// Supported log output formats. +const ( + // TextFormat represents the text logging format. + TextFormat OutputFormat = "text" + + // JSONFormat represents the JSON logging format. + JSONFormat OutputFormat = "json" +) + +// SetFormat sets the log output format ([TextFormat] or [JSONFormat]). +func SetFormat(format OutputFormat) error { + switch format { + case TextFormat: + L.Logger.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: RFC3339NanoFixed, + FullTimestamp: true, + }) + return nil + case JSONFormat: + L.Logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: RFC3339NanoFixed, + }) + return nil + default: + return fmt.Errorf("unknown log format: %s", format) + } +} + +// WithLogger returns a new context with the provided logger. Use in +// combination with logger.WithField(s) for great effect. +func WithLogger(ctx context.Context, logger *Entry) context.Context { + return context.WithValue(ctx, loggerKey{}, logger.WithContext(ctx)) +} + +// GetLogger retrieves the current logger from the context. If no logger is +// available, the default logger is returned. +func GetLogger(ctx context.Context) *Entry { + if logger := ctx.Value(loggerKey{}); logger != nil { + return logger.(*Entry) + } + return L.WithContext(ctx) +} diff --git a/vendor/github.com/containerd/ttrpc/.gitattributes b/vendor/github.com/containerd/ttrpc/.gitattributes new file mode 100644 index 000000000..d207b1802 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/containerd/ttrpc/.gitignore b/vendor/github.com/containerd/ttrpc/.gitignore new file mode 100644 index 000000000..88ceb2764 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +/bin/ +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage.txt diff --git a/vendor/github.com/containerd/ttrpc/.golangci.yml b/vendor/github.com/containerd/ttrpc/.golangci.yml new file mode 100644 index 000000000..ef1a7d963 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/.golangci.yml @@ -0,0 +1,56 @@ +version: "2" +linters: + enable: + - misspell + - revive + - unconvert + disable: + - errcheck + settings: + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + arguments: + - - UID + - GID + - [] + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - example +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - example diff --git a/vendor/github.com/containerd/ttrpc/LICENSE b/vendor/github.com/containerd/ttrpc/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/containerd/ttrpc/Makefile b/vendor/github.com/containerd/ttrpc/Makefile new file mode 100644 index 000000000..c3a497dca --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/Makefile @@ -0,0 +1,180 @@ +# Copyright The containerd Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Go command to use for build +GO ?= go +INSTALL ?= install + +# Root directory of the project (absolute path). +ROOTDIR=$(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +WHALE = "🇩" +ONI = "👹" + +# Project binaries. +COMMANDS=protoc-gen-go-ttrpc protoc-gen-gogottrpc + +ifdef BUILDTAGS + GO_BUILDTAGS = ${BUILDTAGS} +endif +GO_BUILDTAGS ?= +GO_TAGS=$(if $(GO_BUILDTAGS),-tags "$(strip $(GO_BUILDTAGS))",) + +# Project packages. +PACKAGES=$(shell $(GO) list ${GO_TAGS} ./... | grep -v /example) +TESTPACKAGES=$(shell $(GO) list ${GO_TAGS} ./... | grep -v /cmd | grep -v /integration | grep -v /example) +BINPACKAGES=$(addprefix ./cmd/,$(COMMANDS)) + +#Replaces ":" (*nix), ";" (windows) with newline for easy parsing +GOPATHS=$(shell echo ${GOPATH} | tr ":" "\n" | tr ";" "\n") + +TESTFLAGS_RACE= +GO_BUILD_FLAGS= +# See Golang issue re: '-trimpath': https://github.com/golang/go/issues/13809 +GO_GCFLAGS=$(shell \ + set -- ${GOPATHS}; \ + echo "-gcflags=-trimpath=$${1}/src"; \ + ) + +BINARIES=$(addprefix bin/,$(COMMANDS)) + +# Flags passed to `go test` +TESTFLAGS ?= $(TESTFLAGS_RACE) $(EXTRA_TESTFLAGS) +TESTFLAGS_PARALLEL ?= 8 + +# Use this to replace `go test` with, for instance, `gotestsum` +GOTEST ?= $(GO) test + +.PHONY: clean all AUTHORS build binaries test integration generate protos check-protos coverage ci check help install vendor install-protobuf install-protobuild +.DEFAULT: default + +# Forcibly set the default goal to all, in case an include above brought in a rule definition. +.DEFAULT_GOAL := all + +all: binaries + +check: proto-fmt ## run all linters + @echo "$(WHALE) $@" + GOGC=75 golangci-lint run + +ci: check binaries check-protos coverage # coverage-integration ## to be used by the CI + +AUTHORS: .mailmap .git/HEAD + git log --format='%aN <%aE>' | sort -fu > $@ + +generate: protos + @echo "$(WHALE) $@" + @PATH="${ROOTDIR}/bin:${PATH}" $(GO) generate -x ${PACKAGES} + +protos: bin/protoc-gen-gogottrpc bin/protoc-gen-go-ttrpc ## generate protobuf + @echo "$(WHALE) $@" + @(PATH="${ROOTDIR}/bin:${PATH}" protobuild --quiet ${PACKAGES}) + +check-protos: protos ## check if protobufs needs to be generated again + @echo "$(WHALE) $@" + @test -z "$$(git status --short | grep ".pb.go" | tee /dev/stderr)" || \ + ((git diff | cat) && \ + (echo "$(ONI) please run 'make protos' when making changes to proto files" && false)) + +check-api-descriptors: protos ## check that protobuf changes aren't present. + @echo "$(WHALE) $@" + @test -z "$$(git status --short | grep ".pb.txt" | tee /dev/stderr)" || \ + ((git diff $$(find . -name '*.pb.txt') | cat) && \ + (echo "$(ONI) please run 'make protos' when making changes to proto files and check-in the generated descriptor file changes" && false)) + +proto-fmt: ## check format of proto files + @echo "$(WHALE) $@" + @test -z "$$(find . -name '*.proto' -type f -exec grep -Hn -e "^ " {} \; | tee /dev/stderr)" || \ + (echo "$(ONI) please indent proto files with tabs only" && false) + @test -z "$$(find . -name '*.proto' -type f -exec grep -Hn "Meta meta = " {} \; | grep -v '(gogoproto.nullable) = false' | tee /dev/stderr)" || \ + (echo "$(ONI) meta fields in proto files must have option (gogoproto.nullable) = false" && false) + +build: ## build the go packages + @echo "$(WHALE) $@" + @$(GO) build ${DEBUG_GO_GCFLAGS} ${GO_GCFLAGS} ${GO_BUILD_FLAGS} ${EXTRA_FLAGS} ${PACKAGES} + +test: ## run tests, except integration tests and tests that require root + @echo "$(WHALE) $@" + @$(GOTEST) ${TESTFLAGS} ${TESTPACKAGES} + +integration: ## run integration tests + @echo "$(WHALE) $@" + @cd "${ROOTDIR}/integration" && $(GOTEST) -v ${TESTFLAGS} -parallel ${TESTFLAGS_PARALLEL} . + +benchmark: ## run benchmarks tests + @echo "$(WHALE) $@" + @$(GO) test ${TESTFLAGS} -bench . -run Benchmark + +FORCE: + +define BUILD_BINARY +@echo "$(WHALE) $@" +@$(GO) build ${DEBUG_GO_GCFLAGS} ${GO_GCFLAGS} ${GO_BUILD_FLAGS} -o $@ ${GO_TAGS} ./$< +endef + +# Build a binary from a cmd. +bin/%: cmd/% FORCE + $(call BUILD_BINARY) + +binaries: $(BINARIES) ## build binaries + @echo "$(WHALE) $@" + +clean: ## clean up binaries + @echo "$(WHALE) $@" + @rm -f $(BINARIES) + +install: ## install binaries + @echo "$(WHALE) $@ $(BINPACKAGES)" + @$(GO) install $(BINPACKAGES) + +install-protobuf: + @echo "$(WHALE) $@" + @script/install-protobuf + +install-protobuild: + @echo "$(WHALE) $@" + @$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 + @$(GO) install github.com/containerd/protobuild@14832ccc41429f5c4f81028e5af08aa233a219cf + +coverage: ## generate coverprofiles from the unit tests, except tests that require root + @echo "$(WHALE) $@" + @rm -f coverage.txt + @$(GO) test ${TESTFLAGS} ${TESTPACKAGES} 2> /dev/null + @( for pkg in ${PACKAGES}; do \ + $(GO) test ${TESTFLAGS} \ + -cover \ + -coverprofile=profile.out \ + -covermode=atomic $$pkg || exit; \ + if [ -f profile.out ]; then \ + cat profile.out >> coverage.txt; \ + rm profile.out; \ + fi; \ + done ) + +vendor: ## ensure all the go.mod/go.sum files are up-to-date + @echo "$(WHALE) $@" + @$(GO) mod tidy + @$(GO) mod verify + +verify-vendor: ## verify if all the go.mod/go.sum files are up-to-date + @echo "$(WHALE) $@" + @$(GO) mod tidy + @$(GO) mod verify + @test -z "$$(git status --short | grep "go.sum" | tee /dev/stderr)" || \ + ((git diff | cat) && \ + (echo "$(ONI) make sure to checkin changes after go mod tidy" && false)) + +help: ## this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort diff --git a/vendor/github.com/containerd/ttrpc/PROTOCOL.md b/vendor/github.com/containerd/ttrpc/PROTOCOL.md new file mode 100644 index 000000000..12b43f6bd --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/PROTOCOL.md @@ -0,0 +1,240 @@ +# Protocol Specification + +The ttrpc protocol is client/server protocol to support multiple request streams +over a single connection with lightweight framing. The client represents the +process which initiated the underlying connection and the server is the process +which accepted the connection. The protocol is currently defined as +asymmetrical, with clients sending requests and servers sending responses. Both +clients and servers are able to send stream data. The roles are also used in +determining the stream identifiers, with client initiated streams using odd +number identifiers and server initiated using even number. The protocol may be +extended in the future to support server initiated streams, that is not +supported in the latest version. + +## Purpose + +The ttrpc protocol is designed to be lightweight and optimized for low latency +and reliable connections between processes on the same host. The protocol does +not include features for handling unreliable connections such as handshakes, +resets, pings, or flow control. The protocol is designed to make low-overhead +implementations as simple as possible. It is not intended as a suitable +replacement for HTTP2/3 over the network. + +## Message Frame + +Each Message Frame consists of a 10-byte message header followed +by message data. The data length and stream ID are both big-endian +4-byte unsigned integers. The message type is an unsigned 1-byte +integer. The flags are also an unsigned 1-byte integer and +use is defined by the message type. + + +---------------------------------------------------------------+ + | Data Length (32) | + +---------------------------------------------------------------+ + | Stream ID (32) | + +---------------+-----------------------------------------------+ + | Msg Type (8) | + +---------------+ + | Flags (8) | + +---------------+-----------------------------------------------+ + | Data (*) | + +---------------------------------------------------------------+ + +The Data Length field represents the number of bytes in the Data field. The +total frame size will always be Data Length + 10 bytes. The maximum data length +is 4MB and any larger size should be rejected. Due to the maximum data size +being less than 16MB, the first frame byte should always be zero. This first +byte should be considered reserved for future use. + +The Stream ID must be odd for client initiated streams and even for server +initiated streams. Server initiated streams are not currently supported. + +## Mesage Types + +| Message Type | Name | Description | +|--------------|----------|----------------------------------| +| 0x01 | Request | Initiates stream | +| 0x02 | Response | Final stream data and terminates | +| 0x03 | Data | Stream data | + +### Request + +The request message is used to initiate stream and send along request data for +properly routing and handling the stream. The stream may indicate unary without +any inbound or outbound stream data with only a response is expected on the +stream. The request may also indicate the stream is still open for more data and +no response is expected until data is finished. If the remote indicates the +stream is closed, the request may be considered non-unary but without anymore +stream data sent. In the case of `remote closed`, the remote still expects to +receive a response or stream data. For compatibility with non streaming clients, +a request with empty flags indicates a unary request. + +#### Request Flags + +| Flag | Name | Description | +|------|-----------------|--------------------------------------------------| +| 0x01 | `remote closed` | Non-unary, but no more data expected from remote | +| 0x02 | `remote open` | Non-unary, remote is still sending data | + +### Response + +The response message is used to end a stream with data, an empty response, or +an error. A response message is the only expected message after a unary request. +A non-unary request does not require a response message if the server is sending +back stream data. A non-unary stream may return a single response message but no +other stream data may follow. + +#### Response Flags + +No response flags are defined at this time, flags should be empty. + +### Data + +The data message is used to send data on an already initialized stream. Either +client or server may send data. A data message is not allowed on a unary stream. +A data message should not be sent after indicating `remote closed` to the peer. +The last data message on a stream must set the `remote closed` flag. + +The `no data` flag is used to indicate that the data message does not include +any data. This is normally used with the `remote closed` flag to indicate the +stream is now closed without transmitting any data. Since ttrpc normally +transmits a single object per message, a zero length data message may be +interpreted as an empty object. For example, transmitting the number zero as a +protobuf message ends up with a data length of zero, but the message is still +considered data and should be processed. + +#### Data Flags + +| Flag | Name | Description | +|------|-----------------|-----------------------------------| +| 0x01 | `remote closed` | No more data expected from remote | +| 0x04 | `no data` | This message does not have data | + +## Streaming + +All ttrpc requests use streams to transfer data. Unary streams will only have +two messages sent per stream, a request from a client and a response from the +server. Non-unary streams, however, may send any numbers of messages from the +client and the server. This makes stream management more complicated than unary +streams since both client and server need to track additional state. To keep +this management as simple as possible, ttrpc minimizes the number of states and +uses two flags instead of control frames. Each stream has two states while a +stream is still alive: `local closed` and `remote closed`. Each peer considers +local and remote from their own perspective and sets flags from the other peer's +perspective. For example, if a client sends a data frame with the +`remote closed` flag, that is indicating that the client is now `local closed` +and the server will be `remote closed`. A unary operation does not need to send +these flags since each received message always indicates `remote closed`. Once a +peer is both `local closed` and `remote closed`, the stream is considered +finished and may be cleaned up. + +Due to the asymmetric nature of the current protocol, a client should +always be in the `local closed` state before `remote closed` and a server should +always be in the `remote closed` state before `local closed`. This happens +because the client is always initiating requests and a client always expects a +final response back from a server to indicate the initiated request has been +fulfilled. This may mean server sends a final empty response to finish a stream +even after it has already completed sending data before the client. + +### Unary State Diagram + + +--------+ +--------+ + | Client | | Server | + +---+----+ +----+---+ + | +---------+ | + local >---------------+ Request +--------------------> remote + closed | +---------+ | closed + | | + | +----------+ | + finished <--------------+ Response +--------------------< finished + | +----------+ | + | | + +### Non-Unary State Diagrams + +RC: `remote closed` flag +RO: `remote open` flag + + +--------+ +--------+ + | Client | | Server | + +---+----+ +----+---+ + | +--------------+ | + >-------------+ Request [RO] +-----------------> + | +--------------+ | + | | + | +------+ | + >-----------------+ Data +---------------------> + | +------+ | + | | + | +-----------+ | + local >---------------+ Data [RC] +------------------> remote + closed | +-----------+ | closed + | | + | +----------+ | + finished <--------------+ Response +--------------------< finished + | +----------+ | + | | + + +--------+ +--------+ + | Client | | Server | + +---+----+ +----+---+ + | +--------------+ | + local >-------------+ Request [RC] +-----------------> remote + closed | +--------------+ | closed + | | + | +------+ | + <-----------------+ Data +---------------------< + | +------+ | + | | + | +-----------+ | + finished <---------------+ Data [RC] +------------------< finished + | +-----------+ | + | | + + +--------+ +--------+ + | Client | | Server | + +---+----+ +----+---+ + | +--------------+ | + >-------------+ Request [RO] +-----------------> + | +--------------+ | + | | + | +------+ | + >-----------------+ Data +---------------------> + | +------+ | + | | + | +------+ | + <-----------------+ Data +---------------------< + | +------+ | + | | + | +------+ | + >-----------------+ Data +---------------------> + | +------+ | + | | + | +-----------+ | + local >---------------+ Data [RC] +------------------> remote + closed | +-----------+ | closed + | | + | +------+ | + <-----------------+ Data +---------------------< + | +------+ | + | | + | +-----------+ | + finished <---------------+ Data [RC] +------------------< finished + | +-----------+ | + | | + +## RPC + +While this protocol is defined primarily to support Remote Procedure Calls, the +protocol does not define the request and response types beyond the messages +defined in the protocol. The implementation provides a default protobuf +definition of request and response which may be used for cross language rpc. +All implementations should at least define a request type which support +routing by procedure name and a response type which supports call status. + +## Version History + +| Version | Features | +|---------|---------------------| +| 1.0 | Unary requests only | +| 1.2 | Streaming support | diff --git a/vendor/github.com/containerd/ttrpc/Protobuild.toml b/vendor/github.com/containerd/ttrpc/Protobuild.toml new file mode 100644 index 000000000..0f6ccbd1e --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/Protobuild.toml @@ -0,0 +1,28 @@ +version = "2" +generators = ["go"] + +# Control protoc include paths. Below are usually some good defaults, but feel +# free to try it without them if it works for your project. +[includes] + # Include paths that will be added before all others. Typically, you want to + # treat the root of the project as an include, but this may not be necessary. + before = ["."] + + # Paths that will be added untouched to the end of the includes. We use + # `/usr/local/include` to pickup the common install location of protobuf. + # This is the default. + after = ["/usr/local/include"] + +# This section maps protobuf imports to Go packages. These will become +# `-M` directives in the call to the go protobuf generator. +[packages] + "google/protobuf/any.proto" = "github.com/gogo/protobuf/types" + "proto/status.proto" = "google.golang.org/genproto/googleapis/rpc/status" + +[[overrides]] +# enable ttrpc and disable fieldpath and grpc for the shim +prefixes = ["github.com/containerd/ttrpc/integration/streaming"] +generators = ["go", "go-ttrpc"] + +[overrides.parameters.go-ttrpc] +prefix = "TTRPC" diff --git a/vendor/github.com/containerd/ttrpc/README.md b/vendor/github.com/containerd/ttrpc/README.md new file mode 100644 index 000000000..ce95f63be --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/README.md @@ -0,0 +1,59 @@ +# ttrpc + +[![Build Status](https://github.com/containerd/ttrpc/actions/workflows/ci.yml/badge.svg)](https://github.com/containerd/ttrpc/actions/workflows/ci.yml) + +GRPC for low-memory environments. + +The existing grpc-go project requires a lot of memory overhead for importing +packages and at runtime. While this is great for many services with low density +requirements, this can be a problem when running a large number of services on +a single machine or on a machine with a small amount of memory. + +Using the same GRPC definitions, this project reduces the binary size and +protocol overhead required. We do this by eliding the `net/http`, `net/http2` +and `grpc` package used by grpc replacing it with a lightweight framing +protocol. The result are smaller binaries that use less resident memory with +the same ease of use as GRPC. + +Please note that while this project supports generating either end of the +protocol, the generated service definitions will be incompatible with regular +GRPC services, as they do not speak the same protocol. + +# Protocol + +See the [protocol specification](./PROTOCOL.md). + +# Usage + +Create a gogo vanity binary (see +[`cmd/protoc-gen-gogottrpc/main.go`](cmd/protoc-gen-gogottrpc/main.go) for an +example with the ttrpc plugin enabled. + +It's recommended to use [`protobuild`](https://github.com/containerd/protobuild) +to build the protobufs for this project, but this will work with protoc +directly, if required. + +# Differences from GRPC + +- The protocol stack has been replaced with a lighter protocol that doesn't + require http, http2 and tls. +- The client and server interface are identical whereas in GRPC there is a + client and server interface that are different. +- The Go stdlib context package is used instead. + +# Status + +TODO: + +- [ ] Add testing under concurrent load to ensure +- [ ] Verify connection error handling + +# Project details + +ttrpc is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). +As a containerd sub-project, you will find the: + * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), + * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), + * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) + +information in our [`containerd/project`](https://github.com/containerd/project) repository. diff --git a/vendor/github.com/containerd/ttrpc/channel.go b/vendor/github.com/containerd/ttrpc/channel.go new file mode 100644 index 000000000..872261e6d --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/channel.go @@ -0,0 +1,182 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "net" + "sync" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + messageHeaderLength = 10 + messageLengthMax = 4 << 20 +) + +type messageType uint8 + +const ( + messageTypeRequest messageType = 0x1 + messageTypeResponse messageType = 0x2 + messageTypeData messageType = 0x3 +) + +func (mt messageType) String() string { + switch mt { + case messageTypeRequest: + return "request" + case messageTypeResponse: + return "response" + case messageTypeData: + return "data" + default: + return "unknown" + } +} + +const ( + flagRemoteClosed uint8 = 0x1 + flagRemoteOpen uint8 = 0x2 + flagNoData uint8 = 0x4 +) + +// messageHeader represents the fixed-length message header of 10 bytes sent +// with every request. +type messageHeader struct { + Length uint32 // length excluding this header. b[:4] + StreamID uint32 // identifies which request stream message is a part of. b[4:8] + Type messageType // message type b[8] + Flags uint8 // type specific flags b[9] +} + +func readMessageHeader(p []byte, r io.Reader) (messageHeader, error) { + _, err := io.ReadFull(r, p[:messageHeaderLength]) + if err != nil { + return messageHeader{}, err + } + + return messageHeader{ + Length: binary.BigEndian.Uint32(p[:4]), + StreamID: binary.BigEndian.Uint32(p[4:8]), + Type: messageType(p[8]), + Flags: p[9], + }, nil +} + +func writeMessageHeader(w io.Writer, p []byte, mh messageHeader) error { + binary.BigEndian.PutUint32(p[:4], mh.Length) + binary.BigEndian.PutUint32(p[4:8], mh.StreamID) + p[8] = byte(mh.Type) + p[9] = mh.Flags + + _, err := w.Write(p[:]) + return err +} + +var buffers sync.Pool + +type channel struct { + conn net.Conn + bw *bufio.Writer + br *bufio.Reader + hrbuf [messageHeaderLength]byte // avoid alloc when reading header + hwbuf [messageHeaderLength]byte +} + +func newChannel(conn net.Conn) *channel { + return &channel{ + conn: conn, + bw: bufio.NewWriter(conn), + br: bufio.NewReader(conn), + } +} + +// recv a message from the channel. The returned buffer contains the message. +// +// If a valid grpc status is returned, the message header +// returned will be valid and caller should send that along to +// the correct consumer. The bytes on the underlying channel +// will be discarded. +func (ch *channel) recv() (messageHeader, []byte, error) { + mh, err := readMessageHeader(ch.hrbuf[:], ch.br) + if err != nil { + return messageHeader{}, nil, err + } + + if mh.Length > uint32(messageLengthMax) { + if _, err := ch.br.Discard(int(mh.Length)); err != nil { + return mh, nil, fmt.Errorf("failed to discard after receiving oversized message: %w", err) + } + + return mh, nil, status.Errorf(codes.ResourceExhausted, "message length %v exceed maximum message size of %v", mh.Length, messageLengthMax) + } + + var p []byte + if mh.Length > 0 { + p = ch.getmbuf(int(mh.Length)) + if _, err := io.ReadFull(ch.br, p); err != nil { + return messageHeader{}, nil, fmt.Errorf("failed reading message: %w", err) + } + } + + return mh, p, nil +} + +func (ch *channel) send(streamID uint32, t messageType, flags uint8, p []byte) error { + if len(p) > messageLengthMax { + return OversizedMessageError(len(p)) + } + + if err := writeMessageHeader(ch.bw, ch.hwbuf[:], messageHeader{Length: uint32(len(p)), StreamID: streamID, Type: t, Flags: flags}); err != nil { + return err + } + + if len(p) > 0 { + _, err := ch.bw.Write(p) + if err != nil { + return err + } + } + + return ch.bw.Flush() +} + +func (ch *channel) getmbuf(size int) []byte { + // we can't use the standard New method on pool because we want to allocate + // based on size. + b, ok := buffers.Get().(*[]byte) + if !ok || cap(*b) < size { + // TODO(stevvooe): It may be better to allocate these in fixed length + // buckets to reduce fragmentation but its not clear that would help + // with performance. An ilogb approach or similar would work well. + bb := make([]byte, size) + b = &bb + } else { + *b = (*b)[:size] + } + return *b +} + +func (ch *channel) putmbuf(p []byte) { + buffers.Put(&p) +} diff --git a/vendor/github.com/containerd/ttrpc/client.go b/vendor/github.com/containerd/ttrpc/client.go new file mode 100644 index 000000000..be20ed489 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/client.go @@ -0,0 +1,571 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "strings" + "sync" + "syscall" + "time" + + "github.com/containerd/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Client for a ttrpc server +type Client struct { + codec codec + conn net.Conn + channel *channel + + streamLock sync.RWMutex + streams map[streamID]*stream + nextStreamID streamID + sendLock sync.Mutex + + ctx context.Context + closed func() + + closeOnce sync.Once + userCloseFunc func() + userCloseWaitCh chan struct{} + + interceptor UnaryClientInterceptor +} + +// ClientOpts configures a client +type ClientOpts func(c *Client) + +// WithOnClose sets the close func whenever the client's Close() method is called +func WithOnClose(onClose func()) ClientOpts { + return func(c *Client) { + c.userCloseFunc = onClose + } +} + +// WithUnaryClientInterceptor sets the provided client interceptor +func WithUnaryClientInterceptor(i UnaryClientInterceptor) ClientOpts { + return func(c *Client) { + c.interceptor = i + } +} + +// WithChainUnaryClientInterceptor sets the provided chain of client interceptors +func WithChainUnaryClientInterceptor(interceptors ...UnaryClientInterceptor) ClientOpts { + return func(c *Client) { + if len(interceptors) == 0 { + return + } + if c.interceptor != nil { + interceptors = append([]UnaryClientInterceptor{c.interceptor}, interceptors...) + } + c.interceptor = func( + ctx context.Context, + req *Request, + reply *Response, + info *UnaryClientInfo, + final Invoker, + ) error { + return interceptors[0](ctx, req, reply, info, + chainUnaryInterceptors(interceptors[1:], final, info)) + } + } +} + +func chainUnaryInterceptors(interceptors []UnaryClientInterceptor, final Invoker, info *UnaryClientInfo) Invoker { + if len(interceptors) == 0 { + return final + } + return func( + ctx context.Context, + req *Request, + reply *Response, + ) error { + return interceptors[0](ctx, req, reply, info, + chainUnaryInterceptors(interceptors[1:], final, info)) + } +} + +// NewClient creates a new ttrpc client using the given connection +func NewClient(conn net.Conn, opts ...ClientOpts) *Client { + ctx, cancel := context.WithCancel(context.Background()) + channel := newChannel(conn) + c := &Client{ + codec: codec{}, + conn: conn, + channel: channel, + streams: make(map[streamID]*stream), + nextStreamID: 1, + closed: cancel, + ctx: ctx, + userCloseFunc: func() {}, + userCloseWaitCh: make(chan struct{}), + } + + for _, o := range opts { + o(c) + } + + if c.interceptor == nil { + c.interceptor = defaultClientInterceptor + } + + go c.run() + return c +} + +func (c *Client) send(sid uint32, mt messageType, flags uint8, b []byte) error { + c.sendLock.Lock() + defer c.sendLock.Unlock() + return c.channel.send(sid, mt, flags, b) +} + +// Call makes a unary request and returns with response +func (c *Client) Call(ctx context.Context, service, method string, req, resp interface{}) error { + payload, err := c.codec.Marshal(req) + if err != nil { + return err + } + + var ( + creq = &Request{ + Service: service, + Method: method, + Payload: payload, + // TODO: metadata from context + } + + cresp = &Response{} + ) + + if metadata, ok := GetMetadata(ctx); ok { + metadata.setRequest(creq) + } + + if dl, ok := ctx.Deadline(); ok { + creq.TimeoutNano = time.Until(dl).Nanoseconds() + } + + info := &UnaryClientInfo{ + FullMethod: fullPath(service, method), + } + if err := c.interceptor(ctx, creq, cresp, info, c.dispatch); err != nil { + return err + } + + if err := c.codec.Unmarshal(cresp.Payload, resp); err != nil { + return err + } + + if cresp.Status != nil && cresp.Status.Code != int32(codes.OK) { + return status.ErrorProto(cresp.Status) + } + return nil +} + +// StreamDesc describes the stream properties, whether the stream has +// a streaming client, a streaming server, or both +type StreamDesc struct { + StreamingClient bool + StreamingServer bool +} + +// ClientStream is used to send or recv messages on the underlying stream +type ClientStream interface { + CloseSend() error + SendMsg(m interface{}) error + RecvMsg(m interface{}) error +} + +type clientStream struct { + ctx context.Context + s *stream + c *Client + desc *StreamDesc + localClosed bool + remoteClosed bool +} + +func (cs *clientStream) CloseSend() error { + if !cs.desc.StreamingClient { + return fmt.Errorf("%w: cannot close non-streaming client", ErrProtocol) + } + if cs.localClosed { + return ErrStreamClosed + } + err := cs.s.send(messageTypeData, flagRemoteClosed|flagNoData, nil) + if err != nil { + return filterCloseErr(err) + } + cs.localClosed = true + return nil +} + +func (cs *clientStream) SendMsg(m interface{}) error { + if !cs.desc.StreamingClient { + return fmt.Errorf("%w: cannot send data from non-streaming client", ErrProtocol) + } + if cs.localClosed { + return ErrStreamClosed + } + + var ( + payload []byte + err error + ) + if m != nil { + payload, err = cs.c.codec.Marshal(m) + if err != nil { + return err + } + } + + err = cs.s.send(messageTypeData, 0, payload) + if err != nil { + return filterCloseErr(err) + } + + return nil +} + +func (cs *clientStream) RecvMsg(m interface{}) error { + if cs.remoteClosed { + return io.EOF + } + + var msg *streamMessage + select { + case <-cs.ctx.Done(): + return cs.ctx.Err() + case <-cs.s.recvClose: + // If recv has a pending message, process that first + select { + case msg = <-cs.s.recv: + default: + return cs.s.recvErr + } + case msg = <-cs.s.recv: + } + + switch msg.header.Type { + case messageTypeResponse: + resp := &Response{} + err := proto.Unmarshal(msg.payload[:msg.header.Length], resp) + // return the payload buffer for reuse + cs.c.channel.putmbuf(msg.payload) + if err != nil { + return err + } + + if err := cs.c.codec.Unmarshal(resp.Payload, m); err != nil { + return err + } + + if resp.Status != nil && resp.Status.Code != int32(codes.OK) { + return status.ErrorProto(resp.Status) + } + + cs.c.deleteStream(cs.s) + cs.remoteClosed = true + + return nil + case messageTypeData: + if !cs.desc.StreamingServer { + cs.c.deleteStream(cs.s) + cs.remoteClosed = true + return fmt.Errorf("received data from non-streaming server: %w", ErrProtocol) + } + if msg.header.Flags&flagRemoteClosed == flagRemoteClosed { + cs.c.deleteStream(cs.s) + cs.remoteClosed = true + + if msg.header.Flags&flagNoData == flagNoData { + return io.EOF + } + } + + err := cs.c.codec.Unmarshal(msg.payload[:msg.header.Length], m) + cs.c.channel.putmbuf(msg.payload) + if err != nil { + return err + } + return nil + default: + return fmt.Errorf("unexpected %q message received: %w", msg.header.Type, ErrProtocol) + } +} + +// Close closes the ttrpc connection and underlying connection +func (c *Client) Close() error { + c.closeOnce.Do(func() { + c.closed() + + c.conn.Close() + }) + return nil +} + +// UserOnCloseWait is used to block until the user's on-close callback +// finishes. +func (c *Client) UserOnCloseWait(ctx context.Context) error { + select { + case <-c.userCloseWaitCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (c *Client) run() { + err := c.receiveLoop() + c.Close() + c.cleanupStreams(err) + + c.userCloseFunc() + close(c.userCloseWaitCh) +} + +func (c *Client) receiveLoop() error { + for { + select { + case <-c.ctx.Done(): + return ErrClosed + default: + var ( + msg = &streamMessage{} + err error + ) + + msg.header, msg.payload, err = c.channel.recv() + if err != nil { + _, ok := status.FromError(err) + if !ok { + // treat all errors that are not an rpc status as terminal. + // all others poison the connection. + return filterCloseErr(err) + } + } + sid := streamID(msg.header.StreamID) + s := c.getStream(sid) + if s == nil { + log.G(c.ctx).WithField("stream", sid).Error("ttrpc: received message on inactive stream") + continue + } + + if err != nil { + s.closeWithError(err) + } else { + if err := s.receive(c.ctx, msg); err != nil { + log.G(c.ctx).WithFields(log.Fields{"error": err, "stream": sid}).Error("ttrpc: failed to handle message") + } + } + } + } +} + +// createStream creates a new stream and registers it with the client +// Introduce stream types for multiple or single response +func (c *Client) createStream(flags uint8, b []byte) (*stream, error) { + // sendLock must be held across both allocation of the stream ID and sending it across the wire. + // This ensures that new stream IDs sent on the wire are always increasing, which is a + // requirement of the TTRPC protocol. + // This use of sendLock could be split into another mutex that covers stream creation + first send, + // and just use sendLock to guard writing to the wire, but for now it seems simpler to have fewer mutexes. + c.sendLock.Lock() + defer c.sendLock.Unlock() + + // Check if closed since lock acquired to prevent adding + // anything after cleanup completes + select { + case <-c.ctx.Done(): + return nil, ErrClosed + default: + } + + var s *stream + if err := func() error { + // In the future this could be replaced with a sync.Map instead of streamLock+map. + c.streamLock.Lock() + defer c.streamLock.Unlock() + + // Check if closed since lock acquired to prevent adding + // anything after cleanup completes + select { + case <-c.ctx.Done(): + return ErrClosed + default: + } + + s = newStream(c.nextStreamID, c) + c.streams[s.id] = s + c.nextStreamID = c.nextStreamID + 2 + + return nil + }(); err != nil { + return nil, err + } + + if err := c.channel.send(uint32(s.id), messageTypeRequest, flags, b); err != nil { + return s, filterCloseErr(err) + } + + return s, nil +} + +func (c *Client) deleteStream(s *stream) { + c.streamLock.Lock() + delete(c.streams, s.id) + c.streamLock.Unlock() + s.closeWithError(nil) +} + +func (c *Client) getStream(sid streamID) *stream { + c.streamLock.RLock() + s := c.streams[sid] + c.streamLock.RUnlock() + return s +} + +func (c *Client) cleanupStreams(err error) { + c.streamLock.Lock() + defer c.streamLock.Unlock() + + for sid, s := range c.streams { + s.closeWithError(err) + delete(c.streams, sid) + } +} + +// filterCloseErr rewrites EOF and EPIPE errors to ErrClosed. Use when +// returning from call or handling errors from main read loop. +// +// This purposely ignores errors with a wrapped cause. +func filterCloseErr(err error) error { + switch { + case err == nil: + return nil + case err == io.EOF: + return ErrClosed + case errors.Is(err, io.ErrClosedPipe): + return ErrClosed + case errors.Is(err, io.EOF): + return ErrClosed + case strings.Contains(err.Error(), "use of closed network connection"): + return ErrClosed + default: + // if we have an epipe on a write or econnreset on a read , we cast to errclosed + var oerr *net.OpError + if errors.As(err, &oerr) { + if (oerr.Op == "write" && errors.Is(err, syscall.EPIPE)) || + (oerr.Op == "read" && errors.Is(err, syscall.ECONNRESET)) { + return ErrClosed + } + } + } + + return err +} + +// NewStream creates a new stream with the given stream descriptor to the +// specified service and method. If not a streaming client, the request object +// may be provided. +func (c *Client) NewStream(ctx context.Context, desc *StreamDesc, service, method string, req interface{}) (ClientStream, error) { + var payload []byte + if req != nil { + var err error + payload, err = c.codec.Marshal(req) + if err != nil { + return nil, err + } + } + + request := &Request{ + Service: service, + Method: method, + Payload: payload, + // TODO: metadata from context + } + p, err := c.codec.Marshal(request) + if err != nil { + return nil, err + } + + var flags uint8 + if desc.StreamingClient { + flags = flagRemoteOpen + } else { + flags = flagRemoteClosed + } + s, err := c.createStream(flags, p) + if err != nil { + return nil, err + } + + return &clientStream{ + ctx: ctx, + s: s, + c: c, + desc: desc, + }, nil +} + +func (c *Client) dispatch(ctx context.Context, req *Request, resp *Response) error { + p, err := c.codec.Marshal(req) + if err != nil { + return err + } + + s, err := c.createStream(0, p) + if err != nil { + return err + } + defer c.deleteStream(s) + + var msg *streamMessage + select { + case <-ctx.Done(): + return ctx.Err() + case <-c.ctx.Done(): + return ErrClosed + case <-s.recvClose: + // If recv has a pending message, process that first + select { + case msg = <-s.recv: + default: + return s.recvErr + } + case msg = <-s.recv: + } + + if msg.header.Type == messageTypeResponse { + err = proto.Unmarshal(msg.payload[:msg.header.Length], resp) + } else { + err = fmt.Errorf("unexpected %q message received: %w", msg.header.Type, ErrProtocol) + } + + // return the payload buffer for reuse + c.channel.putmbuf(msg.payload) + + return err +} diff --git a/vendor/github.com/containerd/ttrpc/codec.go b/vendor/github.com/containerd/ttrpc/codec.go new file mode 100644 index 000000000..3e82722a4 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/codec.go @@ -0,0 +1,43 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "fmt" + + "google.golang.org/protobuf/proto" +) + +type codec struct{} + +func (c codec) Marshal(msg interface{}) ([]byte, error) { + switch v := msg.(type) { + case proto.Message: + return proto.Marshal(v) + default: + return nil, fmt.Errorf("ttrpc: cannot marshal unknown type: %T", msg) + } +} + +func (c codec) Unmarshal(p []byte, msg interface{}) error { + switch v := msg.(type) { + case proto.Message: + return proto.Unmarshal(p, v) + default: + return fmt.Errorf("ttrpc: cannot unmarshal into unknown type: %T", msg) + } +} diff --git a/vendor/github.com/containerd/ttrpc/config.go b/vendor/github.com/containerd/ttrpc/config.go new file mode 100644 index 000000000..f401f67be --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/config.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 ttrpc + +import ( + "context" + "errors" +) + +type serverConfig struct { + handshaker Handshaker + interceptor UnaryServerInterceptor +} + +// ServerOpt for configuring a ttrpc server +type ServerOpt func(*serverConfig) error + +// WithServerHandshaker can be passed to NewServer to ensure that the +// handshaker is called before every connection attempt. +// +// Only one handshaker is allowed per server. +func WithServerHandshaker(handshaker Handshaker) ServerOpt { + return func(c *serverConfig) error { + if c.handshaker != nil { + return errors.New("only one handshaker allowed per server") + } + c.handshaker = handshaker + return nil + } +} + +// WithUnaryServerInterceptor sets the provided interceptor on the server +func WithUnaryServerInterceptor(i UnaryServerInterceptor) ServerOpt { + return func(c *serverConfig) error { + if c.interceptor != nil { + return errors.New("only one unchained interceptor allowed per server") + } + c.interceptor = i + return nil + } +} + +// WithChainUnaryServerInterceptor sets the provided chain of server interceptors +func WithChainUnaryServerInterceptor(interceptors ...UnaryServerInterceptor) ServerOpt { + return func(c *serverConfig) error { + if len(interceptors) == 0 { + return nil + } + if c.interceptor != nil { + interceptors = append([]UnaryServerInterceptor{c.interceptor}, interceptors...) + } + c.interceptor = func( + ctx context.Context, + unmarshal Unmarshaler, + info *UnaryServerInfo, + method Method) (interface{}, error) { + return interceptors[0](ctx, unmarshal, info, + chainUnaryServerInterceptors(info, method, interceptors[1:])) + } + return nil + } +} + +func chainUnaryServerInterceptors(info *UnaryServerInfo, method Method, interceptors []UnaryServerInterceptor) Method { + if len(interceptors) == 0 { + return method + } + return func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) { + return interceptors[0](ctx, unmarshal, info, + chainUnaryServerInterceptors(info, method, interceptors[1:])) + } +} diff --git a/vendor/github.com/containerd/ttrpc/doc.go b/vendor/github.com/containerd/ttrpc/doc.go new file mode 100644 index 000000000..d80cd424c --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/doc.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 ttrpc defines and implements a low level simple transfer protocol +optimized for low latency and reliable connections between processes on the same +host. The protocol uses simple framing for sending requests, responses, and data +using multiple streams. +*/ +package ttrpc diff --git a/vendor/github.com/containerd/ttrpc/errors.go b/vendor/github.com/containerd/ttrpc/errors.go new file mode 100644 index 000000000..632dbe8bd --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/errors.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 ttrpc + +import ( + "errors" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + // ErrProtocol is a general error in the handling the protocol. + ErrProtocol = errors.New("protocol error") + + // ErrClosed is returned by client methods when the underlying connection is + // closed. + ErrClosed = errors.New("ttrpc: closed") + + // ErrServerClosed is returned when the Server has closed its connection. + ErrServerClosed = errors.New("ttrpc: server closed") + + // ErrStreamClosed is when the streaming connection is closed. + ErrStreamClosed = errors.New("ttrpc: stream closed") +) + +// OversizedMessageErr is used to indicate refusal to send an oversized message. +// It wraps a ResourceExhausted grpc Status together with the offending message +// length. +type OversizedMessageErr struct { + messageLength int + err error +} + +// OversizedMessageError returns an OversizedMessageErr error for the given message +// length if it exceeds the allowed maximum. Otherwise a nil error is returned. +func OversizedMessageError(messageLength int) error { + if messageLength <= messageLengthMax { + return nil + } + + return &OversizedMessageErr{ + messageLength: messageLength, + err: status.Errorf(codes.ResourceExhausted, "message length %v exceed maximum message size of %v", messageLength, messageLengthMax), + } +} + +// Error returns the error message for the corresponding grpc Status for the error. +func (e *OversizedMessageErr) Error() string { + return e.err.Error() +} + +// Unwrap returns the corresponding error with our grpc status code. +func (e *OversizedMessageErr) Unwrap() error { + return e.err +} + +// RejectedLength retrieves the rejected message length which triggered the error. +func (e *OversizedMessageErr) RejectedLength() int { + return e.messageLength +} + +// MaximumLength retrieves the maximum allowed message length that triggered the error. +func (*OversizedMessageErr) MaximumLength() int { + return messageLengthMax +} diff --git a/vendor/github.com/containerd/ttrpc/handshake.go b/vendor/github.com/containerd/ttrpc/handshake.go new file mode 100644 index 000000000..3c6b610d3 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/handshake.go @@ -0,0 +1,50 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "context" + "net" +) + +// Handshaker defines the interface for connection handshakes performed on the +// server or client when first connecting. +type Handshaker interface { + // Handshake should confirm or decorate a connection that may be incoming + // to a server or outgoing from a client. + // + // If this returns without an error, the caller should use the connection + // in place of the original connection. + // + // The second return value can contain credential specific data, such as + // unix socket credentials or TLS information. + // + // While we currently only have implementations on the server-side, this + // interface should be sufficient to implement similar handshakes on the + // client-side. + Handshake(ctx context.Context, conn net.Conn) (net.Conn, interface{}, error) +} + +type handshakerFunc func(ctx context.Context, conn net.Conn) (net.Conn, interface{}, error) + +func (fn handshakerFunc) Handshake(ctx context.Context, conn net.Conn) (net.Conn, interface{}, error) { + return fn(ctx, conn) +} + +func noopHandshake(_ context.Context, conn net.Conn) (net.Conn, interface{}, error) { + return conn, nil, nil +} diff --git a/vendor/github.com/containerd/ttrpc/interceptor.go b/vendor/github.com/containerd/ttrpc/interceptor.go new file mode 100644 index 000000000..7ff5e9d33 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/interceptor.go @@ -0,0 +1,65 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import "context" + +// UnaryServerInfo provides information about the server request +type UnaryServerInfo struct { + FullMethod string +} + +// UnaryClientInfo provides information about the client request +type UnaryClientInfo struct { + FullMethod string +} + +// StreamServerInfo provides information about the server request +type StreamServerInfo struct { + FullMethod string + StreamingClient bool + StreamingServer bool +} + +// Unmarshaler contains the server request data and allows it to be unmarshaled +// into a concrete type +type Unmarshaler func(interface{}) error + +// Invoker invokes the client's request and response from the ttrpc server +type Invoker func(context.Context, *Request, *Response) error + +// UnaryServerInterceptor specifies the interceptor function for server request/response +type UnaryServerInterceptor func(context.Context, Unmarshaler, *UnaryServerInfo, Method) (interface{}, error) + +// UnaryClientInterceptor specifies the interceptor function for client request/response +type UnaryClientInterceptor func(context.Context, *Request, *Response, *UnaryClientInfo, Invoker) error + +func defaultServerInterceptor(ctx context.Context, unmarshal Unmarshaler, _ *UnaryServerInfo, method Method) (interface{}, error) { + return method(ctx, unmarshal) +} + +func defaultClientInterceptor(ctx context.Context, req *Request, resp *Response, _ *UnaryClientInfo, invoker Invoker) error { + return invoker(ctx, req, resp) +} + +type StreamServerInterceptor func(context.Context, StreamServer, *StreamServerInfo, StreamHandler) (interface{}, error) + +func defaultStreamServerInterceptor(ctx context.Context, ss StreamServer, _ *StreamServerInfo, stream StreamHandler) (interface{}, error) { + return stream(ctx, ss) +} + +type StreamClientInterceptor func(context.Context) diff --git a/vendor/github.com/containerd/ttrpc/metadata.go b/vendor/github.com/containerd/ttrpc/metadata.go new file mode 100644 index 000000000..6e0042487 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/metadata.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 ttrpc + +import ( + "context" + "strings" +) + +// MD is the user type for ttrpc metadata +type MD map[string][]string + +// Get returns the metadata for a given key when they exist. +// If there is no metadata, a nil slice and false are returned. +func (m MD) Get(key string) ([]string, bool) { + key = strings.ToLower(key) + list, ok := m[key] + if !ok || len(list) == 0 { + return nil, false + } + + return list, true +} + +// Set sets the provided values for a given key. +// The values will overwrite any existing values. +// If no values provided, a key will be deleted. +func (m MD) Set(key string, values ...string) { + key = strings.ToLower(key) + if len(values) == 0 { + delete(m, key) + return + } + m[key] = values +} + +// Append appends additional values to the given key. +func (m MD) Append(key string, values ...string) { + key = strings.ToLower(key) + if len(values) == 0 { + return + } + current, ok := m[key] + if ok { + m.Set(key, append(current, values...)...) + } else { + m.Set(key, values...) + } +} + +// Clone returns a copy of MD or nil if it's nil. +// It's copied from golang's `http.Header.Clone` implementation: +// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/net/http/header.go;l=94 +func (m MD) Clone() MD { + if m == nil { + return nil + } + + // Find total number of values. + nv := 0 + for _, vv := range m { + nv += len(vv) + } + sv := make([]string, nv) // shared backing array for headers' values + m2 := make(MD, len(m)) + for k, vv := range m { + if vv == nil { + // Preserve nil values. + m2[k] = nil + continue + } + n := copy(sv, vv) + m2[k] = sv[:n:n] + sv = sv[n:] + } + return m2 +} + +func (m MD) setRequest(r *Request) { + for k, values := range m { + for _, v := range values { + r.Metadata = append(r.Metadata, &KeyValue{ + Key: k, + Value: v, + }) + } + } +} + +func (m MD) fromRequest(r *Request) { + for _, kv := range r.Metadata { + m[kv.Key] = append(m[kv.Key], kv.Value) + } +} + +type metadataKey struct{} + +// GetMetadata retrieves metadata from context.Context (previously attached with WithMetadata) +func GetMetadata(ctx context.Context) (MD, bool) { + metadata, ok := ctx.Value(metadataKey{}).(MD) + return metadata, ok +} + +// GetMetadataValue gets a specific metadata value by name from context.Context +func GetMetadataValue(ctx context.Context, name string) (string, bool) { + metadata, ok := GetMetadata(ctx) + if !ok { + return "", false + } + + if list, ok := metadata.Get(name); ok { + return list[0], true + } + + return "", false +} + +// WithMetadata attaches metadata map to a context.Context +func WithMetadata(ctx context.Context, md MD) context.Context { + return context.WithValue(ctx, metadataKey{}, md) +} diff --git a/vendor/github.com/containerd/ttrpc/request.pb.go b/vendor/github.com/containerd/ttrpc/request.pb.go new file mode 100644 index 000000000..3921ae5a3 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/request.pb.go @@ -0,0 +1,396 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.20.1 +// source: github.com/containerd/ttrpc/request.proto + +package ttrpc + +import ( + status "google.golang.org/genproto/googleapis/rpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Request struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` + TimeoutNano int64 `protobuf:"varint,4,opt,name=timeout_nano,json=timeoutNano,proto3" json:"timeout_nano,omitempty"` + Metadata []*KeyValue `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty"` +} + +func (x *Request) Reset() { + *x = Request{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{0} +} + +func (x *Request) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *Request) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *Request) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *Request) GetTimeoutNano() int64 { + if x != nil { + return x.TimeoutNano + } + return 0 +} + +func (x *Request) GetMetadata() []*KeyValue { + if x != nil { + return x.Metadata + } + return nil +} + +type Response struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status *status.Status `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` +} + +func (x *Response) Reset() { + *x = Response{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{1} +} + +func (x *Response) GetStatus() *status.Status { + if x != nil { + return x.Status + } + return nil +} + +func (x *Response) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type StringList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + List []string `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` +} + +func (x *StringList) Reset() { + *x = StringList{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StringList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringList) ProtoMessage() {} + +func (x *StringList) ProtoReflect() protoreflect.Message { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringList.ProtoReflect.Descriptor instead. +func (*StringList) Descriptor() ([]byte, []int) { + return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{2} +} + +func (x *StringList) GetList() []string { + if x != nil { + return x.List + } + return nil +} + +type KeyValue struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *KeyValue) Reset() { + *x = KeyValue{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyValue) ProtoMessage() {} + +func (x *KeyValue) ProtoReflect() protoreflect.Message { + mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyValue.ProtoReflect.Descriptor instead. +func (*KeyValue) Descriptor() ([]byte, []int) { + return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{3} +} + +func (x *KeyValue) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *KeyValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +var File_github_com_containerd_ttrpc_request_proto protoreflect.FileDescriptor + +var file_github_com_containerd_ttrpc_request_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x74, 0x72, + 0x70, 0x63, 0x1a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4e, 0x61, 0x6e, + 0x6f, 0x12, 0x2b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x45, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x20, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, + 0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x32, 0x0a, 0x08, 0x4b, 0x65, 0x79, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1d, 0x5a, 0x1b, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_github_com_containerd_ttrpc_request_proto_rawDescOnce sync.Once + file_github_com_containerd_ttrpc_request_proto_rawDescData = file_github_com_containerd_ttrpc_request_proto_rawDesc +) + +func file_github_com_containerd_ttrpc_request_proto_rawDescGZIP() []byte { + file_github_com_containerd_ttrpc_request_proto_rawDescOnce.Do(func() { + file_github_com_containerd_ttrpc_request_proto_rawDescData = protoimpl.X.CompressGZIP(file_github_com_containerd_ttrpc_request_proto_rawDescData) + }) + return file_github_com_containerd_ttrpc_request_proto_rawDescData +} + +var file_github_com_containerd_ttrpc_request_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_github_com_containerd_ttrpc_request_proto_goTypes = []interface{}{ + (*Request)(nil), // 0: ttrpc.Request + (*Response)(nil), // 1: ttrpc.Response + (*StringList)(nil), // 2: ttrpc.StringList + (*KeyValue)(nil), // 3: ttrpc.KeyValue + (*status.Status)(nil), // 4: Status +} +var file_github_com_containerd_ttrpc_request_proto_depIdxs = []int32{ + 3, // 0: ttrpc.Request.metadata:type_name -> ttrpc.KeyValue + 4, // 1: ttrpc.Response.status:type_name -> Status + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_github_com_containerd_ttrpc_request_proto_init() } +func file_github_com_containerd_ttrpc_request_proto_init() { + if File_github_com_containerd_ttrpc_request_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_github_com_containerd_ttrpc_request_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Request); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_containerd_ttrpc_request_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_containerd_ttrpc_request_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StringList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_containerd_ttrpc_request_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_github_com_containerd_ttrpc_request_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_github_com_containerd_ttrpc_request_proto_goTypes, + DependencyIndexes: file_github_com_containerd_ttrpc_request_proto_depIdxs, + MessageInfos: file_github_com_containerd_ttrpc_request_proto_msgTypes, + }.Build() + File_github_com_containerd_ttrpc_request_proto = out.File + file_github_com_containerd_ttrpc_request_proto_rawDesc = nil + file_github_com_containerd_ttrpc_request_proto_goTypes = nil + file_github_com_containerd_ttrpc_request_proto_depIdxs = nil +} diff --git a/vendor/github.com/containerd/ttrpc/request.proto b/vendor/github.com/containerd/ttrpc/request.proto new file mode 100644 index 000000000..37da334fc --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/request.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package ttrpc; + +import "proto/status.proto"; + +option go_package = "github.com/containerd/ttrpc"; + +message Request { + string service = 1; + string method = 2; + bytes payload = 3; + int64 timeout_nano = 4; + repeated KeyValue metadata = 5; +} + +message Response { + Status status = 1; + bytes payload = 2; +} + +message StringList { + repeated string list = 1; +} + +message KeyValue { + string key = 1; + string value = 2; +} diff --git a/vendor/github.com/containerd/ttrpc/server.go b/vendor/github.com/containerd/ttrpc/server.go new file mode 100644 index 000000000..9606a975d --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/server.go @@ -0,0 +1,587 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "context" + "errors" + "io" + "math/rand" + "net" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/containerd/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Server struct { + config *serverConfig + services *serviceSet + codec codec + + mu sync.Mutex + listeners map[net.Listener]struct{} + connections map[*serverConn]struct{} // all connections to current state + done chan struct{} // marks point at which we stop serving requests +} + +func NewServer(opts ...ServerOpt) (*Server, error) { + config := &serverConfig{} + for _, opt := range opts { + if err := opt(config); err != nil { + return nil, err + } + } + if config.interceptor == nil { + config.interceptor = defaultServerInterceptor + } + + return &Server{ + config: config, + services: newServiceSet(config.interceptor), + done: make(chan struct{}), + listeners: make(map[net.Listener]struct{}), + connections: make(map[*serverConn]struct{}), + }, nil +} + +// Register registers a map of methods to method handlers +// TODO: Remove in 2.0, does not support streams +func (s *Server) Register(name string, methods map[string]Method) { + s.services.register(name, &ServiceDesc{Methods: methods}) +} + +func (s *Server) RegisterService(name string, desc *ServiceDesc) { + s.services.register(name, desc) +} + +func (s *Server) Serve(ctx context.Context, l net.Listener) error { + s.mu.Lock() + s.addListenerLocked(l) + defer s.closeListener(l) + + select { + case <-s.done: + s.mu.Unlock() + return ErrServerClosed + default: + } + s.mu.Unlock() + + var ( + backoff time.Duration + handshaker = s.config.handshaker + ) + + if handshaker == nil { + handshaker = handshakerFunc(noopHandshake) + } + + for { + conn, err := l.Accept() + if err != nil { + select { + case <-s.done: + return ErrServerClosed + default: + } + + if terr, ok := err.(interface { + Temporary() bool + }); ok && terr.Temporary() { + if backoff == 0 { + backoff = time.Millisecond + } else { + backoff *= 2 + } + + backoff = min(time.Second, backoff) + + sleep := time.Duration(rand.Int63n(int64(backoff))) + log.G(ctx).WithError(err).Errorf("ttrpc: failed accept; backoff %v", sleep) + time.Sleep(sleep) + continue + } + + return err + } + + backoff = 0 + + approved, handshake, err := handshaker.Handshake(ctx, conn) + if err != nil { + log.G(ctx).WithError(err).Error("ttrpc: refusing connection after handshake") + conn.Close() + continue + } + + sc, err := s.newConn(approved, handshake) + if err != nil { + log.G(ctx).WithError(err).Error("ttrpc: create connection failed") + conn.Close() + continue + } + + go sc.run(ctx) + } +} + +func (s *Server) Shutdown(ctx context.Context) error { + s.mu.Lock() + select { + case <-s.done: + default: + // protected by mutex + close(s.done) + } + lnerr := s.closeListeners() + s.mu.Unlock() + + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + for { + s.closeIdleConns() + + if s.countConnection() == 0 { + break + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } + + return lnerr +} + +// Close the server without waiting for active connections. +func (s *Server) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + + select { + case <-s.done: + default: + // protected by mutex + close(s.done) + } + + err := s.closeListeners() + for c := range s.connections { + c.close() + delete(s.connections, c) + } + + return err +} + +func (s *Server) addListenerLocked(l net.Listener) { + s.listeners[l] = struct{}{} +} + +func (s *Server) closeListener(l net.Listener) error { + s.mu.Lock() + defer s.mu.Unlock() + + return s.closeListenerLocked(l) +} + +func (s *Server) closeListenerLocked(l net.Listener) error { + defer delete(s.listeners, l) + return l.Close() +} + +func (s *Server) closeListeners() error { + var err error + for l := range s.listeners { + if cerr := s.closeListenerLocked(l); cerr != nil && err == nil { + err = cerr + } + } + return err +} + +func (s *Server) addConnection(c *serverConn) error { + s.mu.Lock() + defer s.mu.Unlock() + + select { + case <-s.done: + return ErrServerClosed + default: + } + + s.connections[c] = struct{}{} + return nil +} + +func (s *Server) delConnection(c *serverConn) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.connections, c) +} + +func (s *Server) countConnection() int { + s.mu.Lock() + defer s.mu.Unlock() + + return len(s.connections) +} + +func (s *Server) closeIdleConns() { + s.mu.Lock() + defer s.mu.Unlock() + + for c := range s.connections { + if st, ok := c.getState(); !ok || st == connStateActive { + continue + } + c.close() + delete(s.connections, c) + } +} + +type connState int + +const ( + connStateActive = iota + 1 // outstanding requests + connStateIdle // no requests + connStateClosed // closed connection +) + +func (cs connState) String() string { + switch cs { + case connStateActive: + return "active" + case connStateIdle: + return "idle" + case connStateClosed: + return "closed" + default: + return "unknown" + } +} + +func (s *Server) newConn(conn net.Conn, handshake interface{}) (*serverConn, error) { + c := &serverConn{ + server: s, + conn: conn, + handshake: handshake, + shutdown: make(chan struct{}), + } + c.setState(connStateIdle) + if err := s.addConnection(c); err != nil { + c.close() + return nil, err + } + return c, nil +} + +type serverConn struct { + server *Server + conn net.Conn + handshake interface{} // data from handshake, not used for now + state atomic.Value + + shutdownOnce sync.Once + shutdown chan struct{} // forced shutdown, used by close +} + +func (c *serverConn) getState() (connState, bool) { + cs, ok := c.state.Load().(connState) + return cs, ok +} + +func (c *serverConn) setState(newstate connState) { + c.state.Store(newstate) +} + +func (c *serverConn) close() error { + c.shutdownOnce.Do(func() { + close(c.shutdown) + }) + + return nil +} + +func (c *serverConn) run(sctx context.Context) { + type ( + response struct { + id uint32 + status *status.Status + data []byte + closeStream bool + streaming bool + } + ) + + var ( + ch = newChannel(c.conn) + ctx, cancel = context.WithCancel(sctx) + state connState = connStateIdle + responses = make(chan response) + recvErr = make(chan error, 1) + done = make(chan struct{}) + streams = sync.Map{} + active int32 + lastStreamID uint32 + ) + + defer c.conn.Close() + defer cancel() + defer close(done) + defer c.server.delConnection(c) + + sendStatus := func(id uint32, st *status.Status) bool { + select { + case responses <- response{ + // even though we've had an invalid stream id, we send it + // back on the same stream id so the client knows which + // stream id was bad. + id: id, + status: st, + closeStream: true, + }: + return true + case <-c.shutdown: + return false + case <-done: + return false + } + } + + go func(recvErr chan error) { + defer close(recvErr) + for { + select { + case <-c.shutdown: + return + case <-done: + return + default: // proceed + } + + mh, p, err := ch.recv() + if err != nil { + status, ok := status.FromError(err) + if !ok { + recvErr <- err + return + } + + // in this case, we send an error for that particular message + // when the status is defined. + if !sendStatus(mh.StreamID, status) { + return + } + + continue + } + + if mh.StreamID%2 != 1 { + // enforce odd client initiated identifiers. + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "StreamID must be odd for client initiated streams")) { + return + } + continue + } + + if mh.Type == messageTypeData { + i, ok := streams.Load(mh.StreamID) + if !ok { + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "StreamID is no longer active")) { + return + } + continue + } + sh := i.(*streamHandler) + if mh.Flags&flagNoData != flagNoData { + unmarshal := func(obj interface{}) error { + err := protoUnmarshal(p, obj) + ch.putmbuf(p) + return err + } + + if err := sh.data(unmarshal); err != nil { + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "data handling error: %v", err)) { + return + } + continue + } + } + + if mh.Flags&flagRemoteClosed == flagRemoteClosed { + sh.closeSend() + if len(p) > 0 { + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "data close message cannot include data")) { + return + } + continue + } + } + } else if mh.Type == messageTypeRequest { + if mh.StreamID <= lastStreamID { + // enforce odd client initiated identifiers. + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "StreamID cannot be re-used and must increment")) { + return + } + continue + + } + lastStreamID = mh.StreamID + + // TODO: Make request type configurable + // Unmarshaller which takes in a byte array and returns an interface? + var req Request + if err := c.server.codec.Unmarshal(p, &req); err != nil { + ch.putmbuf(p) + if !sendStatus(mh.StreamID, status.Newf(codes.InvalidArgument, "unmarshal request error: %v", err)) { + return + } + continue + } + ch.putmbuf(p) + + id := mh.StreamID + respond := func(status *status.Status, data []byte, streaming, closeStream bool) error { + select { + case responses <- response{ + id: id, + status: status, + data: data, + closeStream: closeStream, + streaming: streaming, + }: + case <-done: + return ErrClosed + } + return nil + } + sh, err := c.server.services.handle(ctx, &req, respond) + if err != nil { + status, _ := status.FromError(err) + if !sendStatus(mh.StreamID, status) { + return + } + continue + } + + streams.Store(id, sh) + atomic.AddInt32(&active, 1) + } + // TODO: else we must ignore this for future compat. log this? + } + }(recvErr) + + for { + var ( + newstate connState + shutdown chan struct{} + ) + + activeN := atomic.LoadInt32(&active) + if activeN > 0 { + newstate = connStateActive + shutdown = nil + } else { + newstate = connStateIdle + shutdown = c.shutdown // only enable this branch in idle mode + } + if newstate != state { + c.setState(newstate) + state = newstate + } + + select { + case response := <-responses: + if !response.streaming || response.status.Code() != codes.OK { + p, err := c.server.codec.Marshal(&Response{ + Status: response.status.Proto(), + Payload: response.data, + }) + if err != nil { + log.G(ctx).WithError(err).Error("failed marshaling response") + return + } + + if err := ch.send(response.id, messageTypeResponse, 0, p); err != nil { + log.G(ctx).WithError(err).Error("failed sending message on channel") + return + } + } else { + var flags uint8 + if response.closeStream { + flags = flagRemoteClosed + } + if response.data == nil { + flags = flags | flagNoData + } + if err := ch.send(response.id, messageTypeData, flags, response.data); err != nil { + log.G(ctx).WithError(err).Error("failed sending message on channel") + return + } + } + + if response.closeStream { + // The ttrpc protocol currently does not support the case where + // the server is localClosed but not remoteClosed. Once the server + // is closing, the whole stream may be considered finished + streams.Delete(response.id) + atomic.AddInt32(&active, -1) + } + case err := <-recvErr: + // TODO(stevvooe): Not wildly clear what we should do in this + // branch. Basically, it means that we are no longer receiving + // requests due to a terminal error. + recvErr = nil // connection is now "closing" + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, syscall.ECONNRESET) { + // The client went away and we should stop processing + // requests, so that the client connection is closed + return + } + log.G(ctx).WithError(err).Error("error receiving message") + // else, initiate shutdown + case <-shutdown: + return + } + } +} + +var noopFunc = func() {} + +func getRequestContext(ctx context.Context, req *Request) (retCtx context.Context, cancel func()) { + if len(req.Metadata) > 0 { + md := MD{} + md.fromRequest(req) + ctx = WithMetadata(ctx, md) + } + + cancel = noopFunc + if req.TimeoutNano == 0 { + return ctx, cancel + } + + ctx, cancel = context.WithTimeout(ctx, time.Duration(req.TimeoutNano)) + return ctx, cancel +} diff --git a/vendor/github.com/containerd/ttrpc/services.go b/vendor/github.com/containerd/ttrpc/services.go new file mode 100644 index 000000000..6d092bf95 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/services.go @@ -0,0 +1,279 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path" + "unsafe" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +type Method func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) + +type StreamHandler func(context.Context, StreamServer) (interface{}, error) + +type Stream struct { + Handler StreamHandler + StreamingClient bool + StreamingServer bool +} + +type ServiceDesc struct { + Methods map[string]Method + Streams map[string]Stream +} + +type serviceSet struct { + services map[string]*ServiceDesc + unaryInterceptor UnaryServerInterceptor + streamInterceptor StreamServerInterceptor +} + +func newServiceSet(interceptor UnaryServerInterceptor) *serviceSet { + return &serviceSet{ + services: make(map[string]*ServiceDesc), + unaryInterceptor: interceptor, + streamInterceptor: defaultStreamServerInterceptor, + } +} + +func (s *serviceSet) register(name string, desc *ServiceDesc) { + if _, ok := s.services[name]; ok { + panic(fmt.Errorf("duplicate service %v registered", name)) + } + + s.services[name] = desc +} + +func (s *serviceSet) unaryCall(ctx context.Context, method Method, info *UnaryServerInfo, data []byte) (p []byte, st *status.Status) { + unmarshal := func(obj interface{}) error { + return protoUnmarshal(data, obj) + } + + resp, err := s.unaryInterceptor(ctx, unmarshal, info, method) + if err == nil { + if isNil(resp) { + err = errors.New("ttrpc: marshal called with nil") + } else { + p, err = protoMarshal(resp) + } + } + + st, ok := status.FromError(err) + if !ok { + st = status.New(convertCode(err), err.Error()) + } + + return p, st +} + +func (s *serviceSet) streamCall(ctx context.Context, stream StreamHandler, info *StreamServerInfo, ss StreamServer) (p []byte, st *status.Status) { + resp, err := s.streamInterceptor(ctx, ss, info, stream) + if err == nil { + p, err = protoMarshal(resp) + } + st, ok := status.FromError(err) + if !ok { + st = status.New(convertCode(err), err.Error()) + } + return +} + +func (s *serviceSet) handle(ctx context.Context, req *Request, respond func(*status.Status, []byte, bool, bool) error) (*streamHandler, error) { + srv, ok := s.services[req.Service] + if !ok { + return nil, status.Errorf(codes.Unimplemented, "service %v", req.Service) + } + + if method, ok := srv.Methods[req.Method]; ok { + go func() { + ctx, cancel := getRequestContext(ctx, req) + defer cancel() + + info := &UnaryServerInfo{ + FullMethod: fullPath(req.Service, req.Method), + } + p, st := s.unaryCall(ctx, method, info, req.Payload) + + respond(st, p, false, true) + }() + return nil, nil + } + if stream, ok := srv.Streams[req.Method]; ok { + ctx, cancel := getRequestContext(ctx, req) + info := &StreamServerInfo{ + FullMethod: fullPath(req.Service, req.Method), + StreamingClient: stream.StreamingClient, + StreamingServer: stream.StreamingServer, + } + sh := &streamHandler{ + ctx: ctx, + respond: respond, + recv: make(chan Unmarshaler, 5), + info: info, + } + go func() { + defer cancel() + p, st := s.streamCall(ctx, stream.Handler, info, sh) + respond(st, p, stream.StreamingServer, true) + }() + + // Empty proto messages serialized to 0 payloads, + // so signatures like: rpc Stream(google.protobuf.Empty) returns (stream Data); + // don't get invoked here, which causes hang on client side. + // See https://github.com/containerd/ttrpc/issues/126 + if req.Payload != nil || !info.StreamingClient { + unmarshal := func(obj interface{}) error { + return protoUnmarshal(req.Payload, obj) + } + if err := sh.data(unmarshal); err != nil { + return nil, err + } + } + + return sh, nil + } + return nil, status.Errorf(codes.Unimplemented, "method %v", req.Method) +} + +type streamHandler struct { + ctx context.Context + respond func(*status.Status, []byte, bool, bool) error + recv chan Unmarshaler + info *StreamServerInfo + + remoteClosed bool + localClosed bool +} + +func (s *streamHandler) closeSend() { + if !s.remoteClosed { + s.remoteClosed = true + close(s.recv) + } +} + +func (s *streamHandler) data(unmarshal Unmarshaler) error { + if s.remoteClosed { + return ErrStreamClosed + } + select { + case s.recv <- unmarshal: + return nil + case <-s.ctx.Done(): + return s.ctx.Err() + } +} + +func (s *streamHandler) SendMsg(m interface{}) error { + if s.localClosed { + return ErrStreamClosed + } + p, err := protoMarshal(m) + if err != nil { + return err + } + return s.respond(nil, p, true, false) +} + +func (s *streamHandler) RecvMsg(m interface{}) error { + select { + case unmarshal, ok := <-s.recv: + if !ok { + return io.EOF + } + return unmarshal(m) + case <-s.ctx.Done(): + return s.ctx.Err() + + } +} + +func protoUnmarshal(p []byte, obj interface{}) error { + switch v := obj.(type) { + case proto.Message: + if err := proto.Unmarshal(p, v); err != nil { + return status.Errorf(codes.Internal, "ttrpc: error unmarshalling payload: %v", err.Error()) + } + default: + return status.Errorf(codes.Internal, "ttrpc: error unsupported request type: %T", v) + } + return nil +} + +func protoMarshal(obj interface{}) ([]byte, error) { + if obj == nil { + return nil, nil + } + + switch v := obj.(type) { + case proto.Message: + r, err := proto.Marshal(v) + if err != nil { + return nil, status.Errorf(codes.Internal, "ttrpc: error marshaling payload: %v", err.Error()) + } + + return r, nil + default: + return nil, status.Errorf(codes.Internal, "ttrpc: error unsupported response type: %T", v) + } +} + +// convertCode maps stdlib go errors into grpc space. +// +// This is ripped from the grpc-go code base. +func convertCode(err error) codes.Code { + switch err { + case nil: + return codes.OK + case io.EOF: + return codes.OutOfRange + case io.ErrClosedPipe, io.ErrNoProgress, io.ErrShortBuffer, io.ErrShortWrite, io.ErrUnexpectedEOF: + return codes.FailedPrecondition + case os.ErrInvalid: + return codes.InvalidArgument + case context.Canceled: + return codes.Canceled + case context.DeadlineExceeded: + return codes.DeadlineExceeded + } + switch { + case os.IsExist(err): + return codes.AlreadyExists + case os.IsNotExist(err): + return codes.NotFound + case os.IsPermission(err): + return codes.PermissionDenied + } + return codes.Unknown +} + +func fullPath(service, method string) string { + return "/" + path.Join(service, method) +} + +func isNil(resp interface{}) bool { + return (*[2]uintptr)(unsafe.Pointer(&resp))[1] == 0 +} diff --git a/vendor/github.com/containerd/ttrpc/stream.go b/vendor/github.com/containerd/ttrpc/stream.go new file mode 100644 index 000000000..739a4c967 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/stream.go @@ -0,0 +1,84 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +import ( + "context" + "sync" +) + +type streamID uint32 + +type streamMessage struct { + header messageHeader + payload []byte +} + +type stream struct { + id streamID + sender sender + recv chan *streamMessage + + closeOnce sync.Once + recvErr error + recvClose chan struct{} +} + +func newStream(id streamID, send sender) *stream { + return &stream{ + id: id, + sender: send, + recv: make(chan *streamMessage, 1), + recvClose: make(chan struct{}), + } +} + +func (s *stream) closeWithError(err error) error { + s.closeOnce.Do(func() { + if err != nil { + s.recvErr = err + } else { + s.recvErr = ErrClosed + } + close(s.recvClose) + }) + return nil +} + +func (s *stream) send(mt messageType, flags uint8, b []byte) error { + return s.sender.send(uint32(s.id), mt, flags, b) +} + +func (s *stream) receive(ctx context.Context, msg *streamMessage) error { + select { + case <-s.recvClose: + return s.recvErr + default: + } + select { + case <-s.recvClose: + return s.recvErr + case s.recv <- msg: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +type sender interface { + send(uint32, messageType, uint8, []byte) error +} diff --git a/vendor/github.com/containerd/ttrpc/stream_server.go b/vendor/github.com/containerd/ttrpc/stream_server.go new file mode 100644 index 000000000..b6d1ba720 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/stream_server.go @@ -0,0 +1,22 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ttrpc + +type StreamServer interface { + SendMsg(m interface{}) error + RecvMsg(m interface{}) error +} diff --git a/vendor/github.com/containerd/ttrpc/test.proto b/vendor/github.com/containerd/ttrpc/test.proto new file mode 100644 index 000000000..0e114d556 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/test.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package ttrpc; + +option go_package = "github.com/containerd/ttrpc/internal"; + +message TestPayload { + string foo = 1; + int64 deadline = 2; + string metadata = 3; +} + +message EchoPayload { + int64 seq = 1; + string msg = 2; +} diff --git a/vendor/github.com/containerd/ttrpc/unixcreds_linux.go b/vendor/github.com/containerd/ttrpc/unixcreds_linux.go new file mode 100644 index 000000000..c82c9f9d4 --- /dev/null +++ b/vendor/github.com/containerd/ttrpc/unixcreds_linux.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 ttrpc + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +type UnixCredentialsFunc func(*unix.Ucred) error + +func (fn UnixCredentialsFunc) Handshake(_ context.Context, conn net.Conn) (net.Conn, interface{}, error) { + uc, err := requireUnixSocket(conn) + if err != nil { + return nil, nil, fmt.Errorf("ttrpc.UnixCredentialsFunc: require unix socket: %w", err) + } + + rs, err := uc.SyscallConn() + if err != nil { + return nil, nil, fmt.Errorf("ttrpc.UnixCredentialsFunc: (net.UnixConn).SyscallConn failed: %w", err) + } + var ( + ucred *unix.Ucred + ucredErr error + ) + if err := rs.Control(func(fd uintptr) { + ucred, ucredErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + }); err != nil { + return nil, nil, fmt.Errorf("ttrpc.UnixCredentialsFunc: (*syscall.RawConn).Control failed: %w", err) + } + + if ucredErr != nil { + return nil, nil, fmt.Errorf("ttrpc.UnixCredentialsFunc: failed to retrieve socket peer credentials: %w", ucredErr) + } + + if err := fn(ucred); err != nil { + return nil, nil, fmt.Errorf("ttrpc.UnixCredentialsFunc: credential check failed: %w", err) + } + + return uc, ucred, nil +} + +// UnixSocketRequireUidGid requires specific *effective* UID/GID, rather than the real UID/GID. +// +// For example, if a daemon binary is owned by the root (UID 0) with SUID bit but running as an +// unprivileged user (UID 1001), the effective UID becomes 0, and the real UID becomes 1001. +// So calling this function with uid=0 allows a connection from effective UID 0 but rejects +// a connection from effective UID 1001. +// +// See socket(7), SO_PEERCRED: "The returned credentials are those that were in effect at the time of the call to connect(2) or socketpair(2)." +func UnixSocketRequireUidGid(uid, gid int) UnixCredentialsFunc { + return func(ucred *unix.Ucred) error { + return requireUidGid(ucred, uid, gid) + } +} + +func UnixSocketRequireRoot() UnixCredentialsFunc { + return UnixSocketRequireUidGid(0, 0) +} + +// UnixSocketRequireSameUser resolves the current effective unix user and returns a +// UnixCredentialsFunc that will validate incoming unix connections against the +// current credentials. +// +// This is useful when using abstract sockets that are accessible by all users. +func UnixSocketRequireSameUser() UnixCredentialsFunc { + euid, egid := os.Geteuid(), os.Getegid() + return UnixSocketRequireUidGid(euid, egid) +} + +func requireUidGid(ucred *unix.Ucred, uid, gid int) error { + if (uid != -1 && uint32(uid) != ucred.Uid) || (gid != -1 && uint32(gid) != ucred.Gid) { + return fmt.Errorf("ttrpc: invalid credentials: %v", syscall.EPERM) + } + return nil +} + +func requireUnixSocket(conn net.Conn) (*net.UnixConn, error) { + uc, ok := conn.(*net.UnixConn) + if !ok { + return nil, errors.New("a unix socket connection is required") + } + + return uc, nil +} diff --git a/vendor/github.com/pelletier/go-toml/v2/.dockerignore b/vendor/github.com/pelletier/go-toml/v2/.dockerignore new file mode 100644 index 000000000..7b5883475 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/.dockerignore @@ -0,0 +1,2 @@ +cmd/tomll/tomll +cmd/tomljson/tomljson diff --git a/vendor/github.com/pelletier/go-toml/v2/.gitattributes b/vendor/github.com/pelletier/go-toml/v2/.gitattributes new file mode 100644 index 000000000..34a0a21a3 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/.gitattributes @@ -0,0 +1,4 @@ +* text=auto + +benchmark/benchmark.toml text eol=lf +testdata/** text eol=lf diff --git a/vendor/github.com/pelletier/go-toml/v2/.gitignore b/vendor/github.com/pelletier/go-toml/v2/.gitignore new file mode 100644 index 000000000..eaf580dfd --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/.gitignore @@ -0,0 +1,8 @@ +test_program/test_program_bin +fuzz/ +cmd/tomll/tomll +cmd/tomljson/tomljson +cmd/tomltestgen/tomltestgen +dist +tests/ +test-results diff --git a/vendor/github.com/pelletier/go-toml/v2/.golangci.toml b/vendor/github.com/pelletier/go-toml/v2/.golangci.toml new file mode 100644 index 000000000..7d2e5b04c --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/.golangci.toml @@ -0,0 +1,76 @@ +version = "2" + +[linters] +default = "none" +enable = [ + "asciicheck", + "bodyclose", + "dogsled", + "dupl", + "durationcheck", + "errcheck", + "errorlint", + "exhaustive", + "forbidigo", + "gochecknoinits", + "goconst", + "gocritic", + "godoclint", + "goheader", + "gomodguard", + "goprintffuncname", + "gosec", + "govet", + "importas", + "ineffassign", + "lll", + "makezero", + "mirror", + "misspell", + "nakedret", + "nilerr", + "noctx", + "nolintlint", + "perfsprint", + "prealloc", + "predeclared", + "revive", + "rowserrcheck", + "sqlclosecheck", + "staticcheck", + "thelper", + "tparallel", + "unconvert", + "unparam", + "unused", + "usetesting", + "wastedassign", + "whitespace", +] + +[linters.settings.exhaustive] +default-signifies-exhaustive = true + +[linters.settings.lll] +line-length = 150 + +[[linters.exclusions.rules]] +path = ".test.go" +linters = ["goconst", "gosec"] + +[[linters.exclusions.rules]] +path = "main.go" +linters = ["forbidigo"] + +[[linters.exclusions.rules]] +path = "internal" +linters = ["revive"] +text = "(exported|indent-error-flow): " + +[formatters] +enable = [ + "gci", + "gofmt", + "gofumpt", + "goimports", +] diff --git a/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml b/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml new file mode 100644 index 000000000..3208f721b --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/.goreleaser.yaml @@ -0,0 +1,128 @@ +version: 2 +before: + hooks: + - go mod tidy + - go fmt ./... + - go test ./... +builds: + - id: tomll + main: ./cmd/tomll + binary: tomll + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} + mod_timestamp: '{{ .CommitTimestamp }}' + targets: + - linux_amd64 + - linux_arm64 + - linux_arm + - linux_riscv64 + - windows_amd64 + - windows_arm64 + - darwin_amd64 + - darwin_arm64 + - id: tomljson + main: ./cmd/tomljson + binary: tomljson + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} + mod_timestamp: '{{ .CommitTimestamp }}' + targets: + - linux_amd64 + - linux_arm64 + - linux_arm + - linux_riscv64 + - windows_amd64 + - windows_arm64 + - darwin_amd64 + - darwin_arm64 + - id: jsontoml + main: ./cmd/jsontoml + binary: jsontoml + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} + mod_timestamp: '{{ .CommitTimestamp }}' + targets: + - linux_amd64 + - linux_arm64 + - linux_riscv64 + - linux_arm + - windows_amd64 + - windows_arm64 + - darwin_amd64 + - darwin_arm64 +universal_binaries: + - id: tomll + replace: true + name_template: tomll + - id: tomljson + replace: true + name_template: tomljson + - id: jsontoml + replace: true + name_template: jsontoml +archives: +- id: jsontoml + formats: + - tar.xz + ids: + - jsontoml + files: + - none* + name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}" +- id: tomljson + formats: + - tar.xz + ids: + - tomljson + files: + - none* + name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}" +- id: tomll + formats: + - tar.xz + ids: + - tomll + files: + - none* + name_template: "{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}" +dockers_v2: + - id: tools + ids: + - jsontoml + - tomljson + - tomll + images: + - "ghcr.io/pelletier/go-toml" + tags: + - "latest" + - "{{ .Tag }}" + - "v{{ .Major }}" + platforms: + - linux/amd64 +checksum: + name_template: 'sha256sums.txt' +snapshot: + version_template: "{{ incpatch .Version }}-next" +release: + github: + owner: pelletier + name: go-toml + draft: true + prerelease: auto + mode: replace +changelog: + use: github-native +announce: + skip: true diff --git a/vendor/github.com/pelletier/go-toml/v2/AGENTS.md b/vendor/github.com/pelletier/go-toml/v2/AGENTS.md new file mode 100644 index 000000000..f495afd87 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/AGENTS.md @@ -0,0 +1,73 @@ +# Agent Guidelines for go-toml + +This file provides guidelines for AI agents contributing to go-toml. All agents must follow these rules derived from [CONTRIBUTING.md](./CONTRIBUTING.md). + +## Project Overview + +go-toml is a TOML library for Go. The goal is to provide an easy-to-use and efficient TOML implementation that gets the job done without getting in the way. + +## Code Change Rules + +### Backward Compatibility + +- **No backward-incompatible changes** unless explicitly discussed and approved +- Avoid breaking people's programs unless absolutely necessary + +### Testing Requirements + +- **All bug fixes must include regression tests** +- **All new code must be tested** +- Run tests before submitting: `go test -race ./...` +- Test coverage must not decrease. Check with: + ```bash + go test -covermode=atomic -coverprofile=coverage.out + go tool cover -func=coverage.out + ``` +- All lines of code touched by changes should be covered by tests + +### Performance Requirements + +- go-toml aims to stay efficient; avoid performance regressions +- Run benchmarks to verify: `go test ./... -bench=. -count=10` +- Compare results using [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) + +### Documentation + +- New features or feature extensions must include documentation +- Documentation lives in [README.md](./README.md) and throughout source code + +### Code Style + +- Follow existing code format and structure +- Code must pass `go fmt` +- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`): + ```bash + # Install specific version (check lint.yml for current version) + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin + # Run linter + golangci-lint run ./... + ``` + +### Commit Messages + +- Commit messages must explain **why** the change is needed +- Keep messages clear and informative even if details are in the PR description + +### Capabilities + +go-toml tracks system-level capabilities using [capslock](https://github.com/google/capslock). The baseline is in `capability_baseline.txt` and CI enforces that it does not grow. + +- **Do not introduce new capabilities.** PRs that increase the capability set (e.g., adding network access, subprocess execution, syscalls) are unlikely to be accepted. +- If a change causes the capabilities check to fail, do not update the baseline to make it pass. Instead, rethink the approach to avoid requiring new capabilities. +- To check locally: `./caps.sh check` (requires `capslock` installed via `go install github.com/google/capslock/cmd/capslock@latest`) + +## Pull Request Checklist + +Before submitting: + +1. Tests pass (`go test -race ./...`) +2. No backward-incompatible changes (unless discussed) +3. Relevant documentation added/updated +4. No performance regression (verify with benchmarks) +5. Capabilities are not increasing (`./caps.sh check`) +6. Title is clear and understandable for changelog diff --git a/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md b/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md new file mode 100644 index 000000000..65a3ff494 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/CONTRIBUTING.md @@ -0,0 +1,254 @@ +# Contributing + +Thank you for your interest in go-toml! We appreciate you considering +contributing to go-toml! + +The main goal is the project is to provide an easy-to-use and efficient TOML +implementation for Go that gets the job done and gets out of your way – dealing +with TOML is probably not the central piece of your project. + +As the single maintainer of go-toml, time is scarce. All help, big or small, is +more than welcomed! + +## Ask questions + +Any question you may have, somebody else might have it too. Always feel free to +ask them on the [discussion board][discussions]. We will try to answer them as +clearly and quickly as possible, time permitting. + +Asking questions also helps us identify areas where the documentation needs +improvement, or new features that weren't envisioned before. Sometimes, a +seemingly innocent question leads to the fix of a bug. Don't hesitate and ask +away! + +[discussions]: https://github.com/pelletier/go-toml/discussions + +## Improve the documentation + +The best way to share your knowledge and experience with go-toml is to improve +the documentation. Fix a typo, clarify an interface, add an example, anything +goes! + +The documentation is present in the [README][readme] and thorough the source +code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change +to the documentation, create a pull request with your proposed changes. For +simple changes like that, the easiest way to go is probably the "Fork this +project and edit the file" button on GitHub, displayed at the top right of the +file. Unless it's a trivial change (for example a typo), provide a little bit of +context in your pull request description or commit message. + +## Report a bug + +Found a bug! Sorry to hear that :(. Help us and other track them down and fix by +reporting it. [File a new bug report][bug-report] on the [issues +tracker][issues-tracker]. The template should provide enough guidance on what to +include. When in doubt: add more details! By reducing ambiguity and providing +more information, it decreases back and forth and saves everyone time. + +## Code changes + +Want to contribute a patch? Very happy to hear that! + +First, some high-level rules: + +- A short proposal with some POC code is better than a lengthy piece of text + with no code. Code speaks louder than words. That being said, bigger changes + should probably start with a [discussion][discussions]. +- No backward-incompatible patch will be accepted unless discussed. Sometimes + it's hard, but we try not to break people's programs unless we absolutely have + to. +- If you are writing a new feature or extending an existing one, make sure to + write some documentation. +- Bug fixes need to be accompanied with regression tests. +- New code needs to be tested. +- Your commit messages need to explain why the change is needed, even if already + included in the PR description. + +It does sound like a lot, but those best practices are here to save time overall +and continuously improve the quality of the project, which is something everyone +benefits from. + +### Get started + +The fairly standard code contribution process looks like that: + +1. [Fork the project][fork]. +2. Make your changes, commit on any branch you like. +3. [Open up a pull request][pull-request] +4. Review, potential ask for changes. +5. Merge. + +Feel free to ask for help! You can create draft pull requests to gather +some early feedback! + +### Run the tests + +You can run tests for go-toml using Go's test tool: `go test -race ./...`. + +During the pull request process, all tests will be ran on Linux, Windows, and +MacOS on the last two versions of Go. + +However, given GitHub's new policy to _not_ run Actions on pull requests until a +maintainer clicks on button, it is highly recommended that you run them locally +as you make changes. + +### Test across Go versions + +The repository includes tooling to test go-toml across multiple Go versions +(1.11 through 1.25) both locally and in GitHub Actions. + +#### Local testing with Docker + +Prerequisites: Docker installed and running, Bash shell, `rsync` command. + +```bash +# Test all Go versions in parallel (default) +./test-go-versions.sh + +# Test specific versions +./test-go-versions.sh 1.21 1.22 1.23 + +# Test sequentially (slower but uses less resources) +./test-go-versions.sh --sequential + +# Verbose output with custom results directory +./test-go-versions.sh --verbose --output ./my-results 1.24 1.25 + +# Show all options +./test-go-versions.sh --help +``` + +The script creates Docker containers for each Go version and runs the full test +suite. Results are saved to a `test-results/` directory with individual logs and +a comprehensive summary report. + +The script only exits with a non-zero status code if either of the two most +recent Go versions fail. + +#### GitHub Actions testing (maintainers) + +1. Go to the **Actions** tab in the GitHub repository +2. Select **"Go Versions Compatibility Test"** from the workflow list +3. Click **"Run workflow"** +4. Optionally customize: + - **Go versions**: Space-separated list (e.g., `1.21 1.22 1.23`) + - **Execution mode**: Parallel (faster) or sequential (more stable) + +### Check coverage + +We use `go tool cover` to compute test coverage. Most code editors have a way to +run and display code coverage, but at the end of the day, we do this: + +``` +go test -covermode=atomic -coverprofile=coverage.out +go tool cover -func=coverage.out +``` + +and verify that the overall percentage of tested code does not go down. This is +a requirement. As a rule of thumb, all lines of code touched by your changes +should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your +code lowers the coverage. + +### Verify performance + +Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's +builtin benchmark systems. Because of their noisy nature, containers provided by +GitHub Actions cannot be reliably used for benchmarking. As a result, you are +responsible for checking that your changes do not incur a performance penalty. +You can run their following to execute benchmarks: + +``` +go test ./... -bench=. -count=10 +``` + +Benchmark results should be compared against each other with +[benchstat][benchstat]. Typical flow looks like this: + +1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to + a file (for example `old.txt`). +2. Make some code changes. +3. Run `go test ....` again, and save the output to an other file (for example + `new.txt`). +4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any + test. + +On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts +performance. + +It is highly encouraged to add the benchstat results to your pull request +description. Pull requests that lower performance will receive more scrutiny. + +[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat + +### Capabilities + +We use [capslock](https://github.com/google/capslock) to track what +system-level capabilities (file access, network, syscalls, etc.) each package +requires. The current baseline is in `capability_baseline.txt`. CI will fail if +a change introduces a new capability. + +**Pull requests that increase the set of capabilities are unlikely to be +accepted.** go-toml is a parsing library and should not need network access, +subprocess execution, or other capabilities beyond what it already uses. + +If you believe a new capability is genuinely needed, discuss it in an issue +first. To update the baseline after approval: + +```bash +go install github.com/google/capslock/cmd/capslock@latest +./caps.sh generate +``` + +### Style + +Try to look around and follow the same format and structure as the rest of the +code. We enforce using `go fmt` on the whole code base. + +--- + +## Maintainers-only + +### Merge pull request + +Checklist: + +- Passing CI. +- Does not introduce backward-incompatible changes (unless discussed). +- Has relevant doc changes. +- Benchstat does not show performance regression. +- Pull request is [labeled appropriately][pr-labels]. +- Title will be understandable in the changelog. + +1. Merge using "squash and merge". +2. Make sure to edit the commit message to keep all the useful information + nice and clean. +3. Make sure the commit title is clear and contains the PR number (#123). + +### New release + +1. Decide on the next version number. Use semver. Review commits since last + version to assess. +2. Tag release. For example: + ``` + git checkout v2 + git pull + git tag v2.2.0 + git push --tags + ``` +3. CI automatically builds a draft GitHub release. Review it and edit as + necessary. Look for "Other changes". That would indicate a pull request not + labeled properly. Tweak labels and pull request titles until changelog looks + good for users. +4. Check "create discussion" box, in the "Releases" category. +5. If new version is an alpha or beta only, check pre-release box. + + +[issues-tracker]: https://github.com/pelletier/go-toml/issues +[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md +[pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml +[readme]: ./README.md +[fork]: https://help.github.com/articles/fork-a-repo +[pull-request]: https://help.github.com/en/articles/creating-a-pull-request +[new-release]: https://github.com/pelletier/go-toml/releases/new +[gh]: https://github.com/cli/cli +[pr-labels]: https://github.com/pelletier/go-toml/blob/v2/.github/release.yml diff --git a/vendor/github.com/pelletier/go-toml/v2/Dockerfile b/vendor/github.com/pelletier/go-toml/v2/Dockerfile new file mode 100644 index 000000000..ebd9babac --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/Dockerfile @@ -0,0 +1,6 @@ +FROM scratch +ENV PATH "$PATH:/bin" +ARG TARGETPLATFORM +COPY $TARGETPLATFORM/tomll /bin/tomll +COPY $TARGETPLATFORM/tomljson /bin/tomljson +COPY $TARGETPLATFORM/jsontoml /bin/jsontoml diff --git a/vendor/github.com/pelletier/go-toml/v2/LICENSE b/vendor/github.com/pelletier/go-toml/v2/LICENSE new file mode 100644 index 000000000..991e2ae96 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +go-toml v2 +Copyright (c) 2021 - 2023 Thomas Pelletier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/pelletier/go-toml/v2/README.md b/vendor/github.com/pelletier/go-toml/v2/README.md new file mode 100644 index 000000000..067ba3fd7 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/README.md @@ -0,0 +1,327 @@ +# go-toml v2 + +Go library for the [TOML](https://toml.io/en/) format. + +This library supports [TOML v1.1.0](https://toml.io/en/v1.1.0). + +[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues) + +[💬 Anything else](https://github.com/pelletier/go-toml/discussions) + +## Documentation + +Full API, examples, and implementation notes are available in the Go +documentation. + +[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2) + +## Import + +```go +import "github.com/pelletier/go-toml/v2" +``` + +## Features + +### Stdlib behavior + +As much as possible, this library is designed to behave similarly as the +standard library's `encoding/json`. + +When encoding structs, fields tagged with `omitempty` are omitted if they are +empty. For `time.Time`, the zero value is considered empty, so timestamps such +as `created_at` or `updated_at` are not written unless you remove `omitempty` +from the struct tag or use a pointer type (`*time.Time`). + +### Performance + +While go-toml favors usability, it is written with performance in mind. Most +operations should not be shockingly slow. See [benchmarks](#benchmarks). + +### Strict mode + +`Decoder` can be set to "strict mode", which makes it error when some parts of +the TOML document was not present in the target structure. This is a great way +to check for typos. [See example in the documentation][strict]. + +[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.DisallowUnknownFields + +### Contextualized errors + +When most decoding errors occur, go-toml returns [`DecodeError`][decode-err], +which contains a human readable contextualized version of the error. For +example: + +``` +1| [server] +2| path = 100 + | ~~~ cannot decode TOML integer into struct field toml_test.Server.Path of type string +3| port = 50 +``` + +[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError + +### Local date and time support + +TOML supports native [local date/times][ldt]. It allows to represent a given +date, time, or date-time without relation to a timezone or offset. To support +this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and +[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`, +making them convenient yet unambiguous structures for their respective TOML +representation. + +[ldt]: https://toml.io/en/v1.1.0#local-date-time +[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate +[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime +[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime + +### Commented config + +Since TOML is often used for configuration files, go-toml can emit documents +annotated with [comments and commented-out values][comments-example]. For +example, it can generate the following file: + +```toml +# Host IP to connect to. +host = '127.0.0.1' +# Port of the remote server. +port = 4242 + +# Encryption parameters (optional) +# [TLS] +# cipher = 'AEAD-AES128-GCM-SHA256' +# version = 'TLS 1.3' +``` + +[comments-example]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Marshal-Commented + +## Getting started + +Given the following struct, let's see how to read it and write it as TOML: + +```go +type MyConfig struct { + Version int + Name string + Tags []string +} +``` + +### Unmarshaling + +[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its +content. + +Note that the struct variable names are _capitalized_, while the variables in the toml document are _lowercase_. + +For example: + +```go +doc := ` +version = 2 +name = "go-toml" +tags = ["go", "toml"] +` + +var cfg MyConfig +err := toml.Unmarshal([]byte(doc), &cfg) +if err != nil { + panic(err) +} +fmt.Println("version:", cfg.Version) +fmt.Println("name:", cfg.Name) +fmt.Println("tags:", cfg.Tags) + +// Output: +// version: 2 +// name: go-toml +// tags: [go toml] +``` + +[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal + + +Here is an example using tables with some simple nesting: + +```go +doc := ` +age = 45 +fruits = ["apple", "pear"] + +# these are very important! +[my-variables] +first = 1 +second = 0.2 +third = "abc" + +# this is not so important. +[my-variables.b] +bfirst = 123 +` + +var Document struct { + Age int + Fruits []string + + Myvariables struct { + First int + Second float64 + Third string + + B struct { + Bfirst int + } + } `toml:"my-variables"` +} + +err := toml.Unmarshal([]byte(doc), &Document) +if err != nil { + panic(err) +} + +fmt.Println("age:", Document.Age) +fmt.Println("fruits:", Document.Fruits) +fmt.Println("my-variables.first:", Document.Myvariables.First) +fmt.Println("my-variables.second:", Document.Myvariables.Second) +fmt.Println("my-variables.third:", Document.Myvariables.Third) +fmt.Println("my-variables.B.Bfirst:", Document.Myvariables.B.Bfirst) + +// Output: +// age: 45 +// fruits: [apple pear] +// my-variables.first: 1 +// my-variables.second: 0.2 +// my-variables.third: abc +// my-variables.B.Bfirst: 123 +``` + + +### Marshaling + +[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure +as a TOML document: + +```go +cfg := MyConfig{ + Version: 2, + Name: "go-toml", + Tags: []string{"go", "toml"}, +} + +b, err := toml.Marshal(cfg) +if err != nil { + panic(err) +} +fmt.Println(string(b)) + +// Output: +// Version = 2 +// Name = 'go-toml' +// Tags = ['go', 'toml'] +``` + +[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal + +## Unstable API + +This API does not yet follow the backward compatibility guarantees of this +library. They provide early access to features that may have rough edges or an +API subject to change. + +### Parser + +Parser is the unstable API that allows iterative parsing of a TOML document at +the AST level. See https://pkg.go.dev/github.com/pelletier/go-toml/v2/unstable. + +## Benchmarks + +Execution time speedup compared to other Go TOML libraries: + + + + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/HugoFrontMatter-22.3x2.4x
Marshal/ReferenceFile/map-22.2x2.6x
Marshal/ReferenceFile/struct-24.9x5.0x
Unmarshal/HugoFrontMatter-27.8x5.9x
Unmarshal/ReferenceFile/map-26.8x6.4x
Unmarshal/ReferenceFile/struct-26.8x6.3x
+
See more +

The table above has the results of the most common use-cases. The table below +contains the results of all benchmarks, including unrealistic ones. It is +provided for completeness.

+ + + + + + + + + + + + + + + + + + +
Benchmarkgo-toml v1BurntSushi/toml
Marshal/SimpleDocument/map-22.1x3.1x
Marshal/SimpleDocument/struct-23.4x4.8x
Unmarshal/SimpleDocument/map-210.1x7.0x
Unmarshal/SimpleDocument/struct-212.4x8.0x
UnmarshalDataset/example-28.2x6.9x
UnmarshalDataset/code-27.5x8.3x
UnmarshalDataset/twitter-29.0x7.6x
UnmarshalDataset/citm_catalog-25.0x4.5x
UnmarshalDataset/canada-26.4x4.7x
UnmarshalDataset/config-210.2x6.1x
geomean5.8x5.3x
+

This table can be generated with ./ci.sh benchmark -a -html.

+
+ +## Tools + +Go-toml provides three handy command line tools: + + * `tomljson`: Reads a TOML file and outputs its JSON representation. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomljson@latest + $ tomljson --help + ``` + + * `jsontoml`: Reads a JSON file and outputs a TOML representation. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/jsontoml@latest + $ jsontoml --help + ``` + + * `tomll`: Lints and reformats a TOML file. + + ``` + $ go install github.com/pelletier/go-toml/v2/cmd/tomll@latest + $ tomll --help + ``` + +### Docker image + +Those tools are also available as a [Docker image][docker]. For example, to use +`tomljson`: + +``` +docker run -i ghcr.io/pelletier/go-toml:v2 tomljson < example.toml +``` + +Multiple versions are available on [ghcr.io][docker]. + +[docker]: https://github.com/pelletier/go-toml/pkgs/container/go-toml + +## Versioning + +Expect for parts explicitly marked otherwise, go-toml follows [Semantic +Versioning](https://semver.org). The supported version of +[TOML](https://github.com/toml-lang/toml) is indicated at the beginning of this +document. The last two major versions of Go are supported (see [Go Release +Policy](https://golang.org/doc/devel/release.html#policy)). + +## License + +The MIT License (MIT). Read [LICENSE](LICENSE). diff --git a/vendor/github.com/pelletier/go-toml/v2/SECURITY.md b/vendor/github.com/pelletier/go-toml/v2/SECURITY.md new file mode 100644 index 000000000..d4d554fda --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ---------- | ------------------ | +| Latest 2.x | :white_check_mark: | +| All 1.x | :x: | +| All 0.x | :x: | + +## Reporting a Vulnerability + +Email a vulnerability report to `security@pelletier.codes`. Make sure to include +as many details as possible to reproduce the vulnerability. This is a +side-project: I will try to get back to you as quickly as possible, time +permitting in my personal life. Providing a working patch helps very much! diff --git a/vendor/github.com/pelletier/go-toml/v2/capability_baseline.txt b/vendor/github.com/pelletier/go-toml/v2/capability_baseline.txt new file mode 100644 index 000000000..c556456c9 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/capability_baseline.txt @@ -0,0 +1 @@ +github.com/pelletier/go-toml/v2: CAPABILITY_REFLECT, CAPABILITY_UNANALYZED, CAPABILITY_UNSAFE_POINTER diff --git a/vendor/github.com/pelletier/go-toml/v2/caps.sh b/vendor/github.com/pelletier/go-toml/v2/caps.sh new file mode 100644 index 000000000..31fec2b96 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/caps.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# Generates or checks the capability baseline for go-toml. +# +# Usage: +# ./caps.sh generate # regenerate capability_baseline.txt +# ./caps.sh check # check that capabilities haven't grown +# +# Requires: go, capslock (go install github.com/google/capslock/cmd/capslock@latest) + +set -euo pipefail + +BASELINE="capability_baseline.txt" +CAPSLOCK="${CAPSLOCK:-capslock}" + +# Capabilities that must never appear in any package. +FORBIDDEN_CAPS=( + CAPABILITY_NETWORK + CAPABILITY_CGO + CAPABILITY_EXEC +) + +capslock_to_baseline() { + "$CAPSLOCK" -packages=. -output=package -granularity=package \ + | jq -r 'to_entries | sort_by(.key) | .[] | .key + ": " + (.value | sort | join(", "))' +} + +generate() { + capslock_to_baseline > "$BASELINE" + echo "Wrote $BASELINE" +} + +check() { + if [ ! -f "$BASELINE" ]; then + echo "ERROR: $BASELINE not found. Run '$0 generate' first." + exit 1 + fi + + current=$(mktemp) + trap 'rm -f "$current"' EXIT + + capslock_to_baseline > "$current" + + failed=0 + + # Check for forbidden capabilities in current output. + for cap in "${FORBIDDEN_CAPS[@]}"; do + if grep -q "$cap" "$current"; then + echo "FORBIDDEN capability found: $cap" + grep "$cap" "$current" + failed=1 + fi + done + + # Extract all unique capability names from baseline and current. + baseline_caps=$(grep -oE 'CAPABILITY_[A-Z_]+' "$BASELINE" | sort -u) + current_caps=$(grep -oE 'CAPABILITY_[A-Z_]+' "$current" | sort -u) + + # Check for new capability names not in the baseline. + new_caps=$(comm -13 <(echo "$baseline_caps") <(echo "$current_caps")) + if [ -n "$new_caps" ]; then + echo "NEW capabilities detected (not in baseline):" + echo "$new_caps" + failed=1 + fi + + # Check for new per-package capabilities (a package gained a capability it didn't have before). + while IFS=': ' read -r pkg caps; do + baseline_pkg_caps=$(grep "^${pkg}:" "$BASELINE" 2>/dev/null | sed 's/^[^:]*: //' || true) + if [ -z "$baseline_pkg_caps" ]; then + echo "NEW package with capabilities: $pkg: $caps" + failed=1 + continue + fi + # Check each capability in current for this package + for cap in $(echo "$caps" | tr ', ' '\n' | grep -v '^$'); do + if ! echo "$baseline_pkg_caps" | grep -q "$cap"; then + echo "NEW capability for $pkg: $cap" + failed=1 + fi + done + done < "$current" + + if [ "$failed" -eq 1 ]; then + echo "" + echo "FAILED: capabilities have grown." + echo "If this is intentional, run '$0 generate' and commit the updated $BASELINE." + exit 1 + fi + + echo "OK: no new capabilities detected." +} + +case "${1:-}" in + generate) generate ;; + check) check ;; + *) + echo "Usage: $0 {generate|check}" + exit 1 + ;; +esac diff --git a/vendor/github.com/pelletier/go-toml/v2/ci.sh b/vendor/github.com/pelletier/go-toml/v2/ci.sh new file mode 100644 index 000000000..6b9723bf4 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/ci.sh @@ -0,0 +1,293 @@ +#!/usr/bin/env bash + + +stderr() { + echo "$@" 1>&2 +} + +usage() { + b=$(basename "$0") + echo $b: ERROR: "$@" 1>&2 + + cat 1>&2 < coverage.out + go tool cover -func=coverage.out + echo "Coverage profile for ${branch}: ${dir}/coverage.out" >&2 + popd + + if [ "${branch}" != "HEAD" ]; then + git worktree remove --force "$dir" + fi +} + +coverage() { + case "$1" in + -d) + shift + target="${1?Need to provide a target branch argument}" + + output_dir="$(mktemp -d)" + target_out="${output_dir}/target.txt" + head_out="${output_dir}/head.txt" + + cover "${target}" > "${target_out}" + cover "HEAD" > "${head_out}" + + cat "${target_out}" + cat "${head_out}" + + echo "" + + target_pct="$(tail -n2 ${target_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%.*/\1/')" + head_pct="$(tail -n2 ${head_out} | head -n1 | sed -E 's/.*total.*\t([0-9.]+)%/\1/')" + echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%" + + delta_pct=$(echo "$head_pct - $target_pct" | bc -l) + echo "Delta: ${delta_pct}" + + if [[ $delta_pct = \-* ]]; then + echo "Regression!"; + + target_diff="${output_dir}/target.diff.txt" + head_diff="${output_dir}/head.diff.txt" + cat "${target_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${target_diff}" + cat "${head_out}" | grep -E '^github.com/pelletier/go-toml' | tr -s "\t " | cut -f 2,3 | sort > "${head_diff}" + + diff --side-by-side --suppress-common-lines "${target_diff}" "${head_diff}" + return 1 + fi + return 0 + ;; + esac + + cover "${1-HEAD}" +} + +bench() { + branch="${1}" + out="${2}" + replace="${3}" + dir="$(mktemp -d)" + + stderr "Executing benchmark for ${branch} at ${dir}" + + if [ "${branch}" = "HEAD" ]; then + cp -r . "${dir}/" + else + git worktree add "$dir" "$branch" + fi + + pushd "$dir" + + tags="" + if [ "${replace}" != "" ]; then + find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2\"|${replace}\"|g" {} \; + go get "${replace}" + # The realworld benchmarks use v2-only API and cannot compile against + # the other libraries; exclude them from cross-library comparisons. + tags="-tags cross_library_benchmark" + fi + + export GOMAXPROCS=2 + go test ${tags} '-bench=^Benchmark(Un)?[mM]arshal' -count=10 -run=Nothing ./... | tee "${out}" + popd + + if [ "${branch}" != "HEAD" ]; then + git worktree remove --force "$dir" + fi +} + +fmktemp() { + if mktemp --version &> /dev/null; then + # GNU + mktemp --suffix=-$1 + else + # BSD + mktemp -t $1 + fi +} + +benchstathtml() { +python3 - $1 <<'EOF' +import sys + +lines = [] +stop = False + +with open(sys.argv[1]) as f: + for line in f.readlines(): + line = line.strip() + if line == "": + stop = True + if not stop: + lines.append(line.split(',')) + +results = [] +for line in reversed(lines[2:]): + if len(line) < 8 or line[0] == "": + continue + v2 = float(line[1]) + results.append([ + line[0].replace("-32", ""), + "%.1fx" % (float(line[3])/v2), # v1 + "%.1fx" % (float(line[7])/v2), # bs + ]) + +if not results: + print("No benchmark results to display.", file=sys.stderr) + sys.exit(1) + +# move geomean to the end +results.append(results[0]) +del results[0] + + +def printtable(data): + print(""" + + + + + """) + + for r in data: + print(" ".format(*r)) + + print(""" +
Benchmarkgo-toml v1BurntSushi/toml
{}{}{}
""") + + +def match(x): + return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0] + +above = [x for x in results if match(x)] +below = [x for x in results if not match(x)] + +printtable(above) +print("
See more") +print("""

The table above has the results of the most common use-cases. The table below +contains the results of all benchmarks, including unrealistic ones. It is +provided for completeness.

""") +printtable(below) +print('

This table can be generated with ./ci.sh benchmark -a -html.

') +print("
") + +EOF +} + +benchmark() { + case "$1" in + -d) + shift + target="${1?Need to provide a target branch argument}" + + old=`fmktemp ${target}` + bench "${target}" "${old}" + + new=`fmktemp HEAD` + bench HEAD "${new}" + + benchstat "${old}" "${new}" + return 0 + ;; + -a) + shift + + v2stats=`fmktemp go-toml-v2` + bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2" + v1stats=`fmktemp go-toml-v1` + bench HEAD "${v1stats}" "github.com/pelletier/go-toml" + bsstats=`fmktemp bs-toml` + bench HEAD "${bsstats}" "github.com/BurntSushi/toml" + + cp "${v2stats}" go-toml-v2.txt + cp "${v1stats}" go-toml-v1.txt + cp "${bsstats}" bs-toml.txt + + if [ "$1" = "-html" ]; then + tmpcsv=`fmktemp csv` + benchstat -format csv go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv + benchstathtml $tmpcsv + else + benchstat go-toml-v2.txt go-toml-v1.txt bs-toml.txt + fi + + rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt + return $? + esac + + bench "${1-HEAD}" `mktemp` +} + +case "$1" in + coverage) shift; coverage $@;; + benchmark) shift; benchmark $@;; + *) usage "bad argument $1";; +esac diff --git a/vendor/github.com/pelletier/go-toml/v2/decode.go b/vendor/github.com/pelletier/go-toml/v2/decode.go new file mode 100644 index 000000000..46de2fb44 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/decode.go @@ -0,0 +1,538 @@ +package toml + +import ( + "bytes" + "fmt" + "math" + "strconv" + "time" + + "github.com/pelletier/go-toml/v2/unstable" +) + +func parseInteger(b []byte) (int64, error) { + if len(b) > 2 && b[0] == '0' { + switch b[1] { + case 'x': + return parseIntHex(b) + case 'b': + return parseIntBin(b) + case 'o': + return parseIntOct(b) + default: + panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1])) + } + } + return parseIntDec(b) +} + +func parseIntHex(b []byte) (int64, error) { + var v uint64 + for _, c := range b[2:] { + if c == '_' { + continue + } + var d byte + switch { + case c >= '0' && c <= '9': + d = c - '0' + case c >= 'a' && c <= 'f': + d = c - 'a' + 10 + case c >= 'A' && c <= 'F': + d = c - 'A' + 10 + } + if v > math.MaxInt64>>4 { + return 0, unstable.NewParserError(b, "hexadecimal number is too large to fit in a 64-bit signed integer") + } + v = v<<4 | uint64(d) + } + return int64(v), nil +} + +func parseIntOct(b []byte) (int64, error) { + var v uint64 + for _, c := range b[2:] { + if c == '_' { + continue + } + if v > math.MaxInt64>>3 { + return 0, unstable.NewParserError(b, "octal number is too large to fit in a 64-bit signed integer") + } + v = v<<3 | uint64(c-'0') + } + return int64(v), nil +} + +func parseIntBin(b []byte) (int64, error) { + var v uint64 + for _, c := range b[2:] { + if c == '_' { + continue + } + if v > math.MaxInt64>>1 { + return 0, unstable.NewParserError(b, "binary number is too large to fit in a 64-bit signed integer") + } + v = v<<1 | uint64(c-'0') + } + return int64(v), nil +} + +func parseIntDec(b []byte) (int64, error) { + i := 0 + neg := false + switch b[0] { + case '-': + neg = true + i++ + case '+': + i++ + } + + var limit uint64 = math.MaxInt64 + if neg { + limit = math.MaxInt64 + 1 + } + + var v uint64 + for ; i < len(b); i++ { + c := b[i] + if c == '_' { + continue + } + if v > limit/10 { + return 0, unstable.NewParserError(b, "decimal number is too large to fit in a 64-bit signed integer") + } + v = v*10 + uint64(c-'0') + if v > limit { + return 0, unstable.NewParserError(b, "decimal number is too large to fit in a 64-bit signed integer") + } + } + if neg { + return -int64(v), nil //nolint:gosec // v <= MaxInt64+1, the conversion wraps to the intended negative value + } + return int64(v), nil //nolint:gosec // v <= MaxInt64 +} + +func parseFloat(b []byte) (float64, error) { + i := 0 + if len(b) > 0 && (b[0] == '+' || b[0] == '-') { + i = 1 + } + if len(b) == i+3 { + switch b[i] { + case 'i': + // inf + if b[0] == '-' { + return math.Inf(-1), nil + } + return math.Inf(1), nil + case 'n': + // nan + return math.NaN(), nil + } + } + + // Fast path: a plain decimal whose significand fits in 53 bits and whose + // base-10 exponent is within [-22, 22] is parsed exactly with a single + // rounding (Clinger's method) straight from the bytes, with no string + // allocation and no full strconv parse. This is the common shape for + // numeric data (e.g. coordinate lists). Anything outside those bounds, or + // with underscores, falls through to strconv, which is the reference. + if f, ok := fastParseFloat(b); ok { + return f, nil + } + + // strconv.ParseFloat is the reference implementation for parsing + // floating point numbers. The position of underscores has already been + // validated by the parser; strip them so that they do not interfere with + // Go's own underscore rules. + cleaned := b + if bytes.IndexByte(b, '_') >= 0 { + cleaned = make([]byte, 0, len(b)) + for _, c := range b { + if c != '_' { + cleaned = append(cleaned, c) + } + } + } + + f, err := strconv.ParseFloat(string(cleaned), 64) + if err != nil { + return 0, unstable.NewParserError(b, "unable to parse float: %s", err) + } + return f, nil +} + +// float64pow10 holds the powers of ten that are exactly representable as a +// float64 (10^0 .. 10^22). +var float64pow10 = [...]float64{ + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, +} + +// fastParseFloat parses b as a float64 using Clinger's exact method and reports +// whether it applied. It accepts only plain decimal numbers (optional sign, +// digits, one optional '.', optional 'e'/'E' exponent) whose significand fits +// in 53 bits and whose effective base-10 exponent is within [-22, 22]; under +// those conditions float64(significand) * 10^exp (or / 10^-exp) is the exact, +// correctly-rounded result, identical to strconv.ParseFloat. It returns +// ok=false (deferring to strconv) for underscores, hexadecimal floats, large +// significands or exponents, and any other shape. +func fastParseFloat(b []byte) (float64, bool) { + i := 0 + neg := false + if i < len(b) && (b[i] == '+' || b[i] == '-') { + neg = b[i] == '-' + i++ + } + + var mantissa uint64 + digits := 0 + fracDigits := 0 + sawDot := false + sawDigit := false + for ; i < len(b); i++ { + c := b[i] + switch { + case c >= '0' && c <= '9': + if digits >= 19 { + // Too many significant digits to accumulate without risking a + // uint64 overflow (and well past the 53-bit exact range). + return 0, false + } + mantissa = mantissa*10 + uint64(c-'0') + digits++ + if sawDot { + fracDigits++ + } + sawDigit = true + case c == '.': + if sawDot { + return 0, false + } + sawDot = true + default: + goto exponent + } + } +exponent: + if !sawDigit { + return 0, false + } + exp := -fracDigits + if i < len(b) && (b[i] == 'e' || b[i] == 'E') { + i++ + esign := 1 + if i < len(b) && (b[i] == '+' || b[i] == '-') { + if b[i] == '-' { + esign = -1 + } + i++ + } + if i >= len(b) { + return 0, false + } + eval := 0 + for ; i < len(b); i++ { + c := b[i] + if c < '0' || c > '9' { + return 0, false + } + eval = eval*10 + int(c-'0') + if eval > 1000 { + return 0, false + } + } + exp += esign * eval + } + if i != len(b) { + // Trailing bytes (an underscore, a hexadecimal marker, ...). + return 0, false + } + if mantissa > 1<<53 { + return 0, false + } + + f := float64(mantissa) + switch { + case exp == 0: + case exp > 0 && exp <= 22: + f *= float64pow10[exp] + case exp < 0 && exp >= -22: + f /= float64pow10[-exp] + default: + return 0, false + } + if neg { + f = -f + } + return f, true +} + +func isDecimalDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +// parseLocalDate parses a date of the exact form YYYY-MM-DD and validates +// its components. +func parseLocalDate(b []byte) (LocalDate, error) { + var date LocalDate + + if len(b) != 10 || b[4] != '-' || b[7] != '-' { + return date, unstable.NewParserError(b, "dates are expected to have the format YYYY-MM-DD") + } + + var err error + date.Year, err = parseDecimalDigits(b[0:4]) + if err != nil { + return date, err + } + date.Month, err = parseDecimalDigits(b[5:7]) + if err != nil { + return date, err + } + date.Day, err = parseDecimalDigits(b[8:10]) + if err != nil { + return date, err + } + + if date.Month < 1 || date.Month > 12 { + return date, unstable.NewParserError(b[5:7], "impossible date") + } + maxDay := daysIn(date.Month, date.Year) + if date.Day < 1 || date.Day > maxDay { + return date, unstable.NewParserError(b[8:10], "impossible date") + } + + return date, nil +} + +func daysIn(month int, year int) int { + switch month { + case 2: + if isLeapYear(year) { + return 29 + } + return 28 + case 4, 6, 9, 11: + return 30 + default: + return 31 + } +} + +func isLeapYear(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +// parseDecimalDigits parses a sequence of digits as a decimal number. +func parseDecimalDigits(b []byte) (int, error) { + v := 0 + for i, c := range b { + if !isDecimalDigit(c) { + return 0, unstable.NewParserError(b[i:i+1], "expected digit (0-9)") + } + v = v*10 + int(c-'0') + } + return v, nil +} + +// parseLocalTime parses a time of the form HH:MM with optional seconds and an +// optional fractional part (TOML v1.1.0). It returns the remaining bytes after +// the time. +func parseLocalTime(b []byte) (LocalTime, []byte, error) { + var ( + nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0} + t LocalTime + ) + + // check if b matches to have expected format HH:MM[:SS[.NNNNNN]] + const localTimeByteMinLen = 5 + if len(b) < localTimeByteMinLen { + return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM[:SS[.NNNNNN]]") + } + + var err error + t.Hour, err = parseDecimalDigits(b[0:2]) + if err != nil { + return t, nil, err + } + if t.Hour > 23 { + return t, nil, unstable.NewParserError(b[0:2], "hour cannot be greater 23") + } + if b[2] != ':' { + return t, nil, unstable.NewParserError(b[2:3], "expecting colon between hours and minutes") + } + + t.Minute, err = parseDecimalDigits(b[3:5]) + if err != nil { + return t, nil, err + } + if t.Minute > 59 { + return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59") + } + + b = b[5:] + + // Seconds are optional (TOML v1.1.0). Fractional seconds may only appear + // when seconds are present: + // partial-time = time-hour ":" time-minute [ ":" time-second [ time-secfrac ] ] + secondsPresent := false + + if len(b) >= 1 && b[0] == ':' { + if len(b) < 3 { + return t, nil, unstable.NewParserError(b, "incomplete seconds") + } + + t.Second, err = parseDecimalDigits(b[1:3]) + if err != nil { + return t, nil, err + } + + if t.Second > 59 { + return t, nil, unstable.NewParserError(b[1:3], "seconds cannot be greater than 59") + } + + b = b[3:] + secondsPresent = true + } + + if secondsPresent && len(b) >= 1 && b[0] == '.' { + frac := 0 + precision := 0 + digits := 0 + + for i, c := range b[1:] { + if !isDecimalDigit(c) { + if i == 0 { + return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point") + } + break + } + digits++ + if i < 9 { + frac = frac*10 + int(c-'0') + precision++ + } + } + + if digits == 0 { + return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point") + } + + t.Nanosecond = frac * nspow[precision] + t.Precision = precision + + return t, b[1+digits:], nil + } + return t, b, nil +} + +// parseLocalDateTime parses a local date time of the form +// YYYY-MM-DD(T| )HH:MM:SS[.NNNNNN]. It returns the remaining bytes after the +// date-time. +func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) { + var dt LocalDateTime + + const localDateTimeByteMinLen = 11 + if len(b) < localDateTimeByteMinLen { + return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM[:SS[.NNNNNNNNN]]") + } + + date, err := parseLocalDate(b[:10]) + if err != nil { + return dt, nil, err + } + dt.LocalDate = date + + sep := b[10] + if sep != 'T' && sep != ' ' && sep != 't' { + return dt, nil, unstable.NewParserError(b[10:11], "datetime separator is expected to be T or a space") + } + + t, rest, err := parseLocalTime(b[11:]) + if err != nil { + return dt, nil, err + } + dt.LocalTime = t + + return dt, rest, nil +} + +// parseDateTime parses a date-time with a timezone offset (Z or +/-HH:MM). +func parseDateTime(b []byte) (time.Time, error) { + dt, b, err := parseLocalDateTime(b) + if err != nil { + return time.Time{}, err + } + + var zone *time.Location + + if len(b) == 0 { + // parser should have checked that there is a timezone + return time.Time{}, unstable.NewParserError(b, "date-time is missing timezone") + } + + if b[0] == 'Z' || b[0] == 'z' { + b = b[1:] + zone = time.UTC + } else { + const dateTimeByteLen = 6 + if len(b) != dateTimeByteLen { + return time.Time{}, unstable.NewParserError(b, "invalid date-time timezone") + } + var direction int + switch b[0] { + case '-': + direction = -1 + case '+': + direction = +1 + default: + return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character") + } + + if b[3] != ':' { + return time.Time{}, unstable.NewParserError(b[3:4], "expected a : separator") + } + + hours, err := parseDecimalDigits(b[1:3]) + if err != nil { + return time.Time{}, err + } + if hours > 23 { + return time.Time{}, unstable.NewParserError(b[1:3], "invalid timezone offset hours") + } + + minutes, err := parseDecimalDigits(b[4:6]) + if err != nil { + return time.Time{}, err + } + if minutes > 59 { + return time.Time{}, unstable.NewParserError(b[4:6], "invalid timezone offset minutes") + } + + seconds := direction * (hours*3600 + minutes*60) + if seconds == 0 { + zone = time.UTC + } else { + zone = time.FixedZone("", seconds) + } + b = b[dateTimeByteLen:] + } + + if len(b) > 0 { + return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone") + } + + t := time.Date( + dt.Year, + time.Month(dt.Month), + dt.Day, + dt.Hour, + dt.Minute, + dt.Second, + dt.Nanosecond, + zone) + + return t, nil +} diff --git a/vendor/github.com/pelletier/go-toml/v2/decode_fused.go b/vendor/github.com/pelletier/go-toml/v2/decode_fused.go new file mode 100644 index 000000000..9de2d6522 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/decode_fused.go @@ -0,0 +1,362 @@ +package toml + +import ( + "errors" + "reflect" + "strings" + + "github.com/pelletier/go-toml/v2/internal/parserbridge" + "github.com/pelletier/go-toml/v2/unstable" +) + +// unmarshalFused decodes a whole document into a native map[string]interface{} +// tree with no reflection on the document structure, and without building an +// AST for table headers and scalar key-values. Only container values (arrays +// and inline tables) are parsed into the parser arena, so that the seen-tracker +// can validate them and decodeAny can presize the resulting slices and maps — +// the AST is what makes that cheap O(1) presizing possible. +// +// It is used when the target is a fully generic value (interface{} or +// map[string]interface{}) and the unmarshaler interface is disabled. The +// seen-tracker validates the document (duplicate keys, type consistency), so +// the builder creates and merges containers without revalidating. Strict mode +// never applies to a generic target (a map has no "unknown fields"), and +// captures never apply (a generic value implements no Unmarshaler). +func (d *decoder) unmarshalFused(root reflect.Value, data []byte) error { + var m map[string]interface{} + if !root.IsNil() { + // Decode into (merge with) an existing generic map when present. + if em, ok := root.Interface().(map[string]interface{}); ok { + m = em + } + } + if m == nil { + m = map[string]interface{}{} + } + + if err := d.fusedDocument(m, data); err != nil { + return d.wrapFusedError(data, err) + } + + if root.CanSet() { + root.Set(reflect.ValueOf(m)) + } + return nil +} + +// fusedDocument runs the top-level expression loop, mirroring +// Parser.NextExpression but storing values directly into native maps. +func (d *decoder) fusedDocument(m map[string]interface{}, b []byte) error { + cur := m + for { + b = fusedSkipWS(b) + if len(b) == 0 { + return nil + } + switch b[0] { + case '\n': + b = b[1:] + case '\r': + if len(b) > 1 && b[1] == '\n' { + b = b[2:] + continue + } + return unstable.NewParserError(b[:1], "expected newline but got %#U", b[0]) + case '#': + _, rest, err := parserbridge.ScanComment(b) + if err != nil { + return err + } + rest, err = fusedConsumeEOL(rest) + if err != nil { + return err + } + b = rest + case '[': + rest, err := d.fusedTable(b, m, &cur) + if err != nil { + return err + } + b = rest + default: + rest, err := d.fusedKeyVal(b, cur) + if err != nil { + return err + } + b = rest + } + } +} + +// fusedTable handles a [table] or [[array table]] header. b starts at '['. It +// updates *cur to the table the following key-values belong to. +func (d *decoder) fusedTable(b []byte, root map[string]interface{}, cur *map[string]interface{}) ([]byte, error) { + arrayTable := len(b) > 1 && b[1] == '[' + + var start []byte + if arrayTable { + start = fusedSkipWS(b[2:]) + } else { + start = fusedSkipWS(b[1:]) + } + + var err error + var rawKey []byte + d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, start, d.keyParts[:0]) + if err != nil { + return nil, err + } + + if arrayTable { + if len(b) < 2 || b[0] != ']' || b[1] != ']' { + return nil, unstable.NewParserError(fusedHL1(b), "expected ']]' to close array table name") + } + b = b[2:] + } else { + if len(b) == 0 || b[0] != ']' { + return nil, unstable.NewParserError(fusedHL1(b), "expected ']' to close table name") + } + b = b[1:] + } + + // The whole expression (including its line termination) is parsed before + // it is validated, to keep error precedence identical to the AST path. + b, err = d.fusedFinishLine(b) + if err != nil { + return nil, err + } + + if arrayTable { + first, err := d.seen.CheckArrayTable(d.keyParts) + if err != nil { + return nil, d.fusedSeenError(rawKey, d.keyParts, err) + } + *cur = d.anyArrayTableParts(root, d.keyParts, first) + } else { + if _, err := d.seen.CheckTable(d.keyParts); err != nil { + return nil, d.fusedSeenError(rawKey, d.keyParts, err) + } + *cur = d.anyTableParts(root, d.keyParts) + } + return b, nil +} + +// fusedKeyVal handles a `key = value` expression relative to the current table +// cur. b starts at the first character of the key. +func (d *decoder) fusedKeyVal(b []byte, cur map[string]interface{}) ([]byte, error) { + var err error + var rawKey []byte + d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, b, d.keyParts[:0]) + if err != nil { + return nil, err + } + if len(b) == 0 || b[0] != '=' { + return nil, unstable.NewParserError(fusedHL1(b), "expected '=' after key") + } + b = fusedSkipWS(b[1:]) + if len(b) == 0 { + return nil, unstable.NewParserError(b, "expected value, not end of input") + } + + if c := b[0]; c == '[' || c == '{' { + // Container value: build its AST so the seen-tracker can validate it + // and decodeAny can presize the resulting slices and maps. + nodeAny, rest, err := parserbridge.ParseValue(&d.p, b) + if err != nil { + return nil, err + } + node := nodeAny.(*unstable.Node) + rest, err = d.fusedFinishLine(rest) + if err != nil { + return nil, err + } + leafID, err := d.seen.CheckKeyValue(d.keyParts) + if err != nil { + return nil, d.fusedSeenError(rawKey, d.keyParts, err) + } + if err := d.seen.CheckValueUnder(leafID, node); err != nil { + return nil, d.fusedSeenError(rawKey, d.keyParts, err) + } + av, err := d.decodeAny(node) + if err != nil { + return nil, err + } + d.setFusedLeaf(cur, d.keyParts, av) + return rest, nil + } + + // Scalar value: scan it without building a node, then validate and convert + // it natively. + k, _, value, rest, err := parserbridge.ScanScalar(&d.p, b) + if err != nil { + return nil, err + } + kind := unstable.Kind(k) + rest, err = d.fusedFinishLine(rest) + if err != nil { + return nil, err + } + if _, err := d.seen.CheckKeyValue(d.keyParts); err != nil { + return nil, d.fusedSeenError(rawKey, d.keyParts, err) + } + av, err := d.fusedScalar(kind, value) + if err != nil { + return nil, err + } + d.setFusedLeaf(cur, d.keyParts, av) + return rest, nil +} + +// fusedSeenError turns a bare error returned by a SeenTracker parts-method +// into a ParserError carrying the position (the raw key span) and key path of +// the offending expression, so that it is reported as a DecodeError with +// context. It mirrors decoder.wrapSeenError for the fused (AST-less) path. +func (d *decoder) fusedSeenError(rawKey []byte, parts [][]byte, err error) error { + key := make(Key, len(parts)) + for i, p := range parts { + key[i] = string(p) + } + return &unstable.ParserError{ + Highlight: rawKey, + Message: strings.TrimPrefix(err.Error(), "toml: "), + Key: key, + } +} + +// fusedScalar converts a scanned scalar value into the native Go value used +// for generic targets. It mirrors the scalar cases of decodeAny. +func (d *decoder) fusedScalar(kind unstable.Kind, value []byte) (interface{}, error) { + switch kind { + case unstable.String: + return string(value), nil + case unstable.Integer: + i, err := parseInteger(value) + return i, err + case unstable.Float: + f, err := parseFloat(value) + return f, err + case unstable.Bool: + return value[0] == 't', nil + case unstable.DateTime: + t, err := parseDateTime(value) + return t, err + case unstable.LocalDateTime: + dt, rest, err := parseLocalDateTime(value) + if err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, unstable.NewParserError(rest, "extra characters at the end of a local date time") + } + return dt, nil + case unstable.LocalDate: + date, err := parseLocalDate(value) + return date, err + case unstable.LocalTime: + t, rest, err := parseLocalTime(value) + if err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, unstable.NewParserError(rest, "extra characters at the end of a local time") + } + return t, nil + default: + return nil, unstable.NewParserError(value, "unsupported value kind %s", kind) + } +} + +// anyTableParts navigates a [table] header (given its key parts) to the map it +// designates, creating intermediate tables as needed. +func (d *decoder) anyTableParts(m map[string]interface{}, parts [][]byte) map[string]interface{} { + cur := m + for _, p := range parts { + cur = d.anyChildTable(cur, d.intern(p)) + } + return cur +} + +// anyArrayTableParts navigates a [[array table]] header (given its key parts), +// appends a fresh element to the designated array, and returns it. first is +// true the first time this header is seen, in which case any pre-existing array +// (from a reused target) is reset. +func (d *decoder) anyArrayTableParts(m map[string]interface{}, parts [][]byte, first bool) map[string]interface{} { + cur := m + name := d.intern(parts[0]) + for i := 1; i < len(parts); i++ { + cur = d.anyChildTable(cur, name) + name = d.intern(parts[i]) + } + s, _ := cur[name].([]interface{}) + if first { + s = s[:0] + } + elem := map[string]interface{}{} + cur[name] = append(s, elem) + return elem +} + +// setFusedLeaf assigns av at the (possibly dotted) key parts within cur, +// creating intermediate maps as needed. +func (d *decoder) setFusedLeaf(cur map[string]interface{}, parts [][]byte, av interface{}) { + for i := 0; i < len(parts)-1; i++ { + cur = d.anyChildTable(cur, d.intern(parts[i])) + } + cur[d.intern(parts[len(parts)-1])] = av +} + +// wrapFusedError gives document context to errors produced by the fused +// decoder. +func (d *decoder) wrapFusedError(data []byte, err error) error { + var perr *unstable.ParserError + if errors.As(err, &perr) && len(perr.Highlight) == 0 { + // Mirror NextExpression: give end-of-input errors a usable position by + // extending the empty highlight to the last byte of the document. + if offset := cap(data) - cap(perr.Highlight); offset > 0 && offset == len(data) { + perr.Highlight = data[offset-1 : offset] + } + } + return d.wrapError(data, err) +} + +func fusedSkipWS(b []byte) []byte { + for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { + b = b[1:] + } + return b +} + +func fusedConsumeEOL(b []byte) ([]byte, error) { + if len(b) == 0 { + return b, nil + } + switch b[0] { + case '\n': + return b[1:], nil + case '\r': + if len(b) > 1 && b[1] == '\n' { + return b[2:], nil + } + } + return nil, unstable.NewParserError(b[:1], "expected newline but got %#U", b[0]) +} + +// fusedFinishLine consumes `ws [comment] (newline|eof)` after an expression. +func (d *decoder) fusedFinishLine(b []byte) ([]byte, error) { + b = fusedSkipWS(b) + if len(b) > 0 && b[0] == '#' { + _, rest, err := parserbridge.ScanComment(b) + if err != nil { + return nil, err + } + b = rest + } + return fusedConsumeEOL(b) +} + +func fusedHL1(b []byte) []byte { + if len(b) > 0 { + return b[:1] + } + return b +} diff --git a/vendor/github.com/pelletier/go-toml/v2/doc.go b/vendor/github.com/pelletier/go-toml/v2/doc.go new file mode 100644 index 000000000..b7bc599bd --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/doc.go @@ -0,0 +1,2 @@ +// Package toml is a library to read and write TOML documents. +package toml diff --git a/vendor/github.com/pelletier/go-toml/v2/errors.go b/vendor/github.com/pelletier/go-toml/v2/errors.go new file mode 100644 index 000000000..8e10b128d --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/errors.go @@ -0,0 +1,233 @@ +package toml + +import ( + "errors" + "strconv" + "strings" + + "github.com/pelletier/go-toml/v2/unstable" +) + +// DecodeError represents an error encountered during the parsing or decoding +// of a TOML document. +// +// In addition to the error message, it contains the position in the document +// where it happened, as well as a human-readable representation that shows +// where the error occurred in the document. +type DecodeError struct { + message string + line int + column int + key Key + + human string +} + +// StrictMissingError occurs in a TOML document that does not have a +// corresponding field in the target value. It contains all the missing fields +// in Errors. +// +// Emitted by Decoder when DisallowUnknownFields() was called. +type StrictMissingError struct { + // One error per field that could not be found. + Errors []DecodeError +} + +// Error returns the canonical string for this error. +func (s *StrictMissingError) Error() string { + return "strict mode: fields in the document are missing in the target struct" +} + +// String returns a human readable description of all errors. +func (s *StrictMissingError) String() string { + var buf strings.Builder + + for i, e := range s.Errors { + if i > 0 { + buf.WriteString("\n---\n") + } + buf.WriteString(e.String()) + } + + return buf.String() +} + +// Unwrap returns wrapped decode errors +// +// Implements errors.Join() interface. +func (s *StrictMissingError) Unwrap() []error { + errs := make([]error, len(s.Errors)) + for i := range s.Errors { + errs[i] = &s.Errors[i] + } + return errs +} + +// Key represents a TOML key as a sequence of key parts. +type Key []string + +// Error returns the error message contained in the DecodeError. +func (e *DecodeError) Error() string { + return "toml: " + e.message +} + +// String returns the human-readable contextualized error. This string is +// multi-line. +func (e *DecodeError) String() string { + return e.human +} + +// Position returns the (line, column) pair indicating where the error +// occurred in the document. Positions are 1-indexed. +func (e *DecodeError) Position() (row int, column int) { + return e.line, e.column +} + +// Key that was being processed when the error occurred. +func (e *DecodeError) Key() Key { + return e.key +} + +// wrapDecodeError creates a DecodeError from a ParserError. The highlight of +// the ParserError needs to be a subslice of the document. +func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError { + if de == nil { + return nil + } + return newDecodeError(document, de.Highlight, de.Key, de.Message) +} + +// newDecodeError creates a DecodeError pointing at the given highlight, which +// needs to be a subslice of the document. +func newDecodeError(document []byte, highlight []byte, key Key, message string) *DecodeError { + offset := subsliceOffset(document, highlight) + + errLineIdx, errColumn := positionAt(document, offset) + + human := buildHumanContext(document, errLineIdx, errColumn, len(highlight), message) + + return &DecodeError{ + message: message, + line: errLineIdx + 1, + column: errColumn, + key: key, + human: human, + } +} + +// subsliceOffset returns the offset of the subslice b within the document. +func subsliceOffset(document, b []byte) int { + // Highlights are subslices of the document, which means they share the + // same backing array, and their capacity counts the bytes between their + // start and the end of the backing array. + offset := cap(document) - cap(b) + if offset < 0 || offset+len(b) > len(document) { + panic(errors.New("highlight is not a subslice of the document")) + } + return offset +} + +// positionAt returns the 0-indexed line and the 1-indexed column of the given +// offset in the document. +func positionAt(document []byte, offset int) (lineIdx int, column int) { + lineStart := 0 + for i := 0; i < offset; i++ { + if document[i] == '\n' { + lineIdx++ + lineStart = i + 1 + } + } + return lineIdx, offset - lineStart + 1 +} + +// docLines splits the document into lines, removing the trailing newline +// characters. +func docLines(document []byte) []string { + s := string(document) + lines := strings.Split(s, "\n") + for i, l := range lines { + lines[i] = strings.TrimSuffix(l, "\r") + } + return lines +} + +// buildHumanContext renders the human-readable multi-line context of an +// error: a window of up to 3 lines before and after the error line, with +// the error position underlined. +func buildHumanContext(document []byte, errLineIdx, errColumn, highlightLen int, message string) string { + lines := docLines(document) + + const window = 3 + firstIdx := errLineIdx - window + if firstIdx < 0 { + firstIdx = 0 + } + lastIdx := errLineIdx + window + if lastIdx > len(lines)-1 { + lastIdx = len(lines) - 1 + } + // Empty lines at the edges of the window are dropped, unless the error + // is about that very position. + for firstIdx < errLineIdx && lines[firstIdx] == "" { + firstIdx++ + } + for lastIdx > errLineIdx && lines[lastIdx] == "" { + lastIdx-- + } + + // Width of the column of line numbers. + width := len(strconv.Itoa(lastIdx + 1)) + + var buf strings.Builder + + writeLine := func(idx int) { + number := strconv.Itoa(idx + 1) + for i := len(number); i < width; i++ { + buf.WriteByte(' ') + } + buf.WriteString(number) + buf.WriteByte('|') + if len(lines[idx]) > 0 { + buf.WriteByte(' ') + buf.WriteString(lines[idx]) + } + buf.WriteByte('\n') + } + + for idx := firstIdx; idx <= errLineIdx; idx++ { + writeLine(idx) + } + + // Underline the error. + for i := 0; i < width; i++ { + buf.WriteByte(' ') + } + buf.WriteString("| ") + for i := 1; i < errColumn; i++ { + buf.WriteByte(' ') + } + // The highlight cannot extend past the end of its line. + tildes := highlightLen + if errLineIdx < len(lines) { + if avail := len(lines[errLineIdx]) - errColumn + 1; tildes > avail { + tildes = avail + } + } + if tildes < 1 { + tildes = 1 + } + for i := 0; i < tildes; i++ { + buf.WriteByte('~') + } + if message != "" { + buf.WriteByte(' ') + buf.WriteString(message) + } + buf.WriteByte('\n') + + for idx := errLineIdx + 1; idx <= lastIdx; idx++ { + writeLine(idx) + } + + return strings.TrimSuffix(buf.String(), "\n") +} diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/parserbridge/parserbridge.go b/vendor/github.com/pelletier/go-toml/v2/internal/parserbridge/parserbridge.go new file mode 100644 index 000000000..703aceea4 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/internal/parserbridge/parserbridge.go @@ -0,0 +1,37 @@ +// Package parserbridge exposes the unstable parser's non-AST scanners to the +// root toml package without making them part of the unstable public API. +// +// The fused generic-decode fast path needs to scan keys, scalars and comments +// (and parse container values into the arena) without going through the +// AST-pushing NextExpression/Expression methods. Those scanners depend on +// Parser internals (the string-unescape scratch buffer and the node arena), so +// they have to live in the unstable package; but they are an implementation +// detail of the decoder, not something we want to commit to in the public API. +// +// The unstable package populates these variables in its init; the toml package +// reads them. The parser is passed as an any (it is always an *unstable.Parser) +// and the scalar kind is an int (it is always an unstable.Kind) so that this +// package imports neither unstable nor toml, avoiding an import cycle. Passing +// a pointer through an interface does not allocate, so the fused path keeps its +// allocation profile. +package parserbridge + +var ( + // ScanScalar scans a single scalar value (string, integer, float, bool or + // date/time) without building an AST node. kind is an unstable.Kind. + ScanScalar func(p any, b []byte) (kind int, raw, value, rest []byte, err error) + + // ScanKey scans a (possibly dotted) key without building AST nodes, + // appending each decoded part to dst. + ScanKey func(p any, b []byte, dst [][]byte) (parts [][]byte, raw, rest []byte, err error) + + // ScanComment scans a comment starting at '#', returning the comment bytes + // (including '#', excluding the line ending) and the rest of the input. It + // needs no parser state. + ScanComment func(b []byte) (comment, rest []byte, err error) + + // ParseValue parses a single value (including arrays and inline tables) into + // the parser arena, returning the root *unstable.Node and the rest of the + // input. + ParseValue func(p any, b []byte) (node any, rest []byte, err error) +) diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go new file mode 100644 index 000000000..661b11cf9 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/key.go @@ -0,0 +1,49 @@ +package tracker + +import "github.com/pelletier/go-toml/v2/unstable" + +// KeyTracker is a tracker that keeps track of the current Key as the AST is +// walked. +type KeyTracker struct { + k []string +} + +// UpdateTable sets the state of the tracker with the AST table node. +func (t *KeyTracker) UpdateTable(node *unstable.Node) { + t.reset() + t.Push(node) +} + +// UpdateArrayTable sets the state of the tracker with the AST array table +// node. +func (t *KeyTracker) UpdateArrayTable(node *unstable.Node) { + t.reset() + t.Push(node) +} + +// Push the given key on the stack. +func (t *KeyTracker) Push(node *unstable.Node) { + it := node.Key() + for it.Next() { + t.k = append(t.k, string(it.Node().Data)) + } +} + +// Pop key from stack. +func (t *KeyTracker) Pop(node *unstable.Node) { + it := node.Key() + for it.Next() { + t.k = t.k[:len(t.k)-1] + } +} + +// Key returns the current key. +func (t *KeyTracker) Key() []string { + k := make([]string, len(t.k)) + copy(k, t.k) + return k +} + +func (t *KeyTracker) reset() { + t.k = t.k[:0] +} diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go new file mode 100644 index 000000000..e88cd9415 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/seen.go @@ -0,0 +1,450 @@ +package tracker + +import ( + "bytes" + "fmt" + + "github.com/pelletier/go-toml/v2/unstable" +) + +type keyKind uint8 + +const ( + invalidKind keyKind = iota + // valueKind is a regular value (scalar, array, or inline table). It + // cannot be extended. + valueKind + // kvTableKind is a table created implicitly by a dotted key. It can only + // be extended by other dotted keys. + kvTableKind + // tableKind is a table created by a [header]. The explicit flag tells + // whether the table was created by its own header (true) or as an + // intermediate step of a longer key (false). + tableKind + // arrayTableKind is an array of tables created by [[header]]. + arrayTableKind + // anonymousKind is an entry that cannot be looked up by name. It serves + // as the parent of the content of inline tables stored inside arrays. + anonymousKind +) + +func (k keyKind) String() string { + switch k { + case invalidKind: + return "invalid" + case valueKind: + return "value" + case kvTableKind: + return "kv-table" + case tableKind: + return "table" + case arrayTableKind: + return "array-table" + case anonymousKind: + return "anonymous" + } + panic("missing keyKind string mapping") +} + +// entry represents a node that has been seen in the document. Its size has a +// direct impact on the performance of unmarshaling documents: keep it as +// small as possible. +type entry struct { + parent int32 + kind keyKind + explicit bool + name []byte +} + +// SeenTracker tracks which keys have been seen with which TOML type to flag +// duplicates and mismatches according to the spec. +// +// Each node in the visited tree is represented by an entry. Each entry has +// an identifier, which is provided by a counter. Entries are stored in the +// array entries. As new nodes are discovered (referenced for the first time +// in the TOML document), entries are created and appended to the array. An +// entry points to its parent using its id. +// +// To find whether a given key (sequence of []byte) has already been visited, +// the entries are linearly searched, looking for one with the right name and +// parent id. +// +// Given that all keys appear in the document after their parent, it is +// guaranteed that all descendants of a node are stored after the node, this +// speeds up the search process. +// +// When encountering [[array tables]], the descendants of that node are removed +// to allow that branch of the tree to be "rediscovered". To maintain the +// invariant above, the deletion process needs to keep the order of entries. +// This results in more copies in that case. +type SeenTracker struct { + entries []entry + currentTable int32 + + // scratch buffers for clear() + removedBuf []bool + remapBuf []int32 +} + +// Reset brings the tracker to its initial state, with just a root table, so +// that it can be reused across documents. +func (s *SeenTracker) Reset() { + s.reset() +} + +// reset brings the tracker to its initial state, with just a root table. +func (s *SeenTracker) reset() { + s.entries = append(s.entries[:0], entry{ + parent: -1, + kind: tableKind, + }) + s.currentTable = 0 +} + +// find returns the id of the entry with the given parent and name, or -1. +// Anonymous entries are never returned. +func (s *SeenTracker) find(parent int32, name []byte) int32 { + // Children always appear after their parent. + for i := int(parent) + 1; i < len(s.entries); i++ { + e := &s.entries[i] + if e.parent == parent && e.kind != anonymousKind && bytes.Equal(e.name, name) { + return int32(i) //nolint:gosec // entry counts are bounded by document size + } + } + return -1 +} + +// create appends a new entry and returns its id. +func (s *SeenTracker) create(parent int32, name []byte, kind keyKind, explicit bool) int32 { + id := int32(len(s.entries)) //nolint:gosec // entry counts are bounded by document size + s.entries = append(s.entries, entry{ + parent: parent, + kind: kind, + explicit: explicit, + name: name, + }) + return id +} + +// clear removes all the descendants of the entry with the given id, keeping +// the order of the remaining entries. +func (s *SeenTracker) clear(id int32) { + // Compute which entries are removed. Given that children always appear + // after their parent, a single forward pass is enough. + if cap(s.removedBuf) < len(s.entries) { + s.removedBuf = make([]bool, len(s.entries)) + s.remapBuf = make([]int32, len(s.entries)) + } + removed := s.removedBuf[:len(s.entries)] + remap := s.remapBuf[:len(s.entries)] + for i := range removed { + removed[i] = false + } + + n := int32(0) + for i := 0; i < len(s.entries); i++ { + parent := s.entries[i].parent + if parent >= 0 && (parent == id && s.entries[i].kind != invalidKind || removed[parent]) { + removed[i] = true + continue + } + remap[i] = n + if int32(i) != n { //nolint:gosec // entry counts are bounded by document size + e := s.entries[i] + e.parent = remap[e.parent] + s.entries[n] = e + } + n++ + } + s.entries = s.entries[:n] +} + +// CheckExpression takes a top-level node and checks that it does not contain +// keys that have been seen in previous calls, and validates that types are +// consistent. It returns true if it is the first time this node's key is +// seen. Useful to clear array tables on first use. +func (s *SeenTracker) CheckExpression(node *unstable.Node) (bool, error) { + if len(s.entries) == 0 { + s.reset() + } + switch node.Kind { + case unstable.KeyValue: + return false, s.checkKeyValue(s.currentTable, node) + case unstable.Table: + return s.checkTable(node) + case unstable.ArrayTable: + return s.checkArrayTable(node) + default: + return false, fmt.Errorf("toml: unexpected expression kind %s", node.Kind) + } +} + +// CheckTable validates a [table] header given the decoded parts of its key. +// It mirrors checkTable but is driven directly from the key parts instead of +// an AST, for callers that decode without building one. It returns whether the +// table is seen for the first time. +func (s *SeenTracker) CheckTable(parts [][]byte) (bool, error) { + parent := int32(0) + for k := 0; k < len(parts); k++ { + name := parts[k] + if k == len(parts)-1 { + // Final part of the key. + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, true) + s.currentTable = i + return true, nil + } + e := &s.entries[i] + switch e.kind { + case tableKind: + if e.explicit { + return false, fmt.Errorf("toml: table %s already exists", name) + } + e.explicit = true + s.currentTable = i + return false, nil + case kvTableKind: + return false, fmt.Errorf("toml: table %s already exists as defined by a dotted key", name) + case arrayTableKind: + return false, fmt.Errorf("toml: table %s already exists as an array of tables", name) + default: + return false, fmt.Errorf("toml: key %s should be a table, not a %s", name, e.kind) + } + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, false) + } else { + switch s.entries[i].kind { + case tableKind, arrayTableKind, kvTableKind: + // Tables created by dotted keys can receive new sub-tables, + // but cannot be redefined (handled by the last-part case). + default: + return false, fmt.Errorf("toml: key %s already exists as a value", name) + } + } + parent = i + } + panic("unreachable: table expression without key") +} + +// CheckArrayTable validates a [[array table]] header given the decoded parts +// of its key. It mirrors checkArrayTable but is driven directly from the key +// parts. It returns whether the array table is seen for the first time. +func (s *SeenTracker) CheckArrayTable(parts [][]byte) (bool, error) { + parent := int32(0) + for k := 0; k < len(parts); k++ { + name := parts[k] + if k == len(parts)-1 { + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, arrayTableKind, true) + s.currentTable = i + return true, nil + } + if s.entries[i].kind != arrayTableKind { + return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", name, s.entries[i].kind) + } + // Make the descendants of this array table re-discoverable for + // the new element. + s.clear(i) + s.currentTable = i + return false, nil + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, false) + } else { + switch s.entries[i].kind { + case tableKind, arrayTableKind, kvTableKind: + // Tables created by dotted keys can receive new sub-tables, + // but cannot be redefined (handled by the last-part case). + default: + return false, fmt.Errorf("toml: key %s already exists as a value", name) + } + } + parent = i + } + panic("unreachable: array table expression without key") +} + +// CheckKeyValue validates the (possibly dotted) key of a key-value under the +// current table, WITHOUT validating its value. It returns the id of the leaf +// entry, so the caller can validate a container value with CheckValueUnder. +func (s *SeenTracker) CheckKeyValue(parts [][]byte) (int32, error) { + parent := s.currentTable + for k := 0; k < len(parts); k++ { + name := parts[k] + if k == len(parts)-1 { + if i := s.find(parent, name); i >= 0 { + return -1, fmt.Errorf("toml: key %s is already defined", name) + } + return s.create(parent, name, valueKind, false), nil + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, kvTableKind, false) + } else if s.entries[i].kind != kvTableKind { + return -1, fmt.Errorf("toml: key %s is already defined", name) + } + parent = i + } + panic("unreachable: key-value expression without key") +} + +// CheckValueUnder validates the content of a value stored under the given +// entry (typically the leaf returned by CheckKeyValue): inline tables cannot +// contain duplicate keys, including in the inline tables and arrays they +// contain. +func (s *SeenTracker) CheckValueUnder(parent int32, value *unstable.Node) error { + return s.checkValue(parent, value) +} + +func (s *SeenTracker) checkTable(node *unstable.Node) (bool, error) { + parent := int32(0) + + it := node.Key() + // Handle the intermediate parts of the key. + for it.Next() { + part := it.Node() + name := part.Data + if it.IsLast() { + // Final part of the key. + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, true) + s.currentTable = i + return true, nil + } + e := &s.entries[i] + switch e.kind { + case tableKind: + if e.explicit { + return false, fmt.Errorf("toml: table %s already exists", name) + } + e.explicit = true + s.currentTable = i + return false, nil + case kvTableKind: + return false, fmt.Errorf("toml: table %s already exists as defined by a dotted key", name) + case arrayTableKind: + return false, fmt.Errorf("toml: table %s already exists as an array of tables", name) + default: + return false, fmt.Errorf("toml: key %s should be a table, not a %s", name, e.kind) + } + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, false) + } else { + switch s.entries[i].kind { + case tableKind, arrayTableKind, kvTableKind: + // Tables created by dotted keys can receive new sub-tables, + // but cannot be redefined (handled by the last-part case). + default: + return false, fmt.Errorf("toml: key %s already exists as a value", name) + } + } + parent = i + } + panic("unreachable: table expression without key") +} + +func (s *SeenTracker) checkArrayTable(node *unstable.Node) (bool, error) { + parent := int32(0) + + it := node.Key() + for it.Next() { + part := it.Node() + name := part.Data + if it.IsLast() { + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, arrayTableKind, true) + s.currentTable = i + return true, nil + } + if s.entries[i].kind != arrayTableKind { + return false, fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", name, s.entries[i].kind) + } + // Make the descendants of this array table re-discoverable for + // the new element. + s.clear(i) + // Note: clear cannot move i because i comes before all its + // descendants. + s.currentTable = i + return false, nil + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, tableKind, false) + } else { + switch s.entries[i].kind { + case tableKind, arrayTableKind, kvTableKind: + // Tables created by dotted keys can receive new sub-tables, + // but cannot be redefined (handled by the last-part case). + default: + return false, fmt.Errorf("toml: key %s already exists as a value", name) + } + } + parent = i + } + panic("unreachable: array table expression without key") +} + +func (s *SeenTracker) checkKeyValue(parent int32, node *unstable.Node) error { + it := node.Key() + for it.Next() { + part := it.Node() + name := part.Data + if it.IsLast() { + if i := s.find(parent, name); i >= 0 { + return fmt.Errorf("toml: key %s is already defined", name) + } + id := s.create(parent, name, valueKind, false) + return s.checkValue(id, node.Value()) + } + + i := s.find(parent, name) + if i < 0 { + i = s.create(parent, name, kvTableKind, false) + } else if s.entries[i].kind != kvTableKind { + return fmt.Errorf("toml: key %s is already defined", name) + } + parent = i + } + panic("unreachable: key-value expression without key") +} + +// checkValue verifies the content of a value: inline tables cannot contain +// duplicate keys, including in the inline tables and arrays they contain. +func (s *SeenTracker) checkValue(id int32, value *unstable.Node) error { + switch value.Kind { + case unstable.InlineTable: + it := value.Children() + for it.Next() { + if err := s.checkKeyValue(id, it.Node()); err != nil { + return err + } + } + case unstable.Array: + it := value.Children() + for it.Next() { + elem := it.Node() + if elem.Kind == unstable.InlineTable || elem.Kind == unstable.Array { + elemID := s.create(id, nil, anonymousKind, false) + if err := s.checkValue(elemID, elem); err != nil { + return err + } + } + } + default: + } + return nil +} diff --git a/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go new file mode 100644 index 000000000..ed510382c --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/internal/tracker/tracker.go @@ -0,0 +1,2 @@ +// Package tracker provides functions for keeping track of AST nodes. +package tracker diff --git a/vendor/github.com/pelletier/go-toml/v2/localtime.go b/vendor/github.com/pelletier/go-toml/v2/localtime.go new file mode 100644 index 000000000..eb8c20e6d --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/localtime.go @@ -0,0 +1,121 @@ +package toml + +import ( + "fmt" + "strings" + "time" + + "github.com/pelletier/go-toml/v2/unstable" +) + +// LocalDate represents a calendar day in no specific timezone. +type LocalDate struct { + Year int + Month int + Day int +} + +// AsTime converts d into a specific time instance at midnight in zone. +func (d LocalDate) AsTime(zone *time.Location) time.Time { + return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone) +} + +// String returns RFC 3339 representation of d. +func (d LocalDate) String() string { + return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) +} + +// MarshalText returns RFC 3339 representation of d. +func (d LocalDate) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalDate) UnmarshalText(b []byte) error { + res, err := parseLocalDate(b) + if err != nil { + return err + } + *d = res + return nil +} + +// LocalTime represents a time of day of no specific day in no specific +// timezone. +type LocalTime struct { + Hour int // Hour of the day: [0; 24[ + Minute int // Minute of the hour: [0; 60[ + Second int // Second of the minute: [0; 59] + Nanosecond int // Nanoseconds within the second: [0, 1000000000[ + Precision int // Number of digits to display for Nanosecond. +} + +// String returns RFC 3339 representation of d. +// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond +// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number +// of digits for nanoseconds is provided. +func (d LocalTime) String() string { + s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second) + + if d.Precision > 0 { + s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1] + } else if d.Nanosecond > 0 { + // Nanoseconds are specified, but precision is not provided. Use the + // minimum. + s += strings.TrimRight(fmt.Sprintf(".%09d", d.Nanosecond), "0") + } + + return s +} + +// MarshalText returns RFC 3339 representation of d. +func (d LocalTime) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalTime) UnmarshalText(b []byte) error { + res, left, err := parseLocalTime(b) + if err == nil && len(left) != 0 { + err = unstable.NewParserError(left, "extra characters at the end of a local time") + } + if err != nil { + return err + } + *d = res + return nil +} + +// LocalDateTime represents a time of a specific day in no specific timezone. +type LocalDateTime struct { + LocalDate + LocalTime +} + +// AsTime converts d into a specific time instance in zone. +func (d LocalDateTime) AsTime(zone *time.Location) time.Time { + return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone) +} + +// String returns RFC 3339 representation of d. +func (d LocalDateTime) String() string { + return d.LocalDate.String() + "T" + d.LocalTime.String() +} + +// MarshalText returns RFC 3339 representation of d. +func (d LocalDateTime) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalDateTime) UnmarshalText(data []byte) error { + res, left, err := parseLocalDateTime(data) + if err == nil && len(left) != 0 { + err = unstable.NewParserError(left, "extra characters at the end of a local date time") + } + if err != nil { + return err + } + *d = res + return nil +} diff --git a/vendor/github.com/pelletier/go-toml/v2/marshaler.go b/vendor/github.com/pelletier/go-toml/v2/marshaler.go new file mode 100644 index 000000000..8b8d77238 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/marshaler.go @@ -0,0 +1,1220 @@ +package toml + +import ( + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "reflect" + "slices" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" +) + +// Marshal serializes a Go value as a TOML document. +// +// It is a shortcut for Encoder.Encode() with the default options. +func Marshal(v interface{}) ([]byte, error) { + enc := Encoder{indentSymbol: " "} + + e := encoderStatePool.Get().(*encoderState) + e.Encoder = &enc + e.buf = e.buf[:0] + e.keyStack = e.keyStack[:0] + e.lastWasHeader = false + + err := e.encodeRoot(v) + if err != nil { + encoderStatePool.Put(e) + return nil, err + } + + out := make([]byte, len(e.buf)) + copy(out, e.buf) + encoderStatePool.Put(e) + return out, nil +} + +// Encoder writes a TOML document to an output stream. +type Encoder struct { + // output + w io.Writer + + // global settings + tablesInline bool + arraysMultiline bool + indentSymbol string + indentTables bool + marshalJSONNumbers bool +} + +// NewEncoder returns a new Encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + w: w, + indentSymbol: " ", + } +} + +// SetTablesInline forces the encoder to emit all tables inline. +// +// This behavior can be controlled on an individual struct field basis with +// the inline tag: +// +// MyField `toml:",inline"` +func (enc *Encoder) SetTablesInline(inline bool) *Encoder { + enc.tablesInline = inline + return enc +} + +// SetArraysMultiline forces the encoder to emit all arrays with one element +// per line. +// +// This behavior can be controlled on an individual struct field basis with +// the multiline tag: +// +// MyField `multiline:"true"` +func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder { + enc.arraysMultiline = multiline + return enc +} + +// SetIndentSymbol defines the string that should be used for indentation. The +// provided string is repeated for each indentation level. Defaults to two +// spaces. +func (enc *Encoder) SetIndentSymbol(s string) *Encoder { + enc.indentSymbol = s + return enc +} + +// SetIndentTables forces the encoder to intent tables and array tables. +func (enc *Encoder) SetIndentTables(indent bool) *Encoder { + enc.indentTables = indent + return enc +} + +// SetMarshalJSONNumbers forces the encoder to serialize `json.Number` as a +// float or integer instead of relying on TextMarshaler to emit a string. +// +// *Unstable:* This method does not follow the compatibility guarantees of +// semver. It can be changed or removed without a new major version being +// issued. +func (enc *Encoder) SetMarshalJSONNumbers(indent bool) *Encoder { + enc.marshalJSONNumbers = indent + return enc +} + +// Encode writes a TOML representation of v to the stream. +// +// If v cannot be represented to TOML it returns an error. +// +// # Encoding rules +// +// A top level slice containing only maps or structs is encoded as [[table +// array]]. +// +// All slices not matching rule 1 are encoded as [array]. As a result, any map +// or struct they contain is encoded as an {inline table}. +// +// Nil interfaces and nil pointers are not supported. +// +// Keys in key-values always have one part. +// +// Intermediate tables are always printed. +// +// By default, strings are encoded as literal string, unless they contain +// either a newline character or a single quote. In that case they are emitted +// as quoted strings. +// +// Unsigned integers larger than math.MaxInt64 cannot be encoded. Doing so +// results in an error. This rule exists because the TOML specification only +// requires parsers to support at least the 64 bits integer range. Allowing +// larger numbers would create non-standard TOML documents, which may not be +// readable (at best) by other implementations. To encode such numbers, a +// solution is a custom type that implements encoding.TextMarshaler. +// +// When encoding structs, fields are encoded in order of definition, with +// their exact name. +// +// Tables and array tables are separated by empty lines. However, consecutive +// subtables definitions are not. For example: +// +// [top1] +// +// [top2] +// [top2.child1] +// +// [[array]] +// +// [[array]] +// [array.child2] +// +// # Struct tags +// +// The encoding of each public struct field can be customized by the format +// string in the "toml" key of the struct field's tag. This follows +// encoding/json's convention. The format string starts with the name of the +// field, optionally followed by a comma-separated list of options. The name +// may be empty in order to provide options without overriding the default +// name. +// +// The "multiline" option emits strings as quoted multi-line TOML strings. It +// has no effect on fields that would not be encoded as strings. +// +// The "inline" option turns fields that would be emitted as tables into +// inline tables instead. It has no effect on other fields. +// +// The "omitempty" option prevents empty values or groups from being emitted. +// +// The "omitzero" option prevents zero values or groups from being emitted. +// +// The "commented" option prefixes the value and all its children with a +// comment symbol. +// +// In addition to the "toml" tag struct tag, a "comment" tag can be used to +// emit a TOML comment before the value being annotated. Comments are ignored +// inside inline tables. For array tables, the comment is only present before +// the first element of the array. +func (enc *Encoder) Encode(v interface{}) error { + e := encoderStatePool.Get().(*encoderState) + e.Encoder = enc + e.buf = e.buf[:0] + e.keyStack = e.keyStack[:0] + e.lastWasHeader = false + + err := e.encodeRoot(v) + if err != nil { + encoderStatePool.Put(e) + return err + } + + _, err = enc.w.Write(e.buf) + encoderStatePool.Put(e) + if err != nil { + return fmt.Errorf("toml: cannot write: %w", err) + } + return nil +} + +var encoderStatePool = sync.Pool{ + New: func() interface{} { return &encoderState{} }, +} + +type encoderState struct { + *Encoder + + buf []byte + + // keyStack is the dotted key of the table being encoded, shared by the + // whole encode as a stack. + keyStack []string + + // entriesPool recycles entry slices across tables of the same encode. + entriesPool [][]entry + + // lastWasHeader is true when the last line written was a table header, + // used to avoid empty lines between consecutive table definitions. + lastWasHeader bool + + // stringKeyBuf is a reusable buffer to read string map keys without + // allocating one per map. + stringKeyBuf reflect.Value +} + +// valueOptions are the encoding options attached to one entry of a table. +type valueOptions struct { + multiline bool + inline bool + omitempty bool + omitzero bool + commented bool + comment string +} + +// entry is a deferred key-value of a table being encoded. +type entry struct { + key string + value reflect.Value + options valueOptions +} + +func (e *encoderState) encodeRoot(v interface{}) error { + if v == nil { + return errors.New("toml: cannot encode a nil interface") + } + + rv := reflect.ValueOf(v) + rv, ok := resolve(rv) + if !ok { + return errors.New("toml: cannot encode a nil pointer") + } + + switch rv.Kind() { + case reflect.Map, reflect.Struct: + if isValueKind(rv) { + return fmt.Errorf("toml: cannot encode a %s as a document root", rv.Type()) + } + return e.encodeTable(rv, false, 0) + default: + return fmt.Errorf("toml: cannot encode a %s as a document root", rv.Type()) + } +} + +// resolve unwraps pointers and interfaces until a concrete value is found. +// Returns false if it resolves to nil. +func resolve(v reflect.Value) (reflect.Value, bool) { + for { + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return v, false + } + v = v.Elem() + case reflect.Interface: + if v.IsNil() { + return v, false + } + v = v.Elem() + default: + return v, true + } + } +} + +// typeEncProps caches the per-type facts used on every value encode. +type typeEncProps struct { + // 0: not a TextMarshaler, 1: the type implements it, 2: its pointer does + text uint8 + // encoded as a TOML value (as opposed to a table) + isValue bool +} + +var typeEncPropsCache sync.Map // reflect.Type -> typeEncProps + +func encPropsForType(t reflect.Type) typeEncProps { + if p, ok := typeEncPropsCache.Load(t); ok { + return p.(typeEncProps) + } + var p typeEncProps + switch { + case t.Implements(textMarshalerType): + p.text = 1 + case reflect.PtrTo(t).Implements(textMarshalerType): + p.text = 2 + } + switch t { + case timeType, localDateType, localTimeType, localDateTimeType: + p.isValue = true + default: + if p.text != 0 { + p.isValue = true + } else { + switch t.Kind() { + case reflect.Map, reflect.Struct: + p.isValue = false + default: + p.isValue = true + } + } + } + typeEncPropsCache.Store(t, p) + return p +} + +// isValueKind returns true when the resolved value is encoded as a TOML +// value (as opposed to a table). +func isValueKind(v reflect.Value) bool { + return encPropsForType(v.Type()).isValue +} + +// isTableLike returns true when the value should be encoded as a table (or +// an array of tables for slices). +func (e *encoderState) isTableLike(v reflect.Value) bool { + v, ok := resolve(v) + if !ok { + // Unresolvable values (interface-held nil pointers) are encoded as + // the zero value of their element type by the value path. + return false + } + return !isValueKind(v) +} + +// isArrayOfTables returns true when the value is a non-empty slice or array +// containing only table-like values. +func (e *encoderState) isArrayOfTables(v reflect.Value) bool { + v, ok := resolve(v) + if !ok { + return false + } + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return false + } + if v.Len() == 0 { + return false + } + for i := 0; i < v.Len(); i++ { + elem, ok := resolve(v.Index(i)) + if !ok || isValueKind(elem) { + return false + } + } + return true +} + +// encodeTable writes the content of a table at the given key path. +func (e *encoderState) encodeTable(v reflect.Value, commented bool, indent int) error { + entries, err := e.collectEntries(v) + if err != nil { + return err + } + + // First pass: emit all key-values; tables are handled by the second + // pass. + for i := range entries { + ent := &entries[i] + if e.entryIsTable(ent) { + continue + } + err := e.encodeKeyValue(*ent, commented, indent) + if err != nil { + return err + } + } + + // Second pass: emit the sub-tables, extending the shared key stack. + for i := range entries { + ent := entries[i] + if !e.entryIsTable(&ent) { + continue + } + entCommented := commented || ent.options.commented + e.keyStack = append(e.keyStack, ent.key) + + if e.isArrayOfTables(ent.value) { + err := e.encodeArrayTable(ent, entCommented, indent) + if err != nil { + return err + } + e.keyStack = e.keyStack[:len(e.keyStack)-1] + continue + } + + // The value is resolvable: entryIsTable already resolved it. + tv, _ := resolve(ent.value) + + e.writeTableHeader(ent.options.comment, entCommented, false, indent) + + err := e.encodeTable(tv, entCommented, indent+1) + if err != nil { + return err + } + e.keyStack = e.keyStack[:len(e.keyStack)-1] + } + + e.putEntries(entries) + return nil +} + +// entryIsTable reports whether the entry is emitted as a (sub-)table rather +// than a key-value. +func (e *encoderState) entryIsTable(ent *entry) bool { + return !e.tablesInline && !ent.options.inline && (e.isTableLike(ent.value) || e.isArrayOfTables(ent.value)) +} + +// getEntries returns a reusable entry slice. +func (e *encoderState) getEntries() []entry { + if n := len(e.entriesPool); n > 0 { + s := e.entriesPool[n-1] + e.entriesPool = e.entriesPool[:n-1] + return s[:0] + } + return nil +} + +// putEntries returns an entry slice to the pool. +func (e *encoderState) putEntries(s []entry) { + if cap(s) > 0 { + e.entriesPool = append(e.entriesPool, s) + } +} + +// encodeArrayTable writes all the elements of an array of tables. +func (e *encoderState) encodeArrayTable(ent entry, commented bool, indent int) error { + v, _ := resolve(ent.value) + comment := ent.options.comment + for i := 0; i < v.Len(); i++ { + // Elements are resolvable: isArrayOfTables already resolved them. + elem, _ := resolve(v.Index(i)) + + e.writeTableHeader(comment, commented, true, indent) + // The comment is only present before the first element. + comment = "" + + err := e.encodeTable(elem, commented, indent+1) + if err != nil { + return err + } + } + return nil +} + +// writeTableHeader emits a [table] or [[array table]] header line, preceded +// by an empty line and comments as needed. +func (e *encoderState) writeTableHeader(comment string, commented bool, array bool, indent int) { + key := e.keyStack + if len(e.buf) > 0 && !e.lastWasHeader { + e.buf = append(e.buf, '\n') + } + + headerIndent := indent + + e.writeComment(comment, headerIndent) + + e.writeIndent(headerIndent) + if commented { + e.buf = append(e.buf, "# "...) + } + e.buf = append(e.buf, '[') + if array { + e.buf = append(e.buf, '[') + } + for i, part := range key { + if i > 0 { + e.buf = append(e.buf, '.') + } + e.buf = e.appendKey(e.buf, part) + } + e.buf = append(e.buf, ']') + if array { + e.buf = append(e.buf, ']') + } + e.buf = append(e.buf, '\n') + e.lastWasHeader = true +} + +func (e *encoderState) writeIndent(indent int) { + if !e.indentTables { + return + } + for i := 0; i < indent; i++ { + e.buf = append(e.buf, e.indentSymbol...) + } +} + +// writeComment emits the comment lines attached to an entry. +func (e *encoderState) writeComment(comment string, indent int) { + if comment == "" { + return + } + for _, line := range strings.Split(comment, "\n") { + e.writeIndent(indent) + e.buf = append(e.buf, "# "...) + e.buf = append(e.buf, line...) + e.buf = append(e.buf, '\n') + } +} + +// encodeKeyValue writes one `key = value` line of a table. +func (e *encoderState) encodeKeyValue(ent entry, commented bool, indent int) error { + commented = commented || ent.options.commented + + e.writeComment(ent.options.comment, indent) + + e.writeIndent(indent) + if commented { + e.buf = append(e.buf, "# "...) + } + e.buf = e.appendKey(e.buf, ent.key) + e.buf = append(e.buf, " = "...) + + var err error + e.buf, err = e.appendValue(e.buf, ent.value, ent.options, indent) + if err != nil { + return err + } + e.buf = append(e.buf, '\n') + e.lastWasHeader = false + return nil +} + +// collectEntries builds the ordered list of the entries of a table, +// applying tags and omission rules. +func (e *encoderState) collectEntries(v reflect.Value) ([]entry, error) { + switch v.Kind() { + case reflect.Map: + return e.collectMapEntries(v) + case reflect.Struct: + entries := e.getEntries() + e.collectStructEntries(&entries, v) + return entries, nil + default: + return nil, fmt.Errorf("toml: cannot encode a %s as a table", v.Type()) + } +} + +func (e *encoderState) collectMapEntries(v reflect.Value) ([]entry, error) { + entries := e.getEntries() + + // Keys are converted to strings right away: read them into a reusable + // buffer to avoid one allocation per key. + var kbuf reflect.Value + if v.Type().Key() == stringType { + if !e.stringKeyBuf.IsValid() { + e.stringKeyBuf = reflect.New(stringType).Elem() + } + kbuf = e.stringKeyBuf + } else { + kbuf = reflect.New(v.Type().Key()).Elem() + } + + iter := v.MapRange() + for iter.Next() { + kbuf.SetIterKey(iter) + key, err := mapKeyString(kbuf) + if err != nil { + return nil, err + } + value := iter.Value() + if value.Kind() == reflect.Interface && value.IsNil() { + // nil interface values are skipped + continue + } + if value.Kind() == reflect.Ptr && value.IsNil() { + // nil pointers in maps are encoded as their zero value + value = reflect.New(value.Type().Elem()).Elem() + } + entries = append(entries, entry{key: key, value: value}) + } + + if len(entries) > 1 { + // slices.SortFunc avoids boxing the slice into a sort.Interface (an + // allocation that sort.Sort incurs for every table). + slices.SortFunc(entries, func(a, b entry) int { + return strings.Compare(a.key, b.key) + }) + } + + return entries, nil +} + +// mapKeyString converts a map key to its string representation. +func mapKeyString(k reflect.Value) (string, error) { + kr, ok := resolve(k) + if !ok { + return "", errors.New("toml: cannot encode a nil map key") + } + if kr.Type().Implements(textMarshalerType) { + b, err := kr.Interface().(encoding.TextMarshaler).MarshalText() + if err != nil { + return "", fmt.Errorf("toml: cannot marshal map key: %w", err) + } + return string(b), nil + } + if kr.CanAddr() && reflect.PtrTo(kr.Type()).Implements(textMarshalerType) { + b, err := kr.Addr().Interface().(encoding.TextMarshaler).MarshalText() + if err != nil { + return "", fmt.Errorf("toml: cannot marshal map key: %w", err) + } + return string(b), nil + } + switch kr.Kind() { + case reflect.String: + return kr.String(), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(kr.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(kr.Uint(), 10), nil + case reflect.Float32: + return strconv.FormatFloat(kr.Float(), 'f', -1, 32), nil + case reflect.Float64: + return strconv.FormatFloat(kr.Float(), 'f', -1, 64), nil + default: + return "", fmt.Errorf("toml: cannot encode a map with key type %s", k.Type()) + } +} + +// encPlanField is the static encoding information of one field of a struct. +type encPlanField struct { + name string + index []int + depth int + options valueOptions +} + +// encPlan caches the per-type information needed to encode a struct: +// flattened fields with parsed tags, in order of definition, with shadowed +// duplicates already removed. +type encPlan struct { + fields []encPlanField +} + +var encPlans sync.Map // reflect.Type -> *encPlan + +func encPlanForType(t reflect.Type) *encPlan { + if plan, ok := encPlans.Load(t); ok { + return plan.(*encPlan) + } + plan := &encPlan{} + visited := map[reflect.Type]bool{} + buildEncPlan(plan, t, nil, 0, visited) + dedupEncPlan(plan) + encPlans.Store(t, plan) + return plan +} + +func buildEncPlan(plan *encPlan, t reflect.Type, prefix []int, depth int, visited map[reflect.Type]bool) { + if visited[t] { + return + } + visited[t] = true + defer delete(visited, t) + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + + tag, tagged := f.Tag.Lookup("toml") + if tag == "-" { + continue + } + + name := f.Name + var opts valueOptions + if tagged { + parts := strings.Split(tag, ",") + if parts[0] != "" { + name = parts[0] + } + for _, opt := range parts[1:] { + switch opt { + case "multiline": + opts.multiline = true + case "inline": + opts.inline = true + case "omitempty": + opts.omitempty = true + case "omitzero": + opts.omitzero = true + case "commented": + opts.commented = true + } + } + } + // Standalone boolean tags, e.g. multiline:"true". + const tagTrue = "true" + if f.Tag.Get("multiline") == tagTrue { + opts.multiline = true + } + if f.Tag.Get("inline") == tagTrue { + opts.inline = true + } + if f.Tag.Get("commented") == tagTrue { + opts.commented = true + } + opts.comment = f.Tag.Get("comment") + + index := make([]int, 0, len(prefix)+1) + index = append(index, prefix...) + index = append(index, i) + + if f.Anonymous { + ft := f.Type + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + if ft.Kind() == reflect.Struct && (!tagged || tagName(tag) == "") { + buildEncPlan(plan, ft, index, depth+1, visited) + continue + } + if f.PkgPath != "" && ft.Kind() != reflect.Interface { + continue + } + } else if f.PkgPath != "" { + // unexported + continue + } + + plan.fields = append(plan.fields, encPlanField{ + name: name, + index: index, + depth: depth, + options: opts, + }) + } +} + +// dedupEncPlan removes the fields shadowed by another one with the same +// name (the shallowest wins), keeping the order of first appearance. +func dedupEncPlan(plan *encPlan) { + byName := make(map[string]int, len(plan.fields)) + drop := false + for i := range plan.fields { + f := &plan.fields[i] + j, seen := byName[f.name] + if !seen { + byName[f.name] = i + continue + } + drop = true + // Shallowest wins; on equal depth, the first in order wins. + if f.depth < plan.fields[j].depth { + plan.fields[j].name = "" + byName[f.name] = i + } else { + f.name = "" + } + } + if !drop { + return + } + out := plan.fields[:0] + for _, f := range plan.fields { + if f.name != "" { + out = append(out, f) + } + } + plan.fields = out +} + +// collectStructEntries appends the entries of a struct, flattening embedded +// structs in place. +func (e *encoderState) collectStructEntries(entries *[]entry, v reflect.Value) { + plan := encPlanForType(v.Type()) + + for i := range plan.fields { + f := &plan.fields[i] + fv, ok := fieldByIndexSkipNil(v, f.index) + if !ok { + // nil embedded pointer on the way: skipped + continue + } + + // Anonymous interface fields that are nil are skipped. + if fv.Kind() == reflect.Interface && fv.IsNil() { + continue + } + // nil values in struct fields are skipped + if (fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Map) && fv.IsNil() { + continue + } + + if f.options.omitempty && isEmptyValue(fv) { + continue + } + if f.options.omitzero && isZeroValue(fv) { + continue + } + + *entries = append(*entries, entry{key: f.name, value: fv, options: f.options}) + } +} + +// fieldByIndexSkipNil returns the field at the given index path, reporting +// false if a nil embedded pointer is found on the way. +func fieldByIndexSkipNil(v reflect.Value, index []int) (reflect.Value, bool) { + for i, x := range index { + if i > 0 { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return v, false + } + v = v.Elem() + } + } + v = v.Field(x) + } + return v, true +} + +func tagName(tag string) string { + if idx := strings.IndexByte(tag, ','); idx >= 0 { + return tag[:idx] + } + return tag +} + +// isEmptyValue implements the omitempty rules. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Map, reflect.Slice, reflect.Array: + return v.Len() == 0 + case reflect.Ptr, reflect.Interface: + return v.IsNil() + case reflect.Struct: + return v.IsZero() + default: + return false + } +} + +// isZeroValue implements the omitzero rules: the type's own IsZero() when +// implemented, the reflect zero value otherwise. +func isZeroValue(v reflect.Value) bool { + if v.Type().Implements(isZeroerType) { + return v.Interface().(isZeroer).IsZero() + } + if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(isZeroerType) { + return v.Addr().Interface().(isZeroer).IsZero() + } + if !v.CanAddr() && reflect.PtrTo(v.Type()).Implements(isZeroerType) { + tmp := reflect.New(v.Type()) + tmp.Elem().Set(v) + return tmp.Interface().(isZeroer).IsZero() + } + return v.IsZero() +} + +// appendKey emits a key, quoted only if necessary. +func (e *encoderState) appendKey(b []byte, key string) []byte { + if isBareKey(key) { + return append(b, key...) + } + return e.appendString(b, key) +} + +func isBareKey(key string) bool { + if len(key) == 0 { + return false + } + for _, c := range []byte(key) { + if !isUnquotedKeyByte(c) { + return false + } + } + return true +} + +func isUnquotedKeyByte(c byte) bool { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' +} + +// appendValue emits a TOML value. +func (e *encoderState) appendValue(b []byte, v reflect.Value, opts valueOptions, indent int) ([]byte, error) { + t := v.Type() + + // Special types take precedence over their kind. + switch t { + case timeType: + return v.Interface().(time.Time).AppendFormat(b, "2006-01-02T15:04:05.999999999Z07:00"), nil + case localDateType: + return append(b, v.Interface().(LocalDate).String()...), nil + case localTimeType: + return append(b, v.Interface().(LocalTime).String()...), nil + case localDateTimeType: + return append(b, v.Interface().(LocalDateTime).String()...), nil + case jsonNumberType: + if e.marshalJSONNumbers { + return appendJSONNumber(b, v.Interface().(json.Number)) + } + } + + switch encPropsForType(t).text { + case 1: + if t.Kind() != reflect.String { + return e.appendTextMarshaler(b, v.Interface().(encoding.TextMarshaler)) + } + case 2: + if v.CanAddr() { + return e.appendTextMarshaler(b, v.Addr().Interface().(encoding.TextMarshaler)) + } + tmp := reflect.New(t) + tmp.Elem().Set(v) + return e.appendTextMarshaler(b, tmp.Interface().(encoding.TextMarshaler)) + } + + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + // nil pointers are encoded as the zero value of their element + // type. + return e.appendValue(b, reflect.Zero(t.Elem()), opts, indent) + } + return e.appendValue(b, v.Elem(), opts, indent) + case reflect.Interface: + if v.IsNil() { + return nil, errors.New("toml: cannot encode a nil interface") + } + return e.appendValue(b, v.Elem(), opts, indent) + case reflect.String: + if opts.multiline { + return e.appendMultilineString(b, v.String()), nil + } + return e.appendString(b, v.String()), nil + case reflect.Bool: + if v.Bool() { + return append(b, "true"...), nil + } + return append(b, "false"...), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.AppendInt(b, v.Int(), 10), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + u := v.Uint() + if u > math.MaxInt64 { + return nil, fmt.Errorf("toml: cannot encode an unsigned integer above math.MaxInt64: %d", u) + } + return strconv.AppendUint(b, u, 10), nil + case reflect.Float32: + return appendFloat(b, v.Float(), 32), nil + case reflect.Float64: + return appendFloat(b, v.Float(), 64), nil + case reflect.Slice, reflect.Array: + return e.appendArray(b, v, opts, indent) + case reflect.Map: + return e.appendInlineTable(b, v, indent) + case reflect.Struct: + return e.appendInlineTable(b, v, indent) + default: + return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Type()) + } +} + +var jsonNumberType = reflect.TypeOf(json.Number("")) + +func appendJSONNumber(b []byte, n json.Number) ([]byte, error) { + if n == "" { + return append(b, '0'), nil + } + if i, err := n.Int64(); err == nil { + return strconv.AppendInt(b, i, 10), nil + } + f, err := n.Float64() + if err != nil { + return nil, fmt.Errorf("toml: cannot encode json.Number %q: %w", string(n), err) + } + return appendFloat(b, f, 64), nil +} + +func appendFloat(b []byte, f float64, bitSize int) []byte { + switch { + case math.IsNaN(f): + return append(b, "nan"...) + case math.IsInf(f, 1): + return append(b, "inf"...) + case math.IsInf(f, -1): + return append(b, "-inf"...) + } + start := len(b) + b = strconv.AppendFloat(b, f, 'f', -1, bitSize) + // TOML floats must have a fractional part or an exponent. + if !bytes.ContainsAny(b[start:], ".eE") { + b = append(b, ".0"...) + } + return b +} + +func (e *encoderState) appendTextMarshaler(b []byte, m encoding.TextMarshaler) ([]byte, error) { + text, err := m.MarshalText() + if err != nil { + return nil, fmt.Errorf("toml: error calling MarshalText: %w", err) + } + return e.appendString(b, string(text)), nil +} + +// appendArray encodes a slice or array value. +func (e *encoderState) appendArray(b []byte, v reflect.Value, opts valueOptions, indent int) ([]byte, error) { + multiline := opts.multiline || e.arraysMultiline + + b = append(b, '[') + if multiline && v.Len() > 0 { + for i := 0; i < v.Len(); i++ { + if i > 0 { + b = append(b, ',') + } + b = append(b, '\n') + for j := 0; j <= indent; j++ { + b = append(b, e.indentSymbol...) + } + var err error + b, err = e.appendValue(b, v.Index(i), valueOptions{}, indent+1) + if err != nil { + return nil, err + } + } + b = append(b, '\n') + for j := 0; j < indent; j++ { + b = append(b, e.indentSymbol...) + } + } else { + for i := 0; i < v.Len(); i++ { + if i > 0 { + b = append(b, ", "...) + } + var err error + b, err = e.appendValue(b, v.Index(i), valueOptions{}, indent) + if err != nil { + return nil, err + } + } + } + return append(b, ']'), nil +} + +// appendInlineTable encodes a map or a struct as an inline table. +func (e *encoderState) appendInlineTable(b []byte, v reflect.Value, indent int) ([]byte, error) { + entries, err := e.collectEntries(v) + if err != nil { + return nil, err + } + + b = append(b, '{') + for i, ent := range entries { + if i > 0 { + b = append(b, ", "...) + } + b = e.appendKey(b, ent.key) + b = append(b, " = "...) + // multiline strings are not allowed inside inline tables: they + // would break the single-line requirement. + opts := ent.options + opts.multiline = false + b, err = e.appendValue(b, ent.value, opts, indent) + if err != nil { + return nil, err + } + } + e.putEntries(entries) + return append(b, '}'), nil +} + +// appendString encodes a string, using a literal string when possible and a +// basic string otherwise. +func (e *encoderState) appendString(b []byte, s string) []byte { + if canBeLiteral(s) { + b = append(b, '\'') + b = append(b, s...) + return append(b, '\'') + } + return appendBasicString(b, s) +} + +// canBeLiteral returns true when the string can be represented as a TOML +// literal string: no control characters, no single quote, no newline. +func canBeLiteral(s string) bool { + for i := 0; i < len(s); i++ { + c := s[i] + if c == '\'' || c == 0x7f || c < 0x20 { + return false + } + } + return utf8.ValidString(s) +} + +// appendBasicString encodes a string as a TOML basic (double-quoted) string. +func appendBasicString(b []byte, s string) []byte { + b = append(b, '"') + for i := 0; i < len(s); { + c := s[i] + switch { + case c == '"': + b = append(b, '\\', '"') + i++ + case c == '\\': + b = append(b, '\\', '\\') + i++ + case c == '\b': + b = append(b, '\\', 'b') + i++ + case c == '\f': + b = append(b, '\\', 'f') + i++ + case c == '\n': + b = append(b, '\\', 'n') + i++ + case c == '\r': + b = append(b, '\\', 'r') + i++ + case c == '\t': + b = append(b, '\\', 't') + i++ + case c < 0x20 || c == 0x7f: + b = append(b, fmt.Sprintf("\\u%04X", c)...) + i++ + default: + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + // Replace invalid bytes by the replacement character. + b = append(b, fmt.Sprintf("\\u%04X", c)...) + i++ + continue + } + b = append(b, s[i:i+size]...) + i += size + } + } + return append(b, '"') +} + +// appendMultilineString encodes a string as a TOML multi-line basic string. +func appendMultilineString(b []byte, s string) []byte { + b = append(b, `"""`...) + b = append(b, '\n') + for i := 0; i < len(s); { + c := s[i] + switch { + case c == '"': + // Runs of three or more quotes must be escaped. + j := i + for j < len(s) && s[j] == '"' { + j++ + } + if j-i >= 3 { + for ; i < j; i++ { + b = append(b, '\\', '"') + } + } else { + b = append(b, s[i:j]...) + i = j + } + case c == '\\': + b = append(b, '\\', '\\') + i++ + case c == '\n': + b = append(b, '\n') + i++ + case c == '\b': + b = append(b, '\\', 'b') + i++ + case c == '\f': + b = append(b, '\\', 'f') + i++ + case c == '\r': + b = append(b, '\\', 'r') + i++ + case c == '\t': + b = append(b, '\t') + i++ + case c < 0x20 || c == 0x7f: + b = append(b, fmt.Sprintf("\\u%04X", c)...) + i++ + default: + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + b = append(b, fmt.Sprintf("\\u%04X", c)...) + i++ + continue + } + b = append(b, s[i:i+size]...) + i += size + } + } + return append(b, `"""`...) +} + +func (e *encoderState) appendMultilineString(b []byte, s string) []byte { + return appendMultilineString(b, s) +} diff --git a/vendor/github.com/pelletier/go-toml/v2/strict.go b/vendor/github.com/pelletier/go-toml/v2/strict.go new file mode 100644 index 000000000..b58571df1 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/strict.go @@ -0,0 +1,108 @@ +package toml + +import ( + "github.com/pelletier/go-toml/v2/internal/tracker" + "github.com/pelletier/go-toml/v2/unstable" +) + +type strict struct { + Enabled bool + + // Tracks the current key being processed. + key tracker.KeyTracker + + missing []decodeError +} + +// decodeError is the information needed to materialize a DecodeError once the +// whole document is available. +type decodeError struct { + highlight unstable.Range + key Key + message string +} + +// Reset clears the state of the tracker so it can be reused for another +// document. +func (s *strict) Reset() { + s.key = tracker.KeyTracker{} + s.missing = s.missing[:0] +} + +// EnterTable is called when a new table or array table expression starts +// being processed. +func (s *strict) EnterTable(node *unstable.Node) { + if !s.Enabled { + return + } + s.key.UpdateTable(node) +} + +// MissingTable is called when a table is present in the document but has no +// corresponding field in the target. +func (s *strict) MissingTable(node *unstable.Node) { + if !s.Enabled { + return + } + s.missing = append(s.missing, decodeError{ + highlight: keyLocation(node), + key: s.key.Key(), + message: "missing table", + }) +} + +// MissingField is called when a key-value is present in the document but has +// no corresponding field in the target. +func (s *strict) MissingField(node *unstable.Node) { + if !s.Enabled { + return + } + s.key.Push(node) + s.missing = append(s.missing, decodeError{ + highlight: keyLocation(node), + key: s.key.Key(), + message: "unknown field", + }) + s.key.Pop(node) +} + +// Error returns the cumulated StrictMissingError for the document, or nil. +func (s *strict) Error(document []byte) error { + if !s.Enabled || len(s.missing) == 0 { + return nil + } + + err := &StrictMissingError{ + Errors: make([]DecodeError, 0, len(s.missing)), + } + + for _, derr := range s.missing { + highlight := document[derr.highlight.Offset : derr.highlight.Offset+derr.highlight.Length] + err.Errors = append(err.Errors, *newDecodeError(document, highlight, derr.key, derr.message)) + } + + return err +} + +// keyLocation returns the range of the document covering all the parts of +// the key of the given node. +func keyLocation(node *unstable.Node) unstable.Range { + k := node.Key() + + hasOne := k.Next() + if !hasOne { + panic("should not be called with empty key") + } + + start := k.Node().Raw + end := start + + for k.Next() { + end = k.Node().Raw + } + + return unstable.Range{ + Offset: start.Offset, + Length: end.Offset + end.Length - start.Offset, + } +} diff --git a/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh b/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh new file mode 100644 index 000000000..5fe5c7772 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/test-go-versions.sh @@ -0,0 +1,597 @@ +#!/usr/bin/env bash + +set -uo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Go versions to test (1.11 through 1.26) +GO_VERSIONS=( + "1.11" + "1.12" + "1.13" + "1.14" + "1.15" + "1.16" + "1.17" + "1.18" + "1.19" + "1.20" + "1.21" + "1.22" + "1.23" + "1.24" + "1.25" + "1.26" +) + +# Default values +PARALLEL=true +VERBOSE=false +OUTPUT_DIR="test-results" +DOCKER_TIMEOUT="10m" + +usage() { + cat << EOF +Usage: $0 [OPTIONS] [GO_VERSIONS...] + +Test go-toml across multiple Go versions using Docker containers. + +The script reports the lowest continuous supported Go version (where all subsequent +versions pass) and only exits with non-zero status if either of the two most recent +Go versions fail, indicating immediate attention is needed. + +Note: For Go versions < 1.21, the script automatically updates go.mod to match the +target version, but older versions may still fail due to missing standard library +features (e.g., the 'slices' package introduced in Go 1.21). + +OPTIONS: + -h, --help Show this help message + -s, --sequential Run tests sequentially instead of in parallel + -v, --verbose Enable verbose output + -o, --output DIR Output directory for test results (default: test-results) + -t, --timeout TIME Docker timeout for each test (default: 10m) + --list List available Go versions and exit + +ARGUMENTS: + GO_VERSIONS Specific Go versions to test (default: all supported versions) + Examples: 1.21 1.22 1.23 + +EXAMPLES: + $0 # Test all Go versions in parallel + $0 --sequential # Test all Go versions sequentially + $0 1.21 1.22 1.23 # Test specific versions + $0 --verbose --output ./results 1.25 1.26 # Verbose output to custom directory + +EXIT CODES: + 0 Recent Go versions pass (good compatibility) + 1 Recent Go versions fail (needs attention) or script error + +EOF +} + +log() { + echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" >&2 +} + +log_success() { + echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" >&2 +} + +log_error() { + echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" >&2 +} + +log_warning() { + echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" >&2 +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -s|--sequential) + PARALLEL=false + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -o|--output) + OUTPUT_DIR="$2" + shift 2 + ;; + -t|--timeout) + DOCKER_TIMEOUT="$2" + shift 2 + ;; + --list) + echo "Available Go versions:" + printf '%s\n' "${GO_VERSIONS[@]}" + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + # Remaining arguments are Go versions + break + ;; + esac +done + +# If specific versions provided, use those instead of defaults +if [[ $# -gt 0 ]]; then + GO_VERSIONS=("$@") +fi + +# Validate Go versions +for version in "${GO_VERSIONS[@]}"; do + if ! [[ "$version" =~ ^1\.(1[1-9]|2[0-6])$ ]]; then + log_error "Invalid Go version: $version. Supported versions: 1.11-1.26" + exit 1 + fi +done + +# Check if Docker is available +if ! command -v docker &> /dev/null; then + log_error "Docker is required but not installed or not in PATH" + exit 1 +fi + +# Check if Docker daemon is running +if ! docker info &> /dev/null; then + log_error "Docker daemon is not running" + exit 1 +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Function to test a single Go version +test_go_version() { + local go_version="$1" + local container_name="go-toml-test-${go_version}" + local result_file="${OUTPUT_DIR}/go-${go_version}.txt" + local dockerfile_content + + log "Testing Go $go_version..." + + # Create a temporary Dockerfile for this version + # For Go versions < 1.21, we need to update go.mod to match the Go version + local needs_go_mod_update=false + if [[ $(echo "$go_version 1.21" | tr ' ' '\n' | sort -V | head -n1) == "$go_version" && "$go_version" != "1.21" ]]; then + needs_go_mod_update=true + fi + + dockerfile_content="FROM golang:${go_version}-alpine + +# Install git (required for go mod) +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Copy source code +COPY . ." + + # Add go.mod update step for older Go versions + if [[ "$needs_go_mod_update" == true ]]; then + dockerfile_content="$dockerfile_content + +# Update go.mod to match Go version (required for Go < 1.21) +RUN if [ -f go.mod ]; then sed -i 's/^go [0-9]\\+\\.[0-9]\\+\\(\\.[0-9]\\+\\)\\?/go $go_version/' go.mod; fi + +# Note: Go versions < 1.21 may fail due to missing standard library packages (e.g., slices) +# This is expected for projects that use Go 1.21+ features" + fi + + dockerfile_content="$dockerfile_content + +# Run tests +CMD [\"sh\", \"-c\", \"go version && echo '--- Running go test ./... ---' && go test ./...\"]" + + # Create temporary directory for this test + local temp_dir + temp_dir=$(mktemp -d) + + # Copy source to temp directory (excluding test results and git) + rsync -a --exclude="$OUTPUT_DIR" --exclude=".git" --exclude="*.test" . "$temp_dir/" + + # Create Dockerfile in temp directory + echo "$dockerfile_content" > "$temp_dir/Dockerfile" + + # Build and run container + local exit_code=0 + local output + + if $VERBOSE; then + log "Building Docker image for Go $go_version..." + fi + + # Capture both stdout and stderr, and the exit code + if output=$(cd "$temp_dir" && timeout "$DOCKER_TIMEOUT" docker build -t "$container_name" . 2>&1 && \ + timeout "$DOCKER_TIMEOUT" docker run --rm "$container_name" 2>&1); then + log_success "Go $go_version: PASSED" + echo "PASSED" > "${result_file}.status" + else + exit_code=$? + log_error "Go $go_version: FAILED (exit code: $exit_code)" + echo "FAILED" > "${result_file}.status" + fi + + # Save full output + echo "$output" > "$result_file" + + # Clean up + docker rmi "$container_name" &> /dev/null || true + rm -rf "$temp_dir" + + if $VERBOSE; then + echo "--- Go $go_version output ---" + echo "$output" + echo "--- End Go $go_version output ---" + fi + + return $exit_code +} + +# Function to run tests in parallel +run_parallel() { + local pids=() + local failed_versions=() + + log "Starting parallel tests for ${#GO_VERSIONS[@]} Go versions..." + + # Start all tests in background + for version in "${GO_VERSIONS[@]}"; do + test_go_version "$version" & + pids+=($!) + done + + # Wait for all tests to complete + for i in "${!pids[@]}"; do + local pid=${pids[$i]} + local version=${GO_VERSIONS[$i]} + + if ! wait $pid; then + failed_versions+=("$version") + fi + done + + return ${#failed_versions[@]} +} + +# Function to run tests sequentially +run_sequential() { + local failed_versions=() + + log "Starting sequential tests for ${#GO_VERSIONS[@]} Go versions..." + + for version in "${GO_VERSIONS[@]}"; do + if ! test_go_version "$version"; then + failed_versions+=("$version") + fi + done + + return ${#failed_versions[@]} +} + +# Main execution +main() { + local start_time + start_time=$(date +%s) + + log "Starting Go version compatibility tests..." + log "Testing versions: ${GO_VERSIONS[*]}" + log "Output directory: $OUTPUT_DIR" + log "Parallel execution: $PARALLEL" + + local failed_count + if $PARALLEL; then + run_parallel + failed_count=$? + else + run_sequential + failed_count=$? + fi + + local end_time + end_time=$(date +%s) + local duration=$((end_time - start_time)) + + # Collect results for display + local passed_versions=() + local failed_versions=() + local unknown_versions=() + local passed_count=0 + + for version in "${GO_VERSIONS[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + if [[ -f "$status_file" ]]; then + local status + status=$(cat "$status_file") + if [[ "$status" == "PASSED" ]]; then + passed_versions+=("$version") + ((passed_count++)) + else + failed_versions+=("$version") + fi + else + unknown_versions+=("$version") + fi + done + + # Generate summary report + local summary_file="${OUTPUT_DIR}/summary.txt" + { + echo "Go Version Compatibility Test Summary" + echo "=====================================" + echo "Date: $(date)" + echo "Duration: ${duration}s" + echo "Parallel: $PARALLEL" + echo "" + echo "Results:" + + for version in "${GO_VERSIONS[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + if [[ -f "$status_file" ]]; then + local status + status=$(cat "$status_file") + if [[ "$status" == "PASSED" ]]; then + echo " Go $version: ✓ PASSED" + else + echo " Go $version: ✗ FAILED" + fi + else + echo " Go $version: ? UNKNOWN (no status file)" + fi + done + + echo "" + echo "Summary: $passed_count/${#GO_VERSIONS[@]} versions passed" + + if [[ $failed_count -gt 0 ]]; then + echo "" + echo "Failed versions details:" + for version in "${failed_versions[@]}"; do + echo "" + echo "--- Go $version (FAILED) ---" + local result_file="${OUTPUT_DIR}/go-${version}.txt" + if [[ -f "$result_file" ]]; then + tail -n 30 "$result_file" + fi + done + fi + } > "$summary_file" + + # Find lowest continuous supported version and check recent versions + local lowest_continuous_version="" + local recent_versions_failed=false + + # Sort versions to ensure proper order + local sorted_versions=() + for version in "${GO_VERSIONS[@]}"; do + sorted_versions+=("$version") + done + # Sort versions numerically (1.11, 1.12, ..., 1.25) + IFS=$'\n' sorted_versions=($(sort -V <<< "${sorted_versions[*]}")) + + # Find lowest continuous supported version (all versions from this point onwards pass) + for version in "${sorted_versions[@]}"; do + local status_file="${OUTPUT_DIR}/go-${version}.txt.status" + local all_subsequent_pass=true + + # Check if this version and all subsequent versions pass + local found_current=false + for check_version in "${sorted_versions[@]}"; do + if [[ "$check_version" == "$version" ]]; then + found_current=true + fi + + if [[ "$found_current" == true ]]; then + local check_status_file="${OUTPUT_DIR}/go-${check_version}.txt.status" + if [[ -f "$check_status_file" ]]; then + local status + status=$(cat "$check_status_file") + if [[ "$status" != "PASSED" ]]; then + all_subsequent_pass=false + break + fi + else + all_subsequent_pass=false + break + fi + fi + done + + if [[ "$all_subsequent_pass" == true ]]; then + lowest_continuous_version="$version" + break + fi + done + + # Check if the two most recent versions failed + local num_versions=${#sorted_versions[@]} + if [[ $num_versions -ge 2 ]]; then + local second_recent="${sorted_versions[$((num_versions-2))]}" + local most_recent="${sorted_versions[$((num_versions-1))]}" + + local second_recent_status_file="${OUTPUT_DIR}/go-${second_recent}.txt.status" + local most_recent_status_file="${OUTPUT_DIR}/go-${most_recent}.txt.status" + + local second_recent_failed=false + local most_recent_failed=false + + if [[ -f "$second_recent_status_file" ]]; then + local status + status=$(cat "$second_recent_status_file") + if [[ "$status" != "PASSED" ]]; then + second_recent_failed=true + fi + else + second_recent_failed=true + fi + + if [[ -f "$most_recent_status_file" ]]; then + local status + status=$(cat "$most_recent_status_file") + if [[ "$status" != "PASSED" ]]; then + most_recent_failed=true + fi + else + most_recent_failed=true + fi + + if [[ "$second_recent_failed" == true || "$most_recent_failed" == true ]]; then + recent_versions_failed=true + fi + elif [[ $num_versions -eq 1 ]]; then + # Only one version tested, check if it's the most recent and failed + local only_version="${sorted_versions[0]}" + local only_status_file="${OUTPUT_DIR}/go-${only_version}.txt.status" + + if [[ -f "$only_status_file" ]]; then + local status + status=$(cat "$only_status_file") + if [[ "$status" != "PASSED" ]]; then + recent_versions_failed=true + fi + else + recent_versions_failed=true + fi + fi + + # Display summary + echo "" + log "Test completed in ${duration}s" + log "Summary report: $summary_file" + + echo "" + echo "========================================" + echo " FINAL RESULTS" + echo "========================================" + echo "" + + # Display passed versions + if [[ ${#passed_versions[@]} -gt 0 ]]; then + log_success "PASSED (${#passed_versions[@]}/${#GO_VERSIONS[@]}):" + # Sort passed versions for display + local sorted_passed=() + for version in "${sorted_versions[@]}"; do + for passed_version in "${passed_versions[@]}"; do + if [[ "$version" == "$passed_version" ]]; then + sorted_passed+=("$version") + break + fi + done + done + for version in "${sorted_passed[@]}"; do + echo -e " ${GREEN}✓${NC} Go $version" + done + echo "" + fi + + # Display failed versions + if [[ ${#failed_versions[@]} -gt 0 ]]; then + log_error "FAILED (${#failed_versions[@]}/${#GO_VERSIONS[@]}):" + # Sort failed versions for display + local sorted_failed=() + for version in "${sorted_versions[@]}"; do + for failed_version in "${failed_versions[@]}"; do + if [[ "$version" == "$failed_version" ]]; then + sorted_failed+=("$version") + break + fi + done + done + for version in "${sorted_failed[@]}"; do + echo -e " ${RED}✗${NC} Go $version" + done + echo "" + + # Show failure details + echo "========================================" + echo " FAILURE DETAILS" + echo "========================================" + echo "" + + for version in "${sorted_failed[@]}"; do + echo -e "${RED}--- Go $version FAILURE LOGS (last 30 lines) ---${NC}" + local result_file="${OUTPUT_DIR}/go-${version}.txt" + if [[ -f "$result_file" ]]; then + tail -n 30 "$result_file" | sed 's/^/ /' + else + echo " No log file found: $result_file" + fi + echo "" + done + fi + + # Display unknown versions + if [[ ${#unknown_versions[@]} -gt 0 ]]; then + log_warning "UNKNOWN (${#unknown_versions[@]}/${#GO_VERSIONS[@]}):" + for version in "${unknown_versions[@]}"; do + echo -e " ${YELLOW}?${NC} Go $version (no status file)" + done + echo "" + fi + + echo "========================================" + echo " COMPATIBILITY SUMMARY" + echo "========================================" + echo "" + + if [[ -n "$lowest_continuous_version" ]]; then + log_success "Lowest continuous supported version: Go $lowest_continuous_version" + echo " (All versions from Go $lowest_continuous_version onwards pass)" + else + log_error "No continuous version support found" + echo " (No version has all subsequent versions passing)" + fi + + echo "" + echo "========================================" + echo "Full detailed logs available in: $OUTPUT_DIR" + echo "========================================" + + # Determine exit code based on recent versions + if [[ "$recent_versions_failed" == true ]]; then + log_error "OVERALL RESULT: Recent Go versions failed - this needs attention!" + if [[ -n "$lowest_continuous_version" ]]; then + echo "Note: Continuous support starts from Go $lowest_continuous_version" + fi + exit 1 + else + log_success "OVERALL RESULT: Recent Go versions pass - compatibility looks good!" + if [[ -n "$lowest_continuous_version" ]]; then + echo "Continuous support starts from Go $lowest_continuous_version" + fi + exit 0 + fi +} + +# Trap to clean up on exit +cleanup() { + # Kill any remaining background processes + jobs -p | xargs -r kill 2>/dev/null || true + + # Clean up any remaining Docker containers + docker ps -q --filter "name=go-toml-test-" | xargs -r docker stop 2>/dev/null || true + docker images -q --filter "reference=go-toml-test-*" | xargs -r docker rmi 2>/dev/null || true +} + +trap cleanup EXIT + +# Run main function +main diff --git a/vendor/github.com/pelletier/go-toml/v2/toml.abnf b/vendor/github.com/pelletier/go-toml/v2/toml.abnf new file mode 100644 index 000000000..473f3749e --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/toml.abnf @@ -0,0 +1,243 @@ +;; This document describes TOML's syntax, using the ABNF format (defined in +;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt). +;; +;; All valid TOML documents will match this description, however certain +;; invalid documents would need to be rejected as per the semantics described +;; in the supporting text description. + +;; It is possible to try this grammar interactively, using instaparse. +;; http://instaparse.mojombo.com/ +;; +;; To do so, in the lower right, click on Options and change `:input-format` to +;; ':abnf'. Then paste this entire ABNF document into the grammar entry box +;; (above the options). Then you can type or paste a sample TOML document into +;; the beige box on the left. Tada! + +;; Overall Structure + +toml = expression *( newline expression ) + +expression = ws [ comment ] +expression =/ ws keyval ws [ comment ] +expression =/ ws table ws [ comment ] + +;; Whitespace + +ws = *wschar +wschar = %x20 ; Space +wschar =/ %x09 ; Horizontal tab + +;; Newline + +newline = %x0A ; LF +newline =/ %x0D.0A ; CRLF + +;; Comment + +comment-start-symbol = %x23 ; # +non-ascii = %x80-D7FF / %xE000-10FFFF +non-eol = %x09 / %x20-7F / non-ascii + +comment = comment-start-symbol *non-eol + +;; Key-Value pairs + +keyval = key keyval-sep val + +key = simple-key / dotted-key +simple-key = quoted-key / unquoted-key + +unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ +quoted-key = basic-string / literal-string +dotted-key = simple-key 1*( dot-sep simple-key ) + +dot-sep = ws %x2E ws ; . Period +keyval-sep = ws %x3D ws ; = + +val = string / boolean / array / inline-table / date-time / float / integer + +;; String + +string = ml-basic-string / basic-string / ml-literal-string / literal-string + +;; Basic String + +basic-string = quotation-mark *basic-char quotation-mark + +quotation-mark = %x22 ; " + +basic-char = basic-unescaped / escaped +basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii +escaped = escape escape-seq-char + +escape = %x5C ; \ +escape-seq-char = %x22 ; " quotation mark U+0022 +escape-seq-char =/ %x5C ; \ reverse solidus U+005C +escape-seq-char =/ %x62 ; b backspace U+0008 +escape-seq-char =/ %x66 ; f form feed U+000C +escape-seq-char =/ %x6E ; n line feed U+000A +escape-seq-char =/ %x72 ; r carriage return U+000D +escape-seq-char =/ %x74 ; t tab U+0009 +escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX +escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX + +;; Multiline Basic String + +ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body + ml-basic-string-delim +ml-basic-string-delim = 3quotation-mark +ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ] + +mlb-content = mlb-char / newline / mlb-escaped-nl +mlb-char = mlb-unescaped / escaped +mlb-quotes = 1*2quotation-mark +mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii +mlb-escaped-nl = escape ws newline *( wschar / newline ) + +;; Literal String + +literal-string = apostrophe *literal-char apostrophe + +apostrophe = %x27 ; ' apostrophe + +literal-char = %x09 / %x20-26 / %x28-7E / non-ascii + +;; Multiline Literal String + +ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body + ml-literal-string-delim +ml-literal-string-delim = 3apostrophe +ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ] + +mll-content = mll-char / newline +mll-char = %x09 / %x20-26 / %x28-7E / non-ascii +mll-quotes = 1*2apostrophe + +;; Integer + +integer = dec-int / hex-int / oct-int / bin-int + +minus = %x2D ; - +plus = %x2B ; + +underscore = %x5F ; _ +digit1-9 = %x31-39 ; 1-9 +digit0-7 = %x30-37 ; 0-7 +digit0-1 = %x30-31 ; 0-1 + +hex-prefix = %x30.78 ; 0x +oct-prefix = %x30.6F ; 0o +bin-prefix = %x30.62 ; 0b + +dec-int = [ minus / plus ] unsigned-dec-int +unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT ) + +hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG ) +oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 ) +bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 ) + +;; Float + +float = float-int-part ( exp / frac [ exp ] ) +float =/ special-float + +float-int-part = dec-int +frac = decimal-point zero-prefixable-int +decimal-point = %x2E ; . +zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT ) + +exp = "e" float-exp-part +float-exp-part = [ minus / plus ] zero-prefixable-int + +special-float = [ minus / plus ] ( inf / nan ) +inf = %x69.6e.66 ; inf +nan = %x6e.61.6e ; nan + +;; Boolean + +boolean = true / false + +true = %x74.72.75.65 ; true +false = %x66.61.6C.73.65 ; false + +;; Date and Time (as defined in RFC 3339) + +date-time = offset-date-time / local-date-time / local-date / local-time + +date-fullyear = 4DIGIT +date-month = 2DIGIT ; 01-12 +date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year +time-delim = "T" / %x20 ; T, t, or space +time-hour = 2DIGIT ; 00-23 +time-minute = 2DIGIT ; 00-59 +time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules +time-secfrac = "." 1*DIGIT +time-numoffset = ( "+" / "-" ) time-hour ":" time-minute +time-offset = "Z" / time-numoffset + +partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ] +full-date = date-fullyear "-" date-month "-" date-mday +full-time = partial-time time-offset + +;; Offset Date-Time + +offset-date-time = full-date time-delim full-time + +;; Local Date-Time + +local-date-time = full-date time-delim partial-time + +;; Local Date + +local-date = full-date + +;; Local Time + +local-time = partial-time + +;; Array + +array = array-open [ array-values ] ws-comment-newline array-close + +array-open = %x5B ; [ +array-close = %x5D ; ] + +array-values = ws-comment-newline val ws-comment-newline array-sep array-values +array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ] + +array-sep = %x2C ; , Comma + +ws-comment-newline = *( wschar / [ comment ] newline ) + +;; Table + +table = std-table / array-table + +;; Standard Table + +std-table = std-table-open key std-table-close + +std-table-open = %x5B ws ; [ Left square bracket +std-table-close = ws %x5D ; ] Right square bracket + +;; Inline Table + +inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close + +inline-table-open = %x7B ws ; { +inline-table-close = ws %x7D ; } +inline-table-sep = ws %x2C ws ; , Comma + +inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ] + +;; Array Table + +array-table = array-table-open key array-table-close + +array-table-open = %x5B.5B ws ; [[ Double left square bracket +array-table-close = ws %x5D.5D ; ]] Double right square bracket + +;; Built-in ABNF terms, reproduced here for clarity + +ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +DIGIT = %x30-39 ; 0-9 +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" diff --git a/vendor/github.com/pelletier/go-toml/v2/types.go b/vendor/github.com/pelletier/go-toml/v2/types.go new file mode 100644 index 000000000..420f1982f --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/types.go @@ -0,0 +1,24 @@ +package toml + +import ( + "encoding" + "reflect" + "time" +) + +// isZeroer is used to check whether a value is the zero value for its type, +// as defined by the type itself. +type isZeroer interface { + IsZero() bool +} + +var isZeroerType = reflect.TypeOf(new(isZeroer)).Elem() + +var ( + timeType = reflect.TypeOf(time.Time{}) + textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() + textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() + mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil)) + sliceInterfaceType = reflect.TypeOf([]interface{}(nil)) + stringType = reflect.TypeOf("") +) diff --git a/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go b/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go new file mode 100644 index 000000000..d497669e9 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unmarshaler.go @@ -0,0 +1,2334 @@ +package toml + +import ( + "encoding" + "errors" + "fmt" + "io" + "math" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/pelletier/go-toml/v2/internal/tracker" + "github.com/pelletier/go-toml/v2/unstable" +) + +// decoderPool recycles decoders (and their internal buffers: parser arena, +// seen-tracker entries, scratch buffers) across calls to Unmarshal and +// Decode. +var decoderPool = sync.Pool{ + New: func() interface{} { return &decoder{} }, +} + +func getDecoder(strictMode, unmarshalerInterface bool) *decoder { + d := decoderPool.Get().(*decoder) + d.reset() + d.strict.Enabled = strictMode + d.unmarshalerInterface = unmarshalerInterface + return d +} + +func putDecoder(d *decoder) { + decoderPool.Put(d) +} + +// reset clears the per-document state of the decoder, keeping the allocated +// buffers for reuse. +func (d *decoder) reset() { + d.seen.Reset() + d.tableKey = d.tableKey[:0] + d.skipUntilTable = false + d.path = d.path[:0] + d.captures = d.captures[:0] + d.captureIdx = -1 + d.segIdx = d.segIdx[:0] + // Reuse the array-table counter slots across documents instead of + // deleting them: a zeroed slot is indistinguishable from an absent one, + // and keeping it alive means setArrayCount does not have to allocate a new + // *int every time the same path reappears. A safety valve bounds the table + // for adversarial inputs that introduce unboundedly many distinct paths. + if len(d.arrayCounts) > 1<<14 { + d.arrayCounts = nil + } else { + for _, p := range d.arrayCounts { + *p = 0 + } + } + d.tableTarget = reflect.Value{} + d.tableTargetValid = false + d.tableFlush = d.tableFlush[:0] + d.tableParentSlot = slotWriter{} + d.keyParts = d.keyParts[:0] + d.strict.Reset() +} + +// Unmarshal deserializes a TOML document into a Go value. +// +// It is a shortcut for Decoder.Decode() with the default options. +func Unmarshal(data []byte, v interface{}) error { + d := getDecoder(false, false) + err := d.unmarshal(data, v) + putDecoder(d) + return err +} + +// Decoder reads and decode a TOML document from an input stream. +type Decoder struct { + // input + r io.Reader + + // global settings + strict bool + + // toggles unmarshaler interface + unmarshalerInterface bool +} + +// NewDecoder creates a new Decoder that will read from r. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r: r} +} + +// DisallowUnknownFields causes the Decoder to return an error when the +// destination is a struct and the input contains a key that does not match a +// non-ignored field. +// +// In that case, the Decoder returns a StrictMissingError that can be used to +// retrieve the individual errors as well as generate a human readable +// description of the missing fields. +func (d *Decoder) DisallowUnknownFields() *Decoder { + d.strict = true + return d +} + +// EnableUnmarshalerInterface allows to enable unmarshaler interface. +// +// With this feature enabled, types implementing the unstable.Unmarshaler +// interface can be decoded from any structure of the document. It allows types +// that don't have a straightforward TOML representation to provide their own +// decoding logic. +// +// The UnmarshalTOML method receives raw TOML bytes: +// - For single values: the raw value bytes (e.g., `"hello"` for a string) +// - For tables: all key-value lines belonging to that table +// - For inline tables/arrays: the raw bytes of the inline structure +// +// The unstable.RawMessage type can be used to capture raw TOML bytes for +// later processing, similar to json.RawMessage. +// +// *Unstable:* This method does not follow the compatibility guarantees of +// semver. It can be changed or removed without a new major version being +// issued. +func (d *Decoder) EnableUnmarshalerInterface() *Decoder { + d.unmarshalerInterface = true + return d +} + +// Decode the whole content of r into v. +// +// By default, values in the document that don't exist in the target Go value +// are ignored. See Decoder.DisallowUnknownFields() to change this behavior. +// +// When a TOML local date, time, or date-time is decoded into a time.Time, its +// value is represented in time.Local timezone. Otherwise the appropriate Local* +// structure is used. For time values, precision up to the nanosecond is +// supported by truncating extra digits. +// +// Empty tables decoded in an interface{} create an empty initialized +// map[string]interface{}. +// +// Types implementing the encoding.TextUnmarshaler interface are decoded from a +// TOML string. +// +// When decoding a number, go-toml will return an error if the number is out of +// bounds for the target type (which includes negative numbers when decoding +// into an unsigned int). +// +// If an error occurs while decoding the content of the document, this function +// returns a toml.DecodeError, providing context about the issue. When using +// strict mode and a field is missing, a `toml.StrictMissingError` is +// returned. In any other case, this function returns a standard Go error. +// +// # Type mapping +// +// List of supported TOML types and their associated accepted Go types: +// +// String -> string +// Integer -> uint*, int*, depending on size +// Float -> float*, depending on size +// Boolean -> bool +// Offset Date-Time -> time.Time +// Local Date-time -> LocalDateTime, time.Time +// Local Date -> LocalDate, time.Time +// Local Time -> LocalTime, time.Time +// Array -> slice and array, depending on elements types +// Table -> map and struct +// Inline Table -> same as Table +// Array of Tables -> same as Array and Table +func (d *Decoder) Decode(v interface{}) error { + b, err := io.ReadAll(d.r) + if err != nil { + return fmt.Errorf("toml: %w", err) + } + + dec := getDecoder(d.strict, d.unmarshalerInterface) + err = dec.unmarshal(b, v) + putDecoder(dec) + return err +} + +// pathPart is one part of the key path leading to a value. Parts that come +// from the current table header only carry a name; parts that come from the +// key of the current key-value expression also carry the AST node, and their +// name is materialized lazily to avoid allocations. +type pathPart struct { + name string + node *unstable.Node +} + +// bytes returns the raw bytes of the key part. +func (p *pathPart) bytes() []byte { + if p.node != nil { + return p.node.Data + } + return []byte(p.name) +} + +// str returns the key part as a string, possibly allocating. +func (p *pathPart) str() string { + if p.node != nil { + return string(p.node.Data) + } + return p.name +} + +// rawCapture accumulates the raw bytes fed to a type implementing +// unstable.Unmarshaler for a table target. The target is identified by the +// parts of its key and the array-table indexes in effect when the capture +// was created, so that it can be located again once the whole document has +// been processed (the address of the target may change as slices grow). +type rawCapture struct { + names []string + // indexes[i] is the index to use when reaching a slice or array right + // before consuming names[i]. indexes[len(names)] is the index of the + // element when the target is an element of an array table. -1 when not + // relevant. + indexes []int + buf []byte +} + +type decoder struct { + p unstable.Parser + + // strict mode + strict strict + + // toggles unmarshaler interface + unmarshalerInterface bool + + // tracks the duplicate and type consistency of the keys + seen tracker.SeenTracker + + // path of the current table header, as copied strings + tableKey []string + + // true when the expressions under the current table header cannot be + // stored anywhere and should be skipped + skipUntilTable bool + + // scratch buffer for the key path of the current expression + path []pathPart + + // raw captures for the unmarshaler interface, in order of first + // appearance. captureIdx is the index of the capture the current table + // belongs to, or -1. + captures []rawCapture + captureIdx int + + // segIdx[i] records the array element index used when traversing a + // slice or array right before consuming the i-th part of the current + // table key. Reset for each table expression. + segIdx []int + + // arrayCounts tracks the number of elements appended to fixed-size + // arrays used as array tables, keyed by the NUL-joined key parts. + // Values are pointer slots so that updating an existing path does not + // allocate a new key string. + arrayCounts map[string]*int + + // Cached target of the current table, so that key-values do not need to + // walk the document structure from the root for every expression. + // tableFlush holds the write-backs to perform when leaving the table + // (for targets reached through map values, which are copies). + // tableParentSlot stores a replacement of the target itself (e.g. a nil + // map that was allocated) into its parent. + tableTarget reflect.Value + tableTargetValid bool + tableFlush []flushOp + tableParentSlot slotWriter + + // strKey is a reusable string value used as map key, so that map + // operations with string keys do not need to allocate a boxed key for + // every access. It must be refreshed with stringMapKey immediately + // before each use: any recursive call may overwrite it. + strKey reflect.Value + + // interned de-duplicates key strings: documents repeat the same keys + // over and over, and the table survives pooling, so repeated decodes + // of similar documents stop allocating key strings altogether. + interned map[string]string + + // pathScratch is the buffer used by joinPath. + pathScratch []byte + + // keyParts is the reusable buffer holding the decoded parts of the key of + // the current expression in the fused generic decode path. + keyParts [][]byte +} + +// slotWriter remembers how to store a value at some location of the target +// structure. Implemented as a struct instead of a closure to avoid +// allocations. +type slotWriter struct { + kind uint8 // 0: none, 1: slot.Set, 2: m.SetMapIndex(k, ...), 3: m.SetMapIndex(string key ks, ...) + slot reflect.Value + m reflect.Value + k reflect.Value + ks string +} + +func (d *decoder) storeSlot(s *slotWriter, nv reflect.Value) { + switch s.kind { + case 1: + if s.slot.CanSet() { + s.slot.Set(nv) + } + case 2: + s.m.SetMapIndex(s.k, nv) + case 3: + s.m.SetMapIndex(d.stringMapKey(s.ks), nv) + } +} + +// flushOp stores val using w when the table is flushed. +type flushOp struct { + w slotWriter + val reflect.Value +} + +// flushTable performs the pending write-backs of the cached table target, in +// reverse order so that inner copies land before their parents are stored. +func (d *decoder) flushTable() { + for i := len(d.tableFlush) - 1; i >= 0; i-- { + d.storeSlot(&d.tableFlush[i].w, d.tableFlush[i].val) + } + d.tableFlush = d.tableFlush[:0] + d.tableTargetValid = false + d.tableParentSlot = slotWriter{} + d.tableTarget = reflect.Value{} +} + +// intern returns the string corresponding to the given bytes, reusing a +// previous allocation when the same key has been seen before. +func (d *decoder) intern(b []byte) string { + if s, ok := d.interned[string(b)]; ok { // does not allocate + return s + } + if d.interned == nil { + d.interned = make(map[string]string, 64) + } else if len(d.interned) >= 1<<14 { + // Safety valve for adversarial inputs: do not let the table grow + // without bounds. + for k := range d.interned { + delete(d.interned, k) + } + } + s := string(b) + d.interned[s] = s + return s +} + +// partString returns the name of a path part, interning it when it comes +// from the document. +func (d *decoder) partString(p *pathPart) string { + if p.node != nil { + return d.intern(p.node.Data) + } + return p.name +} + +// stringMapKey returns a reflect.Value holding the given string, reusing the +// same allocation every time. The result must be used (the map operation +// performed) before any recursive call, which may overwrite the buffer. +func (d *decoder) stringMapKey(s string) reflect.Value { + if !d.strKey.IsValid() { + d.strKey = reflect.New(stringType).Elem() + } + d.strKey.SetString(s) + return d.strKey +} + +// joinPath builds the NUL-joined representation of a key path in the +// decoder's scratch buffer. The result is only valid until the next call. +func (d *decoder) joinPath(parts []string) []byte { + d.pathScratch = d.pathScratch[:0] + for i, p := range parts { + if i > 0 { + d.pathScratch = append(d.pathScratch, 0) + } + d.pathScratch = append(d.pathScratch, p...) + } + return d.pathScratch +} + +// arrayCount returns the number of elements appended so far to the array +// table at the given path. +func (d *decoder) arrayCount(key []byte) int { + if d.arrayCounts == nil { + return 0 + } + if p := d.arrayCounts[string(key)]; p != nil { // does not allocate + return *p + } + return 0 +} + +func (d *decoder) setArrayCount(key []byte, n int) { + if d.arrayCounts == nil { + d.arrayCounts = map[string]*int{} + } + if p := d.arrayCounts[string(key)]; p != nil { // does not allocate + *p = n + return + } + v := n + d.arrayCounts[string(key)] = &v +} + +// resetChildArrayCounts forgets the counts of all the array tables under +// the given path, so that a new element starts fresh. +func (d *decoder) resetChildArrayCounts(key []byte) { + if len(d.arrayCounts) == 0 { + return + } + for k, p := range d.arrayCounts { + // Prefix match without building the prefix string: same bytes as + // key, followed by the NUL separator. + if len(k) > len(key) && k[len(key)] == 0 && k[:len(key)] == string(key) { + // Zero instead of delete: the next element of the parent table + // will reuse the slot without allocating a new key. + *p = 0 + } + } +} + +func (d *decoder) typeMismatchError(toml string, target reflect.Type, highlight []byte) error { + return &typeMismatchError{ + toml: toml, + target: target, + highlight: highlight, + } +} + +type typeMismatchError struct { + toml string + target reflect.Type + highlight []byte + // key is the TOML key being processed when the mismatch occurred. It is + // populated lazily as the error propagates back up to the key-value + // handler (see contextualizeError). + key Key +} + +func (e *typeMismatchError) Error() string { + return fmt.Sprintf("cannot decode TOML %s into %s", e.toml, e.target) +} + +// contextualizeError attaches the TOML key currently being processed to errors +// raised while decoding a key-value expression, so that DecodeError.Key() +// reports the offending key (e.g. on type mismatch errors). The current key is +// reconstructed from d.path; when the table target is cached, d.path holds only +// the key-value parts, so the table key prefix is prepended. This only runs on +// the error path and adds no cost to successful decodes. +func (d *decoder) contextualizeError(err error, withTableKey bool) error { + var mm *typeMismatchError + if errors.As(err, &mm) { + if mm.key == nil { + mm.key = d.currentKey(withTableKey) + } + return err + } + var perr *unstable.ParserError + if errors.As(err, &perr) { + if perr.Key == nil { + perr.Key = d.currentKey(withTableKey) + } + } + return err +} + +// currentKey reconstructs the full TOML key being processed from the decoder's +// path. When withTableKey is true, d.path contains only the key-value parts +// (the table target is cached) and the table key is prepended. +func (d *decoder) currentKey(withTableKey bool) Key { + n := len(d.path) + if withTableKey { + n += len(d.tableKey) + } + key := make(Key, 0, n) + if withTableKey { + key = append(key, d.tableKey...) + } + for i := range d.path { + key = append(key, d.path[i].str()) + } + return key +} + +func (d *decoder) unmarshal(data []byte, v interface{}) error { + r := reflect.ValueOf(v) + if r.Kind() != reflect.Ptr { + return fmt.Errorf("toml: decoding can only be performed into a pointer, not %s", r.Kind()) + } + if r.IsNil() { + return errors.New("toml: decoding pointer target cannot be nil") + } + + root := r.Elem() + + d.captureIdx = -1 + d.p.Reset(data) + + // Fully generic targets (interface{} or map[string]interface{}) are + // decoded straight into native Go maps and slices, with no reflection on + // the document structure at all. This covers the common "decode arbitrary + // TOML into a map" case, including every standard benchmark dataset. + if !d.unmarshalerInterface { + if k := root.Kind(); k == reflect.Interface || (k == reflect.Map && root.Type() == mapStringInterfaceType) { + return d.unmarshalFused(root, data) + } + } + + for d.p.NextExpression() { + err := d.handleRootExpression(d.p.Expression(), root) + if err != nil { + return d.wrapError(data, err) + } + } + if err := d.p.Error(); err != nil { + var perr *unstable.ParserError + if errors.As(err, &perr) { + return wrapDecodeError(data, perr) + } + return err + } + + d.flushTable() + + // Deliver the accumulated raw documents to the unmarshaler-interface + // targets. + for i := range d.captures { + nv, err := d.resolveCapture(root, &d.captures[i], 0, false) + if err != nil { + return err + } + if nv.IsValid() { + root.Set(nv) + } + } + + // An empty document into a generic target still initializes it. + switch root.Kind() { + case reflect.Map: + if root.IsNil() { + root.Set(reflect.MakeMap(root.Type())) + } + case reflect.Interface: + if root.IsNil() { + root.Set(reflect.ValueOf(map[string]interface{}{})) + } + default: + } + + return d.strict.Error(data) +} + +// setAnyKey assigns the value of a key-value into the native map m, following +// the (possibly dotted) key and creating intermediate maps as needed. +func (d *decoder) setAnyKey(m map[string]interface{}, key unstable.Iterator, value *unstable.Node) error { + cur := m + for key.Next() { + name := d.intern(key.Node().Data) + if key.IsLast() { + av, err := d.decodeAny(value) + if err != nil { + return err + } + cur[name] = av + return nil + } + cur = d.anyChildTable(cur, name) + } + return nil +} + +// anyChildTable returns the child table at name within cur, creating it if +// absent and descending into the current (last) element when an array table +// occupies the slot. A non-container in the slot cannot occur for a document +// the seen-tracker has accepted. +func (d *decoder) anyChildTable(cur map[string]interface{}, name string) map[string]interface{} { + switch v := cur[name].(type) { + case map[string]interface{}: + return v + case []interface{}: + if len(v) > 0 { + if last, ok := v[len(v)-1].(map[string]interface{}); ok { + return last + } + } + } + nm := map[string]interface{}{} + cur[name] = nm + return nm +} + +// wrapError gives document context to errors generated while processing an +// expression. +func (d *decoder) wrapError(data []byte, err error) error { + var perr *unstable.ParserError + if errors.As(err, &perr) { + return wrapDecodeError(data, perr) + } + var mm *typeMismatchError + if errors.As(err, &mm) { + return wrapDecodeError(data, &unstable.ParserError{ + Highlight: mm.highlight, + Message: mm.Error(), + Key: mm.key, + }) + } + return err +} + +// wrapSeenError turns an error returned by SeenTracker.CheckExpression into a +// ParserError carrying the position and key of the offending expression, so +// that redefinition and duplicate-key errors are reported as a DecodeError +// with context (see issue #668). +// +// The highlight spans the expression's key. Unlike Node.Raw, key nodes always +// carry a Raw range, so this works for tables and array tables too (whose own +// Raw range is not set by the parser). For a duplicate detected inside an +// inline table, node is the enclosing key-value expression, so the error +// points at that expression's key. +func (d *decoder) wrapSeenError(node *unstable.Node, err error) error { + if err == nil { + return nil + } + + var key Key + var start, end unstable.Range + it := node.Key() + for it.Next() { + n := it.Node() + key = append(key, string(n.Data)) + if len(key) == 1 { + start = n.Raw + } + end = n.Raw + } + + var highlight []byte + if len(key) > 0 { + highlight = d.p.Raw(unstable.Range{ + Offset: start.Offset, + Length: end.Offset + end.Length - start.Offset, + }) + } + + return &unstable.ParserError{ + Highlight: highlight, + Message: strings.TrimPrefix(err.Error(), "toml: "), + Key: key, + } +} + +func (d *decoder) handleRootExpression(expr *unstable.Node, root reflect.Value) error { + first, err := d.seen.CheckExpression(expr) + if err != nil { + return d.wrapSeenError(expr, err) + } + + switch expr.Kind { + case unstable.KeyValue: + if d.skipUntilTable { + return nil + } + if d.captureIdx >= 0 { + d.captureKeyValue(expr) + return nil + } + return d.handleKeyValueExpression(expr, root) + case unstable.Table: + d.flushTable() + d.skipUntilTable = false + d.captureIdx = -1 + d.strict.EnterTable(expr) + return d.handleTableExpression(expr, root, false, first) + case unstable.ArrayTable: + d.flushTable() + d.skipUntilTable = false + d.captureIdx = -1 + d.strict.EnterTable(expr) + return d.handleTableExpression(expr, root, true, first) + default: + return unstable.NewParserError(expr.Data, "unsupported expression kind %s", expr.Kind) + } +} + +// updateTableKey copies the parts of the key of a table expression into +// tableKey. +func (d *decoder) updateTableKey(expr *unstable.Node) { + d.tableKey = d.tableKey[:0] + it := expr.Key() + for it.Next() { + d.tableKey = append(d.tableKey, d.intern(it.Node().Data)) + } +} + +func (d *decoder) handleTableExpression(expr *unstable.Node, root reflect.Value, isArrayTable bool, first bool) error { + d.updateTableKey(expr) + + // Check whether this table belongs to an exisiting raw capture (split + // tables, or children of a table assigned to an Unmarshaler). + if d.unmarshalerInterface { + if d.resumeCapture(expr) { + return nil + } + } + + // Reset the per-segment array indexes. + d.segIdx = d.segIdx[:0] + for i := 0; i <= len(d.tableKey); i++ { + d.segIdx = append(d.segIdx, -1) + } + + return d.walkTable(root, expr, isArrayTable, first) +} + +// newContainerElem returns a fresh element for a slice of the given element +// type. Plain interface elements start out as an empty table. +func newContainerElem(et reflect.Type) reflect.Value { + if et == interfaceType { + return reflect.ValueOf(map[string]interface{}{}) + } + return reflect.New(et).Elem() +} + +// walkTable processes a [table] or [[array table]] header: it creates the +// intermediate containers, appends array-table elements, applies the strict +// policy, registers unmarshaler-interface captures, and caches the target +// container so that the key-values that follow are stored directly. +// +// Map values are not addressable: when one needs in-place mutations (struct +// or array values), a copy is made and registered to be stored back when the +// table changes (see flushTable). Maps and slices are references and are +// traversed without copies. +func (d *decoder) walkTable(root reflect.Value, expr *unstable.Node, isArrayTable bool, first bool) error { + v := root + pf := slotWriter{kind: 1, slot: root} + idx := 0 + +walk: + for { + // Dereference pointers in place. + for v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + elem := v.Elem() + pf = slotWriter{kind: 1, slot: elem} + v = elem + } + + // Tables assigned to a type implementing the unmarshaler interface + // are captured as raw bytes, delivered once the document is read. + if d.unmarshalerInterface && hasUnmarshaler(v) { + d.startCapture(idx, expr) + return nil + } + + if idx >= len(d.tableKey) { + break walk + } + + name := d.tableKey[idx] + + switch v.Kind() { + case reflect.Interface: + if !v.IsNil() { + c := v.Elem() + if k := c.Kind(); k == reflect.Map || k == reflect.Slice { + // Reference types: mutations are visible through the + // existing interface value. + v = c + continue + } + } + // Anything else is replaced by a fresh generic map. + if !mapStringInterfaceType.AssignableTo(v.Type()) { + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot store a table in a %s", v.Type()) + } + fresh := reflect.ValueOf(map[string]interface{}{}) + d.storeSlot(&pf, fresh) + v = fresh + case reflect.Slice: + if v.Len() == 0 { + // Implicit creation of the first element: the array table + // that would create it has not been seen yet (issue 995). + if v.IsNil() { + v = reflect.MakeSlice(v.Type(), 0, 4) + } + v = reflect.Append(v, newContainerElem(v.Type().Elem())) + d.storeSlot(&pf, v) + } + n := v.Len() - 1 + d.segIdx[idx] = n + elem := v.Index(n) + pf = slotWriter{kind: 1, slot: elem} + v = elem + case reflect.Array: + key := d.joinPath(d.tableKey[:idx]) + cnt := d.arrayCount(key) + if cnt == 0 { + cnt = 1 + d.setArrayCount(key, 1) + } + if cnt > v.Len() { + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot reach element %d of array of size %d", cnt-1, v.Len()) + } + d.segIdx[idx] = cnt - 1 + elem := v.Index(cnt - 1) + pf = slotWriter{kind: 1, slot: elem} + v = elem + case reflect.Map: + if v.IsNil() { + nm := reflect.MakeMap(v.Type()) + d.storeSlot(&pf, nm) + v = nm + } + var key reflect.Value + var w slotWriter + if v.Type().Key() == stringType { + key = d.stringMapKey(name) + w = slotWriter{kind: 3, m: v, ks: name} + } else { + k, err := makeMapKey(v.Type().Key(), name) + if err != nil { + return err + } + key = k + w = slotWriter{kind: 2, m: v, k: k} + } + + elem := v.MapIndex(key) + + // The last part of an array table is finalized as a slice + // container: do not materialize a table for it. + if isArrayTable && idx == len(d.tableKey)-1 { + et := v.Type().Elem() + switch et.Kind() { + case reflect.Interface, reflect.Slice, reflect.Array: + if elem.IsValid() { + v = elem + } else { + v = reflect.Zero(et) + } + pf = w + idx++ + continue + default: + } + } + + if elem.IsValid() { + ce := elem + ceIface := false + if ce.Kind() == reflect.Interface { + ceIface = true + if !ce.IsNil() { + ce = ce.Elem() + } + } + switch ce.Kind() { + case reflect.Map, reflect.Slice: + pf = w + v = ce + case reflect.Ptr: + if ce.IsNil() { + np := reflect.New(ce.Type().Elem()) + d.storeSlot(&w, np) + ce = np + } + pf = w + v = ce + case reflect.Struct, reflect.Array: + if ceIface { + // Interface-held non-generic content is replaced. + fresh := reflect.ValueOf(map[string]interface{}{}) + d.storeSlot(&w, fresh) + pf = w + v = fresh + } else { + tmp := reflect.New(elem.Type()).Elem() + tmp.Set(elem) + d.tableFlush = append(d.tableFlush, flushOp{w: w, val: tmp}) + pf = slotWriter{kind: 1, slot: tmp} + v = tmp + } + default: + if !ceIface { + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot store a table in a %s", ce.Type()) + } + fresh := reflect.ValueOf(map[string]interface{}{}) + d.storeSlot(&w, fresh) + pf = w + v = fresh + } + } else { + et := v.Type().Elem() + switch et.Kind() { + case reflect.Interface: + if !mapStringInterfaceType.AssignableTo(et) { + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot store a table in a %s", et) + } + fresh := reflect.ValueOf(map[string]interface{}{}) + d.storeSlot(&w, fresh) + pf = w + v = fresh + case reflect.Map: + nm := reflect.MakeMap(et) + d.storeSlot(&w, nm) + pf = w + v = nm + case reflect.Ptr: + np := reflect.New(et.Elem()) + d.storeSlot(&w, np) + pf = w + v = np + case reflect.Struct, reflect.Array, reflect.Slice: + tmp := reflect.New(et).Elem() + d.tableFlush = append(d.tableFlush, flushOp{w: w, val: tmp}) + pf = slotWriter{kind: 1, slot: tmp} + v = tmp + default: + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot store a table in a %s", et) + } + } + idx++ + case reflect.Struct: + plan := planForType(v.Type()) + f, found := plan.lookup(name) + if !found { + d.strict.MissingTable(expr) + d.skipUntilTable = true + return nil + } + fv := fieldByIndexAlloc(v, f.index) + pf = slotWriter{kind: 1, slot: fv} + v = fv + idx++ + default: + return unstable.NewParserError(d.p.Raw(expr.Raw), "cannot store a table in a %s", v.Kind()) + } + } + + if isArrayTable { + akey := d.joinPath(d.tableKey) + d.resetChildArrayCounts(akey) + + // Unwrap an interface container. + if v.Kind() == reflect.Interface { + var slice []interface{} + if !v.IsNil() { + if s, ok := v.Elem().Interface().([]interface{}); ok { + slice = s + } + } + if first { + slice = slice[:0] + } + m := map[string]interface{}{} + slice = append(slice, m) + sv := reflect.ValueOf(slice) + d.storeSlot(&pf, sv) + d.setArrayCount(akey, len(slice)) + d.segIdx[len(d.tableKey)] = len(slice) - 1 + d.tableTarget = reflect.ValueOf(m) + d.tableParentSlot = slotWriter{kind: 1, slot: sv.Index(len(slice) - 1)} + d.tableTargetValid = true + return nil + } + + switch v.Kind() { + case reflect.Slice: + if v.IsNil() { + v = reflect.MakeSlice(v.Type(), 0, 4) + } else if first { + v = v.Slice(0, 0) + } + v = reflect.Append(v, newContainerElem(v.Type().Elem())) + d.storeSlot(&pf, v) + n := v.Len() - 1 + d.setArrayCount(akey, n+1) + d.segIdx[len(d.tableKey)] = n + elem := v.Index(n) + if d.unmarshalerInterface && hasUnmarshaler(elem) { + d.startCapture(len(d.tableKey), expr) + return nil + } + pf = slotWriter{kind: 1, slot: elem} + v = elem + case reflect.Array: + cnt := d.arrayCount(akey) + if first { + cnt = 0 + } + if cnt >= v.Len() { + return unstable.NewParserError(d.p.Raw(expr.Raw), "array of size %d is too small to store this array table", v.Len()) + } + v.Index(cnt).Set(reflect.Zero(v.Type().Elem())) + d.setArrayCount(akey, cnt+1) + d.segIdx[len(d.tableKey)] = cnt + elem := v.Index(cnt) + if d.unmarshalerInterface && hasUnmarshaler(elem) { + d.startCapture(len(d.tableKey), expr) + return nil + } + pf = slotWriter{kind: 1, slot: elem} + v = elem + default: + return fmt.Errorf("toml: cannot store an array table in a %s", v.Kind()) + } + } + + // Settle on the concrete container for the key-values that follow. + for { + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + if !v.CanSet() { + return nil + } + v.Set(reflect.New(v.Type().Elem())) + } + elem := v.Elem() + pf = slotWriter{kind: 1, slot: elem} + v = elem + continue + case reflect.Interface: + if !v.IsNil() { + c := v.Elem() + if c.Type() == mapStringInterfaceType || c.Type() == sliceInterfaceType { + v = c + continue + } + } + if !mapStringInterfaceType.AssignableTo(v.Type()) { + return fmt.Errorf("toml: cannot store a table in a %s", v.Type()) + } + fresh := reflect.ValueOf(map[string]interface{}{}) + d.storeSlot(&pf, fresh) + v = fresh + continue + case reflect.Slice: + if v.Len() == 0 { + if v.IsNil() { + v = reflect.MakeSlice(v.Type(), 0, 4) + } + v = reflect.Append(v, newContainerElem(v.Type().Elem())) + d.storeSlot(&pf, v) + } + n := v.Len() - 1 + d.segIdx[len(d.tableKey)] = n + elem := v.Index(n) + pf = slotWriter{kind: 1, slot: elem} + v = elem + continue + case reflect.Map, reflect.Struct: + d.tableTarget = v + d.tableParentSlot = pf + d.tableTargetValid = true + return nil + default: + return fmt.Errorf("toml: cannot store a table in a %s", v.Kind()) + } + } +} + +// resumeCapture looks for an existing capture this table expression belongs +// to. It returns true if the expression was consumed. +func (d *decoder) resumeCapture(expr *unstable.Node) bool { + // Iterate in reverse, so that tables attach to the latest element of + // array tables. + for i := len(d.captures) - 1; i >= 0; i-- { + c := &d.captures[i] + if len(d.tableKey) < len(c.names) { + continue + } + if expr.Kind == unstable.ArrayTable && len(d.tableKey) == len(c.names) { + // A new element of an array table is not part of the capture of + // the previous element. + continue + } + match := true + for j, p := range c.names { + if d.tableKey[j] != p { + match = false + break + } + } + if !match { + continue + } + d.captureIdx = i + if len(d.tableKey) > len(c.names) { + d.appendCaptureHeader(c, expr, len(c.names)) + } + return true + } + return false +} + +// appendCaptureHeader writes the table header of expr in the capture buffer, +// adjusted to be relative to the capture root. +func (d *decoder) appendCaptureHeader(c *rawCapture, expr *unstable.Node, skip int) { + c.buf = append(c.buf, '[') + if expr.Kind == unstable.ArrayTable { + c.buf = append(c.buf, '[') + } + c.buf = append(c.buf, d.rawKeySuffix(expr, skip)...) + c.buf = append(c.buf, ']') + if expr.Kind == unstable.ArrayTable { + c.buf = append(c.buf, ']') + } + c.buf = append(c.buf, '\n') +} + +// rawKeySuffix returns the raw bytes of the key of the expression, skipping +// the first n parts. +func (d *decoder) rawKeySuffix(expr *unstable.Node, n int) []byte { + it := expr.Key() + idx := 0 + var start, end unstable.Range + for it.Next() { + if idx >= n { + r := it.Node().Raw + if start.Length == 0 && start.Offset == 0 && idx == n { + start = r + } + end = r + } + idx++ + } + return d.p.Data()[start.Offset : end.Offset+end.Length] +} + +// startCapture registers a new capture for the table at the given path +// (prefix of tableKey). +func (d *decoder) startCapture(pathLen int, expr *unstable.Node) { + names := make([]string, pathLen) + copy(names, d.tableKey[:pathLen]) + indexes := make([]int, pathLen+1) + copy(indexes, d.segIdx[:pathLen+1]) + d.captures = append(d.captures, rawCapture{ + names: names, + indexes: indexes, + }) + d.captureIdx = len(d.captures) - 1 + if pathLen < len(d.tableKey) { + d.appendCaptureHeader(&d.captures[d.captureIdx], expr, pathLen) + } +} + +// resolveCapture walks back to the target of a capture and delivers the +// accumulated raw bytes to its UnmarshalTOML implementation. +func (d *decoder) resolveCapture(v reflect.Value, c *rawCapture, idx int, indexed bool) (reflect.Value, error) { + if v.Kind() == reflect.Ptr { + if v.Type().Implements(unmarshalerType) && idx == len(c.names) { + u, _ := unmarshalerOf(v) + return v, u.UnmarshalTOML(c.buf) + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + nv, err := d.resolveCapture(v.Elem(), c, idx, indexed) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + v.Elem().Set(nv) + } + return v, nil + } + + if !indexed && (v.Kind() == reflect.Slice || v.Kind() == reflect.Array) && c.indexes[idx] >= 0 { + i := c.indexes[idx] + if i >= v.Len() { + return reflect.Value{}, errors.New("toml: internal error: capture index out of range") + } + elem := v.Index(i) + nv, err := d.resolveCapture(elem, c, idx, true) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + elem.Set(nv) + } + return v, nil + } + + if idx == len(c.names) { + u, ok := unmarshalerOf(v) + if !ok { + return reflect.Value{}, errors.New("toml: internal error: capture target does not implement UnmarshalTOML") + } + return v, u.UnmarshalTOML(c.buf) + } + + name := c.names[idx] + + switch v.Kind() { + case reflect.Struct: + plan := planForType(v.Type()) + f, found := plan.lookup(name) + if !found { + return v, nil + } + fv := fieldByIndexAlloc(v, f.index) + nv, err := d.resolveCapture(fv, c, idx+1, false) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() && fv.CanSet() { + fv.Set(nv) + } + return v, nil + case reflect.Map: + key, err := makeMapKey(v.Type().Key(), name) + if err != nil { + return reflect.Value{}, err + } + if v.IsNil() { + v = reflect.MakeMap(v.Type()) + } + elem := reflect.New(v.Type().Elem()).Elem() + if existing := v.MapIndex(key); existing.IsValid() { + elem.Set(existing) + } + nv, err := d.resolveCapture(elem, c, idx+1, false) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + v.SetMapIndex(key, nv) + } + return v, nil + case reflect.Interface: + elem := elemOrNewMap(v) + nv, err := d.resolveCapture(elem, c, idx, indexed) + if err != nil || !nv.IsValid() { + return reflect.Value{}, err + } + return nv, nil + default: + return reflect.Value{}, fmt.Errorf("toml: internal error: cannot resolve capture target through %s", v.Kind()) + } +} + +// captureKeyValue appends the raw bytes of a key-value expression to the +// current capture. +func (d *decoder) captureKeyValue(expr *unstable.Node) { + c := &d.captures[d.captureIdx] + c.buf = append(c.buf, d.p.Raw(expr.Raw)...) + c.buf = append(c.buf, '\n') +} + +// hasUnmarshaler reports whether v can provide an unstable.Unmarshaler, +// without allocating anything. +func hasUnmarshaler(v reflect.Value) bool { + t := v.Type() + return t.Implements(unmarshalerType) || (v.CanAddr() && reflect.PtrTo(t).Implements(unmarshalerType)) +} + +// makeMapKey converts a TOML key into a value usable as the given map key +// type. +func makeMapKey(kt reflect.Type, name string) (reflect.Value, error) { + switch kt.Kind() { + case reflect.String: + return reflect.ValueOf(name).Convert(kt), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(name, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("toml: cannot parse map key %q as %s: %w", name, kt, err) + } + k := reflect.New(kt).Elem() + if k.OverflowInt(i) { + return reflect.Value{}, fmt.Errorf("toml: map key %q overflows %s", name, kt) + } + k.SetInt(i) + return k, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + u, err := strconv.ParseUint(name, 10, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("toml: cannot parse map key %q as %s: %w", name, kt, err) + } + k := reflect.New(kt).Elem() + if k.OverflowUint(u) { + return reflect.Value{}, fmt.Errorf("toml: map key %q overflows %s", name, kt) + } + k.SetUint(u) + return k, nil + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(name, 64) + if err != nil { + return reflect.Value{}, fmt.Errorf("toml: cannot parse map key %q as %s: %w", name, kt, err) + } + k := reflect.New(kt).Elem() + k.SetFloat(f) + return k, nil + case reflect.Ptr: + if kt.Implements(textUnmarshalerType) { + k := reflect.New(kt.Elem()) + err := k.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(name)) + if err != nil { + return reflect.Value{}, fmt.Errorf("toml: error unmarshaling map key %q: %w", name, err) + } + return k, nil + } + default: + if reflect.PtrTo(kt).Implements(textUnmarshalerType) { + k := reflect.New(kt) + err := k.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(name)) + if err != nil { + return reflect.Value{}, fmt.Errorf("toml: error unmarshaling map key %q: %w", name, err) + } + return k.Elem(), nil + } + } + return reflect.Value{}, fmt.Errorf("toml: cannot decode a key into a map with key type %s", kt) +} + +// elemOrNewMap unwraps an interface value to descend into it. Contents that +// can hold a table (generic maps and slices) are kept; anything else is +// replaced by a fresh map[string]interface{}. Maps and slices are reference +// types: they are returned directly, not copied. +func elemOrNewMap(v reflect.Value) reflect.Value { + if !v.IsNil() { + concrete := v.Elem() + t := concrete.Type() + if t == mapStringInterfaceType || t == sliceInterfaceType { + return concrete + } + } + return reflect.ValueOf(map[string]interface{}{}) +} + +// handleKeyValueExpression stores the value of a top-level key-value +// expression, relative to the current table. +func (d *decoder) handleKeyValueExpression(expr *unstable.Node, root reflect.Value) error { + d.path = d.path[:0] + + target := root + useCache := d.tableTargetValid && len(d.tableKey) > 0 + if useCache { + target = d.tableTarget + } else { + for _, name := range d.tableKey { + d.path = append(d.path, pathPart{name: name}) + } + } + + it := expr.Key() + for it.Next() { + d.path = append(d.path, pathPart{node: it.Node()}) + } + + nv, err := d.descend(target, d.path, 0, expr, expr.Value()) + if err != nil { + return d.contextualizeError(err, useCache) + } + if !nv.IsValid() { + return nil + } + if useCache { + // The target may have been replaced (e.g. a nil map allocated): + // re-link it into its parent. + if nv.Kind() == reflect.Map && nv.Pointer() != d.tableTarget.Pointer() { + d.storeSlot(&d.tableParentSlot, nv) + d.tableTarget = nv + } + } else { + if root.CanSet() { + root.Set(nv) + } + } + return nil +} + +// descend walks the given key path into v, and assigns the value at the +// end. It returns the value to store back at this level. An invalid value +// means nothing should be stored (e.g. unknown field). +func (d *decoder) descend(v reflect.Value, path []pathPart, idx int, expr *unstable.Node, value *unstable.Node) (reflect.Value, error) { + if idx == len(path) { + return d.assignValue(v, expr, value) + } + + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + nv, err := d.descend(v.Elem(), path, idx, expr, value) + if err != nil || !nv.IsValid() { + return reflect.Value{}, err + } + v.Elem().Set(nv) + return v, nil + } + + // A target implementing the unmarshaler interface consumes the value, + // whatever the remaining parts of the key are. + if d.unmarshalerInterface { + if u, ok := unmarshalerOf(v); ok { + return v, u.UnmarshalTOML(d.rawValue(expr, value)) + } + } + + part := path[idx] + + switch v.Kind() { + case reflect.Map: + // Native fast path for the most common generic target: walk the + // remaining dotted-key path with plain Go map operations and decode + // the value directly, skipping the reflect.Value round-trips + // (stringMapKey, MapIndex, New, SetMapIndex) entirely. + if !d.unmarshalerInterface && v.Type() == mapStringInterfaceType { + return d.descendStrMap(v, path, idx, value) + } + var name string + var key reflect.Value + var err error + fastKey := v.Type().Key() == stringType + if fastKey { + name = d.partString(&part) + key = d.stringMapKey(name) + } else { + key, err = makeMapKey(v.Type().Key(), d.partString(&part)) + if err != nil { + return reflect.Value{}, err + } + } + if v.IsNil() { + v = reflect.MakeMap(v.Type()) + } + elemType := v.Type().Elem() + existing := v.MapIndex(key) + var elem reflect.Value + switch { + case existing.IsValid(): + elem = reflect.New(elemType).Elem() + elem.Set(existing) + case idx+1 == len(path) && elemType.Kind() == reflect.Interface: + // Fast path: a fresh interface element does not need to be + // materialized, the assigned value is stored directly. + elem = reflect.Zero(elemType) + default: + elem = reflect.New(elemType).Elem() + } + nv, err := d.descend(elem, path, idx+1, expr, value) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + if fastKey { + // The recursion may have overwritten the key buffer. + key = d.stringMapKey(name) + } + v.SetMapIndex(key, nv) + } + return v, nil + case reflect.Struct: + plan := planForType(v.Type()) + f, found := plan.lookupBytes(part.bytes()) + if !found { + if part.node != nil { + d.strict.MissingField(expr) + } + return v, nil + } + fv := fieldByIndexAlloc(v, f.index) + var nv reflect.Value + var err error + if idx+1 == len(path) { + // Leaf field: assign directly. descend's first action for a + // fully-consumed path is exactly this call, so skipping the extra + // frame is equivalent and avoids a call per scalar field. + nv, err = d.assignValue(fv, expr, value) + } else { + nv, err = d.descend(fv, path, idx+1, expr, value) + } + if err != nil { + var mm *typeMismatchError + if errors.As(err, &mm) { + err = &unstable.ParserError{ + Highlight: mm.highlight, + Message: fmt.Sprintf("cannot decode TOML %s into struct field %s.%s of type %s", + mm.toml, v.Type(), f.fieldName, mm.target), + } + } + return reflect.Value{}, err + } + if nv.IsValid() && fv.CanSet() { + fv.Set(nv) + } + return v, nil + case reflect.Interface: + elem := elemOrNewMap(v) + nv, err := d.descend(elem, path, idx, expr, value) + if err != nil || !nv.IsValid() { + return reflect.Value{}, err + } + return nv, nil + case reflect.Slice: + if v.Len() == 0 { + if v.IsNil() { + v = reflect.MakeSlice(v.Type(), 0, 4) + } + v = reflect.Append(v, reflect.New(v.Type().Elem()).Elem()) + } + elem := v.Index(v.Len() - 1) + nv, err := d.descend(elem, path, idx, expr, value) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + elem.Set(nv) + } + return v, nil + case reflect.Array: + names := make([]string, idx) + for i := range names { + names[i] = path[i].str() + } + cnt := d.arrayCount(d.joinPath(names)) + if cnt == 0 { + cnt = 1 + } + elemIdx := cnt - 1 + if elemIdx >= v.Len() { + return reflect.Value{}, unstable.NewParserError(keyHighlight(d.p.Data(), part.node), + "cannot reach element %d of array of size %d", elemIdx, v.Len()) + } + elem := v.Index(elemIdx) + nv, err := d.descend(elem, path, idx, expr, value) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + elem.Set(nv) + } + return v, nil + default: + return reflect.Value{}, d.typeMismatchError("table", v.Type(), keyHighlight(d.p.Data(), part.node)) + } +} + +// descendStrMap assigns into a native map[string]interface{} target, following +// the remaining dotted-key parts with plain Go map operations and decoding the +// value with decodeAny. It returns the map to store back at this level: a new +// map when v was nil, otherwise v unchanged, since maps are reference types and +// are mutated in place. +func (d *decoder) descendStrMap(v reflect.Value, path []pathPart, idx int, value *unstable.Node) (reflect.Value, error) { + var m map[string]interface{} + if v.IsNil() { + m = make(map[string]interface{}) + v = reflect.ValueOf(m) + } else { + m = v.Interface().(map[string]interface{}) + } + + // Walk intermediate parts, creating or reusing nested generic maps. A + // non-map value at an intermediate key can only occur in a document the + // seen-tracker has already rejected; replacing it mirrors the reflect + // path (elemOrNewMap). + for ; idx < len(path)-1; idx++ { + name := d.partString(&path[idx]) + child, _ := m[name].(map[string]interface{}) + if child == nil { + child = make(map[string]interface{}) + m[name] = child + } + m = child + } + + av, err := d.decodeAny(value) + if err != nil { + return reflect.Value{}, err + } + m[d.partString(&path[idx])] = av + return v, nil +} + +// keyHighlight returns a highlight for the given key part node, falling back +// to the start of the document. +func keyHighlight(doc []byte, node *unstable.Node) []byte { + if node == nil { + return doc[0:0] + } + return doc[node.Raw.Offset : node.Raw.Offset+node.Raw.Length] +} + +// rawValue returns the raw bytes of the value of a key-value expression. +func (d *decoder) rawValue(expr *unstable.Node, value *unstable.Node) []byte { + if value.Kind != unstable.InlineTable && value.Kind != unstable.Array { + return d.p.Raw(value.Raw) + } + if expr == nil || expr.Kind != unstable.KeyValue { + // Inline container nested in another container: best effort. + return d.p.Raw(value.Raw) + } + // Reconstruct the span of the value: it starts after the equal sign + // following the last part of the key, and stops at the end of the + // expression. + var last unstable.Range + it := expr.Key() + for it.Next() { + last = it.Node().Raw + } + doc := d.p.Data() + i := int(last.Offset + last.Length) + for i < len(doc) && (doc[i] == ' ' || doc[i] == '\t') { + i++ + } + i++ // equal sign + for i < len(doc) && (doc[i] == ' ' || doc[i] == '\t') { + i++ + } + end := int(expr.Raw.Offset + expr.Raw.Length) + return doc[i:end] +} + +// unmarshalerOf returns the unstable.Unmarshaler implementation of v, if +// any. It allocates intermediate pointers as needed. +func unmarshalerOf(v reflect.Value) (unstable.Unmarshaler, bool) { + t := v.Type() + if t.Implements(unmarshalerType) { + if v.Kind() == reflect.Ptr && v.IsNil() { + v.Set(reflect.New(t.Elem())) + } + return v.Interface().(unstable.Unmarshaler), true + } + if v.CanAddr() && reflect.PtrTo(t).Implements(unmarshalerType) { + return v.Addr().Interface().(unstable.Unmarshaler), true + } + return nil, false +} + +var unmarshalerType = reflect.TypeOf(new(unstable.Unmarshaler)).Elem() + +// assignValue stores the TOML value carried by the node into v. +func (d *decoder) assignValue(v reflect.Value, expr *unstable.Node, value *unstable.Node) (reflect.Value, error) { + if v.Kind() == reflect.Ptr { + if d.unmarshalerInterface { + if u, ok := unmarshalerOf(v); ok { + return v, u.UnmarshalTOML(d.rawValue(expr, value)) + } + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + nv, err := d.assignValue(v.Elem(), expr, value) + if err != nil || !nv.IsValid() { + return reflect.Value{}, err + } + v.Elem().Set(nv) + return v, nil + } + + if d.unmarshalerInterface { + if u, ok := unmarshalerOf(v); ok { + return v, u.UnmarshalTOML(d.rawValue(expr, value)) + } + } + + switch value.Kind { + case unstable.String: + return d.assignString(v, value) + case unstable.Integer: + return d.assignInteger(v, value) + case unstable.Float: + return d.assignFloat(v, value) + case unstable.Bool: + return d.assignBool(v, value) + case unstable.DateTime: + return d.assignDateTime(v, value) + case unstable.LocalDateTime: + return d.assignLocalDateTime(v, value) + case unstable.LocalDate: + return d.assignLocalDate(v, value) + case unstable.LocalTime: + return d.assignLocalTime(v, value) + case unstable.Array: + return d.assignArray(v, expr, value) + case unstable.InlineTable: + return d.assignInlineTable(v, expr, value) + default: + return reflect.Value{}, unstable.NewParserError(value.Data, "unsupported value kind %s", value.Kind) + } +} + +func (d *decoder) assignString(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + switch v.Kind() { + case reflect.String: + v.SetString(string(value.Data)) + return v, nil + case reflect.Interface: + return boxInto(v, reflect.ValueOf(string(value.Data))) + default: + } + if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) { + err := v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(value.Data) + if err != nil { + return reflect.Value{}, unstable.NewParserError(d.p.Raw(value.Raw), "%s", err) + } + return v, nil + } + return reflect.Value{}, d.typeMismatchError("string", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignInteger(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + // Integer values targeting a float field are parsed as floats: they can + // represent (approximately) numbers beyond the int64 range. + if k := v.Kind(); k == reflect.Float32 || k == reflect.Float64 { + return d.assignFloat(v, value) + } + + i, err := parseInteger(value.Data) + if err != nil { + return reflect.Value{}, err + } + + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v.OverflowInt(i) { + return reflect.Value{}, unstable.NewParserError(value.Data, "integer value %d cannot be stored in %s", i, v.Type()) + } + v.SetInt(i) + return v, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if i < 0 { + return reflect.Value{}, unstable.NewParserError(value.Data, "negative integer value %d cannot be stored in %s", i, v.Type()) + } + if v.OverflowUint(uint64(i)) { + return reflect.Value{}, unstable.NewParserError(value.Data, "integer value %d cannot be stored in %s", i, v.Type()) + } + v.SetUint(uint64(i)) + return v, nil + case reflect.Interface: + return boxInto(v, reflect.ValueOf(i)) + default: + } + if ok, err := tryTextUnmarshaler(v, value.Data); ok { + return v, err + } + return reflect.Value{}, d.typeMismatchError("integer", v.Type(), d.p.Raw(value.Raw)) +} + +// tryTextUnmarshaler attempts to deliver the raw text of a value to a target +// implementing encoding.TextUnmarshaler. +func tryTextUnmarshaler(v reflect.Value, text []byte) (bool, error) { + if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) { + return true, v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(text) + } + return false, nil +} + +func (d *decoder) assignFloat(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + f, err := parseFloat(value.Data) + if err != nil { + return reflect.Value{}, err + } + + switch v.Kind() { + case reflect.Float64: + v.SetFloat(f) + return v, nil + case reflect.Float32: + if !math.IsInf(f, 0) && math.Abs(f) > math.MaxFloat32 { + return reflect.Value{}, unstable.NewParserError(value.Data, "float value %f cannot be stored in float32", f) + } + v.SetFloat(f) + return v, nil + case reflect.Interface: + return boxInto(v, reflect.ValueOf(f)) + default: + } + if ok, err := tryTextUnmarshaler(v, value.Data); ok { + return v, err + } + return reflect.Value{}, d.typeMismatchError("float", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignBool(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + b := value.Data[0] == 't' + + switch v.Kind() { + case reflect.Bool: + v.SetBool(b) + return v, nil + case reflect.Interface: + return boxInto(v, reflect.ValueOf(b)) + default: + } + if ok, err := tryTextUnmarshaler(v, value.Data); ok { + return v, err + } + return reflect.Value{}, d.typeMismatchError("boolean", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignDateTime(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + t, err := parseDateTime(value.Data) + if err != nil { + return reflect.Value{}, err + } + + if v.Type() == timeType { + v.Set(reflect.ValueOf(t)) + return v, nil + } + if v.Kind() == reflect.Interface { + return boxInto(v, reflect.ValueOf(t)) + } + return reflect.Value{}, d.typeMismatchError("datetime", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignLocalDateTime(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + dt, rest, err := parseLocalDateTime(value.Data) + if err != nil { + return reflect.Value{}, err + } + if len(rest) > 0 { + return reflect.Value{}, unstable.NewParserError(rest, "extra characters at the end of a local date time") + } + + switch v.Type() { + case localDateTimeType: + v.Set(reflect.ValueOf(dt)) + return v, nil + case timeType: + v.Set(reflect.ValueOf(dt.AsTime(time.Local))) + return v, nil + } + if v.Kind() == reflect.Interface { + return boxInto(v, reflect.ValueOf(dt)) + } + return reflect.Value{}, d.typeMismatchError("local datetime", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignLocalDate(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + date, err := parseLocalDate(value.Data) + if err != nil { + return reflect.Value{}, err + } + + switch v.Type() { + case localDateType: + v.Set(reflect.ValueOf(date)) + return v, nil + case timeType: + v.Set(reflect.ValueOf(date.AsTime(time.Local))) + return v, nil + } + if v.Kind() == reflect.Interface { + return boxInto(v, reflect.ValueOf(date)) + } + return reflect.Value{}, d.typeMismatchError("local date", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignLocalTime(v reflect.Value, value *unstable.Node) (reflect.Value, error) { + t, rest, err := parseLocalTime(value.Data) + if err != nil { + return reflect.Value{}, err + } + if len(rest) > 0 { + return reflect.Value{}, unstable.NewParserError(rest, "extra characters at the end of a local time") + } + + switch v.Type() { + case localTimeType: + v.Set(reflect.ValueOf(t)) + return v, nil + case timeType: + v.Set(reflect.ValueOf(time.Date(0, 1, 1, t.Hour, t.Minute, t.Second, t.Nanosecond, time.Local))) + return v, nil + } + if v.Kind() == reflect.Interface { + return boxInto(v, reflect.ValueOf(t)) + } + return reflect.Value{}, d.typeMismatchError("local time", v.Type(), d.p.Raw(value.Raw)) +} + +func (d *decoder) assignArray(v reflect.Value, expr *unstable.Node, value *unstable.Node) (reflect.Value, error) { + // Count the elements to allocate the target in one go. + count := 0 + cit := value.Children() + for cit.Next() { + if cit.Node().Kind != unstable.Comment { + count++ + } + } + + switch v.Kind() { + case reflect.Slice: + // Allocate the backing array once at its final length and assign each + // element in place. This avoids a reflect.New allocation per element + // and the repeated growth checks of reflect.Append. + slice := reflect.MakeSlice(v.Type(), count, count) + i := 0 + it := value.Children() + for it.Next() { + n := it.Node() + if n.Kind == unstable.Comment { + continue + } + elem := slice.Index(i) + nv, err := d.assignValue(elem, nil, n) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + elem.Set(nv) + } + i++ + } + return slice, nil + case reflect.Array: + it := value.Children() + i := 0 + for it.Next() { + n := it.Node() + if n.Kind == unstable.Comment { + continue + } + if i >= v.Len() { + // Extra elements are dropped when the target array is too + // small. + break + } + elem := v.Index(i) + nv, err := d.assignValue(elem, nil, n) + if err != nil { + return reflect.Value{}, err + } + elem.Set(nv) + i++ + } + return v, nil + case reflect.Interface: + // Build the []interface{} natively: each element is decoded straight + // into a Go value with no intermediate addressable reflect.Value and + // no reflect round-trip, and nested arrays recurse the same way. + slice := make([]interface{}, 0, count) + it := value.Children() + for it.Next() { + n := it.Node() + if n.Kind == unstable.Comment { + continue + } + ev, err := d.decodeAny(n) + if err != nil { + return reflect.Value{}, err + } + slice = append(slice, ev) + } + return boxInto(v, reflect.ValueOf(slice)) + default: + } + return reflect.Value{}, d.typeMismatchError("array", v.Type(), d.rawValue(expr, value)) +} + +// decodeAny decodes a value node into a native Go value (the representation +// used for interface{} targets), without going through reflect. Scalars and +// arrays are handled directly; inline tables still defer to the reflect-based +// path so that their dotted-key merge semantics remain identical. +func (d *decoder) decodeAny(n *unstable.Node) (interface{}, error) { + switch n.Kind { + case unstable.String: + return string(n.Data), nil + case unstable.Integer: + i, err := parseInteger(n.Data) + return i, err + case unstable.Float: + f, err := parseFloat(n.Data) + return f, err + case unstable.Bool: + return n.Data[0] == 't', nil + case unstable.Array: + count := 0 + cit := n.Children() + for cit.Next() { + if cit.Node().Kind != unstable.Comment { + count++ + } + } + slice := make([]interface{}, 0, count) + it := n.Children() + for it.Next() { + c := it.Node() + if c.Kind == unstable.Comment { + continue + } + ev, err := d.decodeAny(c) + if err != nil { + return nil, err + } + slice = append(slice, ev) + } + return slice, nil + case unstable.InlineTable: + // Build the map natively: navigate each (possibly dotted) key with + // plain Go map operations and decode each value with decodeAny. The + // seen-tracker has already rejected duplicate or conflicting keys, so + // intermediate parts can be created/merged without revalidation. + count := 0 + cit := n.Children() + for cit.Next() { + count++ + } + m := make(map[string]interface{}, count) + it := n.Children() + for it.Next() { + kv := it.Node() + if err := d.setAnyKey(m, kv.Key(), kv.Value()); err != nil { + return nil, err + } + } + return m, nil + case unstable.DateTime: + t, err := parseDateTime(n.Data) + return t, err + case unstable.LocalDateTime: + dt, rest, err := parseLocalDateTime(n.Data) + if err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, unstable.NewParserError(rest, "extra characters at the end of a local date time") + } + return dt, nil + case unstable.LocalDate: + date, err := parseLocalDate(n.Data) + return date, err + case unstable.LocalTime: + t, rest, err := parseLocalTime(n.Data) + if err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, unstable.NewParserError(rest, "extra characters at the end of a local time") + } + return t, nil + default: + return nil, unstable.NewParserError(n.Data, "unsupported value kind %s", n.Kind) + } +} + +func (d *decoder) assignInlineTable(v reflect.Value, expr *unstable.Node, value *unstable.Node) (reflect.Value, error) { + switch v.Kind() { + case reflect.Map: + // Inline tables are self-contained: they fully replace the target. + v = reflect.MakeMap(v.Type()) + case reflect.Struct: + // fields are set in place + case reflect.Interface: + elem := reflect.ValueOf(map[string]interface{}{}) + nv, err := d.assignInlineTable(elem, expr, value) + if err != nil { + return reflect.Value{}, err + } + return boxInto(v, nv) + default: + return reflect.Value{}, d.typeMismatchError("inline table", v.Type(), d.rawValue(expr, value)) + } + + it := value.Children() + for it.Next() { + kv := it.Node() + // Build the path from the key of this key-value. Keys of inline + // tables rarely have more than a few parts. + var pathBuf [4]pathPart + path := pathBuf[:0] + kit := kv.Key() + for kit.Next() { + path = append(path, pathPart{node: kit.Node()}) + } + nv, err := d.descend(v, path, 0, kv, kv.Value()) + if err != nil { + return reflect.Value{}, err + } + if nv.IsValid() { + v = nv + } + } + return v, nil +} + +// boxInto returns the value to store in place of the interface value v. The +// caller stores the result in the slot v was found in, which performs the +// interface conversion, so the concrete value can be returned as-is. +func boxInto(v reflect.Value, c reflect.Value) (reflect.Value, error) { + if !c.Type().AssignableTo(v.Type()) { + return reflect.Value{}, fmt.Errorf("toml: cannot store %s into %s", c.Type(), v.Type()) + } + return c, nil +} + +var ( + interfaceType = reflect.TypeOf(new(interface{})).Elem() + localDateType = reflect.TypeOf(LocalDate{}) + localTimeType = reflect.TypeOf(LocalTime{}) + localDateTimeType = reflect.TypeOf(LocalDateTime{}) +) + +// structPlan caches the mapping between TOML keys and the fields of a struct +// type. byFold, keyed by the lowercased name, resolves any key on its own when +// no two fields fold to the same name (the overwhelmingly common case, marked +// by hasCollision == false): TOML keys are usually lowercase and never match +// the exact (capitalized) Go field names, so the byName probe was always a +// wasted lookup. byName (the exact names) is only consulted, first, when +// fields do collide under folding, to preserve the exact-match-wins tiebreak. +type structPlan struct { + byName map[string]structField + byFold map[string]structField + hasCollision bool +} + +type structField struct { + index []int + fieldName string +} + +// foldBufSize bounds the stack buffer used to lowercase keys without +// allocating. Keys longer than this (extremely rare) take the strings.ToLower +// fallback. +const foldBufSize = 68 + +// lookup and lookupBytes keep the hot path to a single inlinable byFold lookup. +// byFold is indexed by both the exact field/tag names and their lowercased +// forms, so that lookup resolves the two common cases — a lowercase key, or a +// key matching the field's own casing — directly. byName is consulted first +// only for types whose fields collide under case-folding, to preserve the +// exact-match-wins tiebreak. The buffer-fold for other casings lives +// out-of-line so it does not bloat the hot path. +func (p *structPlan) lookup(name string) (structField, bool) { + if p.hasCollision { + if f, ok := p.byName[name]; ok { + return f, true + } + } + if f, ok := p.byFold[name]; ok { + return f, true + } + return p.lookupFoldStr(name) +} + +func (p *structPlan) lookupBytes(name []byte) (structField, bool) { + if p.hasCollision { + if f, ok := p.byName[string(name)]; ok { // does not allocate + return f, true + } + } + if f, ok := p.byFold[string(name)]; ok { // does not allocate + return f, true + } + return p.lookupFold(name) +} + +// lookupFold resolves keys whose casing matches neither the exact nor the +// lowercased index: it folds to lowercase (in a stack buffer for ASCII, so no +// allocation) and retries; only non-ASCII or oversized keys hit strings.ToLower. +func (p *structPlan) lookupFold(name []byte) (structField, bool) { + if len(name) <= foldBufSize { + // Fold into a stack buffer: len(name) <= cap(buf), so the append + // never reallocates and nothing escapes to the heap. + var buf [foldBufSize]byte + b := buf[:0] + ascii := true + for _, c := range name { + if c >= 0x80 { + ascii = false + break + } + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + b = append(b, c) + } + if ascii { + f, ok := p.byFold[string(b)] // does not allocate + return f, ok + } + } + f, ok := p.byFold[strings.ToLower(string(name))] + return f, ok +} + +func (p *structPlan) lookupFoldStr(name string) (structField, bool) { + if len(name) <= foldBufSize { + // Fold into a stack buffer: len(name) <= cap(buf), so the append + // never reallocates and nothing escapes to the heap. + var buf [foldBufSize]byte + b := buf[:0] + ascii := true + for i := 0; i < len(name); i++ { + c := name[i] + if c >= 0x80 { + ascii = false + break + } + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + b = append(b, c) + } + if ascii { + f, ok := p.byFold[string(b)] // does not allocate + return f, ok + } + } + f, ok := p.byFold[strings.ToLower(name)] + return f, ok +} + +var structPlans sync.Map // reflect.Type -> *structPlan + +func planForType(t reflect.Type) *structPlan { + if plan, ok := structPlans.Load(t); ok { + return plan.(*structPlan) + } + plan := buildPlan(t) + structPlans.Store(t, plan) + return plan +} + +func buildPlan(t reflect.Type) *structPlan { + plan := &structPlan{ + byName: map[string]structField{}, + byFold: map[string]structField{}, + } + addFields(plan, t, nil) + return plan +} + +func addFields(plan *structPlan, t reflect.Type, prefix []int) { + var embedded []reflect.StructField + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + tag, tagged := f.Tag.Lookup("toml") + name := f.Name + if tagged { + // A tag of exactly "-" drops the field. "-," names it "-". + if tag == "-" { + continue + } + parts := strings.SplitN(tag, ",", 2) + if parts[0] != "" { + name = parts[0] + } + } + if f.Anonymous { + ft := f.Type + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + if ft.Kind() != reflect.Struct { + // Embedded non-struct fields are not decoded into. + continue + } + if !tagged { + // Untagged embedded structs are flattened, even when their + // type is unexported: only their own exported fields are + // reachable. + embedded = append(embedded, f) + continue + } + // A tagged embedded struct acts as a regular named field. + } else if f.PkgPath != "" { + // unexported + continue + } + index := make([]int, 0, len(prefix)+1) + index = append(index, prefix...) + index = append(index, i) + sf := structField{index: index, fieldName: f.Name} + if _, ok := plan.byName[name]; !ok { + plan.byName[name] = sf + } + lower := strings.ToLower(name) + if _, ok := plan.byFold[lower]; !ok { + plan.byFold[lower] = sf + } else { + // Two distinct fields fold to the same name: case-insensitive + // matching is ambiguous, so lookups must consult byName first to + // keep the exact-match-wins tiebreak deterministic. + plan.hasCollision = true + } + // Index the exact (cased) name as well, so a key written with the + // field's own casing resolves in a single byFold lookup. Only fields + // whose name is not already lowercase need this extra entry. Any name + // that would conflict here also collides under folding (handled + // above), so byName-first preserves the exact tiebreak in that case. + if name != lower { + if _, ok := plan.byFold[name]; !ok { + plan.byFold[name] = sf + } + } + } + // Embedded structs are flattened after the regular fields, so that + // shallower fields win. + for _, f := range embedded { + ft := f.Type + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + index := make([]int, 0, len(prefix)+1) + index = append(index, prefix...) + idx := f.Index[0] + index = append(index, idx) + addFields(plan, ft, index) + } +} + +// fieldByIndexAlloc returns the field of v at the given index path, +// allocating intermediate embedded pointers as needed. +func fieldByIndexAlloc(v reflect.Value, index []int) reflect.Value { + // Fast path for non-embedded fields, which have a single-element index: + // no intermediate pointer dereferencing is possible. + if len(index) == 1 { + return v.Field(index[0]) + } + for i, x := range index { + if i > 0 { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + } + v = v.Field(x) + } + return v +} diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go b/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go new file mode 100644 index 000000000..2cf3bdb6d --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/ast.go @@ -0,0 +1,124 @@ +package unstable + +import ( + "errors" + "fmt" +) + +// Iterator over a sequence of nodes. +// +// Starts uninitialized, you need to call Next() first. +// +// For example: +// +// it := n.Children() +// for it.Next() { +// n := it.Node() +// // do something with n +// } +type Iterator struct { + started bool + node *Node +} + +// Next moves the iterator forward and returns true if points to a node, false +// otherwise. +func (c *Iterator) Next() bool { + if !c.started { + c.started = true + } else if c.node.Valid() { + c.node = c.node.Next() + } + return c.node.Valid() +} + +// IsLast returns true if the current node of the iterator is the last +// one. Subsequent calls to Next() will return false. +func (c *Iterator) IsLast() bool { + return c.node.next == 0 +} + +// Node returns a pointer to the node pointed at by the iterator. +func (c *Iterator) Node() *Node { + return c.node +} + +// Node in a TOML expression AST. +// +// Depending on Kind, its sequence of children should be interpreted +// differently. +// +// - Array have one child per element in the array. +// - InlineTable have one child per key-value in the table (each of kind +// InlineTable). +// - KeyValue have at least two children. The first one is the value. The +// rest make a potentially dotted key. +// - Table and ArrayTable's children represent a dotted key (same as +// KeyValue, but without the first node being the value). +// +// When relevant, Raw describes the range of bytes this node is referring to in +// the input document. Use Parser.Raw() to retrieve the actual bytes. +type Node struct { + Kind Kind + Raw Range // Raw bytes from the input. + Data []byte // Node value (either allocated or referencing the input). + + // References to other nodes, as 1-based indexes into the parser's arena. + // 0 means no node. + parser *Parser + next int32 + child int32 +} + +// Next returns a pointer to the next node, or nil if there is no next node. +func (n *Node) Next() *Node { + if n.next == 0 { + return nil + } + return &n.parser.nodes[n.next-1] +} + +// Child returns a pointer to the first child node of this node. Other children +// can be accessed calling Next on the first child. Returns nil if there is no +// child node. +func (n *Node) Child() *Node { + if n.child == 0 { + return nil + } + return &n.parser.nodes[n.child-1] +} + +// Valid returns true if the node's kind is set (not to Invalid). +func (n *Node) Valid() bool { + return n != nil && n.Kind != Invalid +} + +// Key returns the children nodes making the Key on a supported node. Panics +// otherwise. They are guaranteed to be all be of the Kind Key. A simple key +// would return just one element. +func (n *Node) Key() Iterator { + switch n.Kind { + case KeyValue: + value := n.Child() + if !value.Valid() { + panic(errors.New("KeyValue should have at least two children")) + } + return Iterator{node: value.Next()} + case Table, ArrayTable: + return Iterator{node: n.Child()} + default: + panic(fmt.Errorf("Key() is not supported on a %s", n.Kind)) + } +} + +// Value returns a pointer to the value node of a KeyValue. +// Guaranteed to be non-nil. Panics if not called on a KeyValue node, +// or if the Children are malformed. +func (n *Node) Value() *Node { + return n.Child() +} + +// Children returns an iterator over a node's children. +func (n *Node) Children() Iterator { + return Iterator{node: n.Child()} +} diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/bridge.go b/vendor/github.com/pelletier/go-toml/v2/unstable/bridge.go new file mode 100644 index 000000000..5482bde0b --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/bridge.go @@ -0,0 +1,21 @@ +package unstable + +import "github.com/pelletier/go-toml/v2/internal/parserbridge" + +// Expose the non-AST scanners to the root toml package without committing to +// them in the public API. See internal/parserbridge for the rationale. +// +//nolint:gochecknoinits // load-time wiring of an internal bridge (see internal/parserbridge) +func init() { + parserbridge.ScanScalar = func(p any, b []byte) (kind int, raw, value, rest []byte, err error) { + k, raw, value, rest, err := p.(*Parser).scanScalar(b) + return int(k), raw, value, rest, err + } + parserbridge.ScanKey = func(p any, b []byte, dst [][]byte) (parts [][]byte, raw, rest []byte, err error) { + return p.(*Parser).scanKey(b, dst) + } + parserbridge.ScanComment = scanComment + parserbridge.ParseValue = func(p any, b []byte) (node any, rest []byte, err error) { + return p.(*Parser).parseValue(b) + } +} diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/doc.go b/vendor/github.com/pelletier/go-toml/v2/unstable/doc.go new file mode 100644 index 000000000..7ff26c53c --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/doc.go @@ -0,0 +1,3 @@ +// Package unstable provides APIs that do not meet the backward compatibility +// guarantees yet. +package unstable diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go b/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go new file mode 100644 index 000000000..54b19f9dd --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/kind.go @@ -0,0 +1,83 @@ +package unstable + +import "fmt" + +// Kind represents the type of TOML structure contained in a given Node. +type Kind int + +const ( + // Invalid represents an invalid meta node. + Invalid Kind = iota + // Comment represents a comment meta node. + Comment + // Key represents a key meta node. + Key + + // Table represents a top-level table. + Table + // ArrayTable represents a top-level array table. + ArrayTable + // KeyValue represents a top-level key value. + KeyValue + + // Array represents an array container value. + Array + // InlineTable represents an inline table container value. + InlineTable + + // String represents a string value. + String + // Bool represents a boolean value. + Bool + // Float represents a floating point value. + Float + // Integer represents an integer value. + Integer + // LocalDate represents a a local date value. + LocalDate + // LocalTime represents a local time value. + LocalTime + // LocalDateTime represents a local date/time value. + LocalDateTime + // DateTime represents a data/time value. + DateTime +) + +// String implementation of fmt.Stringer. +func (k Kind) String() string { + switch k { + case Invalid: + return "Invalid" + case Comment: + return "Comment" + case Key: + return "Key" + case Table: + return "Table" + case ArrayTable: + return "ArrayTable" + case KeyValue: + return "KeyValue" + case Array: + return "Array" + case InlineTable: + return "InlineTable" + case String: + return "String" + case Bool: + return "Bool" + case Float: + return "Float" + case Integer: + return "Integer" + case LocalDate: + return "LocalDate" + case LocalTime: + return "LocalTime" + case LocalDateTime: + return "LocalDateTime" + case DateTime: + return "DateTime" + } + panic(fmt.Errorf("Kind.String() not implemented for kind %d", int(k))) +} diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go b/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go new file mode 100644 index 000000000..91e226998 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/parser.go @@ -0,0 +1,1658 @@ +package unstable + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "unicode/utf8" +) + +// ParserError describes an error relative to the content of the document. +// +// It cannot outlive the instance of Parser it refers to, and may cause panics +// if the parser is reset. +type ParserError struct { + Highlight []byte + Message string + Key []string // optional +} + +// Error is the implementation of the error interface. +func (e *ParserError) Error() string { + return e.Message +} + +// NewParserError is a convenience function to create a ParserError +// +// Warning: Highlight needs to be a subslice of Parser.data, so only slices +// returned by Parser.Raw are valid candidates. +func NewParserError(highlight []byte, format string, args ...interface{}) error { + return &ParserError{ + Highlight: highlight, + Message: fmt.Errorf(format, args...).Error(), + } +} + +// Parser scans over a TOML-encoded document and generates an iterative AST. +// +// To prime the Parser, first reset it with the contents of a TOML document. +// Then, process all top-level expressions sequentially. See Example. +// +// Don't forget to check Error() after you're done parsing. +// +// Each top-level expression needs to be fully processed before calling +// NextExpression() again. Otherwise, calls to various Node methods may panic +// if the parser has moved on the next expression. +// +// For performance reasons, go-toml doesn't make a copy of the input bytes to +// the parser. Make sure to copy all the bytes you need to outlive the slice +// given to the parser. +type Parser struct { + KeepComments bool + + data []byte + left []byte + nodes []Node + err error +} + +// Data returns the slice provided to the last call to Reset. +func (p *Parser) Data() []byte { + return p.data +} + +// Range returns a range description that corresponds to a given slice of the +// input. If the argument is not a subslice of the parser input, this function +// panics. +func (p *Parser) Range(b []byte) Range { + // b is a subslice of p.data if and only if they share the same backing + // array. In that case, because subslicing cannot extend capacity, the + // number of bytes between the start of b and the end of the backing array + // (its capacity) identifies the offset of b within data. + offset := cap(p.data) - cap(b) + if offset < 0 || offset+len(b) > len(p.data) { + panic(errors.New("not a slice of the data slice")) + } + return Range{ + Offset: uint32(offset), //nolint:gosec // TOML documents are small + Length: uint32(len(b)), //nolint:gosec // TOML documents are small + } +} + +// Raw returns the slice corresponding to the bytes in the given range. +func (p *Parser) Raw(raw Range) []byte { + return p.data[raw.Offset : raw.Offset+raw.Length] +} + +// Reset brings the parser to its initial state for a given input. It wipes an +// reuses internal storage to reduce allocation. +func (p *Parser) Reset(b []byte) { + p.data = b + p.left = b + p.nodes = p.nodes[:0] + p.err = nil +} + +// Error returns any error that has occurred during parsing. +func (p *Parser) Error() error { + return p.err +} + +// Range of bytes in the document. +type Range struct { + Offset uint32 + Length uint32 +} + +// Position describes a position in the input. +type Position struct { + // Number of bytes from the beginning of the input. + Offset int + // Line number, starting at 1. + Line int + // Column number, starting at 1. + Column int +} + +// Shape describes the position of a range in the input. +type Shape struct { + Start Position + End Position +} + +func (p *Parser) position(offset int) Position { + pos := Position{ + Offset: offset, + Line: 1, + Column: 1, + } + b := p.data[:offset] + for { + idx := bytes.IndexByte(b, '\n') + if idx < 0 { + break + } + pos.Line++ + b = b[idx+1:] + } + pos.Column = len(b) + 1 + return pos +} + +// Shape returns the shape of the given range in the input. Will +// panic if the range is not a subslice of the input. +func (p *Parser) Shape(r Range) Shape { + raw := p.Raw(r) + return Shape{ + Start: p.position(int(r.Offset)), + End: p.position(int(r.Offset) + len(raw)), + } +} + +// Expression returns a pointer to the node representing the last successfully +// parsed expression. +func (p *Parser) Expression() *Node { + if len(p.nodes) == 0 { + return nil + } + return &p.nodes[0] +} + +// push appends a node to the arena and returns its handle (1-based index). +func (p *Parser) push(n Node) int32 { + if len(p.nodes) == cap(p.nodes) { + // Grow by 2x: large expressions (huge arrays) would otherwise grow + // the arena in small steps, copying it repeatedly. + newCap := 2 * cap(p.nodes) + if newCap < 64 { + newCap = 64 + } + nodes := make([]Node, len(p.nodes), newCap) + copy(nodes, p.nodes) + p.nodes = nodes + } + n.parser = p + p.nodes = append(p.nodes, n) + return int32(len(p.nodes)) //nolint:gosec // node counts are bounded by document size +} + +// at returns a pointer to the node with the given handle. Only valid until +// the next call to push. +func (p *Parser) at(handle int32) *Node { + return &p.nodes[handle-1] +} + +// offsetOf returns the offset of b within the parser's data. b must be a +// subslice of p.data. +func (p *Parser) offsetOf(b []byte) int { + return cap(p.data) - cap(b) +} + +// rangeFrom returns the Range covering bytes from the start of `from` to the +// start of `to`. Both must be subslices of p.data. +func (p *Parser) rangeFrom(from, to []byte) Range { + start := p.offsetOf(from) + end := p.offsetOf(to) + return Range{ + Offset: uint32(start), //nolint:gosec // TOML documents are small + Length: uint32(end - start), //nolint:gosec // TOML documents are small + } +} + +// NextExpression parses the next top-level expression. If an expression was +// successfully parsed, it returns true. If the parser is at the end of the +// document or an error occurred, it returns false. +// +// Retrieve the parsed expression with Expression(). +func (p *Parser) NextExpression() bool { + if p.err != nil { + return false + } + + p.nodes = p.nodes[:0] + + for { + b := skipWhitespace(p.left) + if len(b) == 0 { + p.left = b + return false + } + + var err error + switch b[0] { + case '\n': + p.left = b[1:] + continue + case '\r': + if len(b) > 1 && b[1] == '\n' { + p.left = b[2:] + continue + } + err = NewParserError(b[:1], "expected newline but got %#U", b[0]) + case '#': + var comment, rest []byte + comment, rest, err = scanComment(b) + if err == nil { + rest, err = consumeEOL(rest) + } + if err == nil { + if p.KeepComments { + p.push(Node{ + Kind: Comment, + Raw: p.Range(comment), + Data: comment, + }) + p.left = rest + return true + } + p.left = rest + continue + } + case '[': + var rest []byte + rest, err = p.parseExprTable(b) + if err == nil { + p.left = rest + return true + } + default: + var rest []byte + rest, err = p.parseExprKeyval(b) + if err == nil { + p.left = rest + return true + } + } + + // Errors at the end of the input have an empty highlight. Extend + // them to the last byte of the input so that they carry a usable + // position. + var perr *ParserError + if errors.As(err, &perr) && len(perr.Highlight) == 0 { + if offset := p.offsetOf(perr.Highlight); offset > 0 && offset == len(p.data) { + perr.Highlight = p.data[offset-1 : offset] + } + } + + p.err = err + return false + } +} + +// consumeEOL consumes a newline (LF or CRLF) or end of input. +func consumeEOL(b []byte) ([]byte, error) { + if len(b) == 0 { + return b, nil + } + switch b[0] { + case '\n': + return b[1:], nil + case '\r': + if len(b) > 1 && b[1] == '\n' { + return b[2:], nil + } + } + return nil, NewParserError(b[:1], "expected newline but got %#U", b[0]) +} + +// finishLine handles `ws [comment] (newline|eof)` after a top-level +// expression. If a comment is present and KeepComments is set, it is attached +// as the next sibling of the expression's root node. +func (p *Parser) finishLine(root int32, b []byte) ([]byte, error) { + b = skipWhitespace(b) + if len(b) > 0 && b[0] == '#' { + comment, rest, err := scanComment(b) + if err != nil { + return nil, err + } + if p.KeepComments { + h := p.push(Node{ + Kind: Comment, + Raw: p.Range(comment), + Data: comment, + }) + p.at(root).next = h + } + b = rest + } + return consumeEOL(b) +} + +// parseExprKeyval parses a top-level `key = value` expression, including its +// line termination. +func (p *Parser) parseExprKeyval(b []byte) ([]byte, error) { + root, rest, err := p.parseKeyval(b) + if err != nil { + return nil, err + } + return p.finishLine(root, rest) +} + +// parseExprTable parses a `[table]` or `[[array table]]` expression, +// including its line termination. b starts at '['. +func (p *Parser) parseExprTable(b []byte) ([]byte, error) { + var root int32 + var err error + var rest []byte + if len(b) > 1 && b[1] == '[' { + root, rest, err = p.parseArrayTableHeader(b) + } else { + root, rest, err = p.parseTableHeader(b) + } + if err != nil { + return nil, err + } + return p.finishLine(root, rest) +} + +// parseTableHeader parses `[ ws key ws ]`. b starts at '['. +func (p *Parser) parseTableHeader(b []byte) (int32, []byte, error) { + root := p.push(Node{Kind: Table}) + + first, b, err := p.parseKey(skipWhitespace(b[1:])) + if err != nil { + return 0, nil, err + } + p.at(root).child = first + + if len(b) == 0 || b[0] != ']' { + return 0, nil, NewParserError(highlight1(b), "expected ']' to close table name") + } + return root, b[1:], nil +} + +// parseArrayTableHeader parses `[[ ws key ws ]]`. b starts at '[['. +func (p *Parser) parseArrayTableHeader(b []byte) (int32, []byte, error) { + root := p.push(Node{Kind: ArrayTable}) + + first, b, err := p.parseKey(skipWhitespace(b[2:])) + if err != nil { + return 0, nil, err + } + p.at(root).child = first + + if len(b) < 2 || b[0] != ']' || b[1] != ']' { + return 0, nil, NewParserError(highlight1(b), "expected ']]' to close array table name") + } + return root, b[2:], nil +} + +// parseKeyval parses `key keyval-sep val`. Returns the handle to the KeyValue +// node. +func (p *Parser) parseKeyval(b []byte) (int32, []byte, error) { + root := p.push(Node{Kind: KeyValue}) + start := b + + firstKey, b, err := p.parseKey(b) + if err != nil { + return 0, nil, err + } + + if len(b) == 0 || b[0] != '=' { + return 0, nil, NewParserError(highlight1(b), "expected '=' after key") + } + b = skipWhitespace(b[1:]) + + value, b, err := p.parseVal(b) + if err != nil { + return 0, nil, err + } + + p.at(root).child = value + p.at(value).next = firstKey + p.at(root).Raw = p.rangeFrom(start, b) + return root, b, nil +} + +// parseKey parses a potentially dotted key. It consumes the whitespace +// following the key, so that the caller can directly check for the next +// expected character ('=', ']', ...). Returns the handle of the first Key +// node; subsequent parts are chained via next. +func (p *Parser) parseKey(b []byte) (int32, []byte, error) { + var first, last int32 + for { + h, rest, err := p.parseSimpleKey(b) + if err != nil { + return 0, nil, err + } + if first == 0 { + first = h + } else { + p.at(last).next = h + } + last = h + + b = skipWhitespace(rest) + if len(b) > 0 && b[0] == '.' { + b = skipWhitespace(b[1:]) + continue + } + return first, b, nil + } +} + +func isUnquotedKeyChar(c byte) bool { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' +} + +// parseSimpleKey parses one key part: either a bare key or a quoted key. +func (p *Parser) parseSimpleKey(b []byte) (int32, []byte, error) { + raw, value, rest, err := p.scanSimpleKey(b) + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: Key, Raw: p.Range(raw), Data: value}) + return h, rest, nil +} + +// scanSimpleKey scans one key part (bare or quoted) without building an AST +// node. It returns the raw bytes, the decoded key value, and the rest of the +// input. +func (p *Parser) scanSimpleKey(b []byte) (raw, value, rest []byte, err error) { + if len(b) == 0 { + return nil, nil, nil, NewParserError(b, "expected key but reached end of input") + } + + switch b[0] { + case '\'': + return p.parseLiteralString(b) + case '"': + return p.parseBasicString(b) + default: + i := 0 + for i < len(b) && isUnquotedKeyChar(b[i]) { + i++ + } + if i == 0 { + return nil, nil, nil, NewParserError(b[:1], "invalid character at start of key: %#U", b[0]) + } + return b[:i], b[:i], b[i:], nil + } +} + +// parseVal parses a TOML value and returns the handle to its node. +func (p *Parser) parseVal(b []byte) (int32, []byte, error) { + if len(b) == 0 { + return 0, nil, NewParserError(b, "expected value, not end of input") + } + + c := b[0] + switch { + case c == '"': + var raw, value, rest []byte + var err error + if len(b) > 2 && b[1] == '"' && b[2] == '"' { + raw, value, rest, err = p.parseMultilineBasicString(b) + } else { + raw, value, rest, err = p.parseBasicString(b) + } + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: String, Raw: p.Range(raw), Data: value}) + return h, rest, nil + case c == '\'': + var raw, value, rest []byte + var err error + if len(b) > 2 && b[1] == '\'' && b[2] == '\'' { + raw, value, rest, err = p.parseMultilineLiteralString(b) + } else { + raw, value, rest, err = p.parseLiteralString(b) + } + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: String, Raw: p.Range(raw), Data: value}) + return h, rest, nil + case c == 't': + return p.parseKeyword(b, "true", Bool) + case c == 'f': + return p.parseKeyword(b, "false", Bool) + case c == 'i': + return p.parseKeyword(b, "inf", Float) + case c == 'n': + return p.parseKeyword(b, "nan", Float) + case c == '[': + return p.parseValArray(b) + case c == '{': + return p.parseInlineTable(b) + case c == '+' || c == '-': + return p.parseIntOrFloat(b) + case c >= '0' && c <= '9': + if isDateTimeStart(b) { + return p.parseDateTime(b) + } + return p.parseIntOrFloat(b) + default: + return 0, nil, NewParserError(b[:1], "unexpected character %#U at start of value", c) + } +} + +// scanScalar scans a single scalar TOML value (string, integer, float, +// boolean, or date/time) without building any AST node. It returns the kind of +// the value, its raw bytes, its decoded value bytes (for strings: quotes +// removed and escapes resolved; identical to raw for the other kinds), and the +// rest of the input. Arrays and inline tables are not scalars and produce an +// error: use parseValue for those. +// +// It is exposed to the root toml package through internal/parserbridge for the +// fused generic-decode path; it is not part of the public API. +func (p *Parser) scanScalar(b []byte) (kind Kind, raw, value, rest []byte, err error) { + if len(b) == 0 { + return Invalid, nil, nil, nil, NewParserError(b, "expected value, not end of input") + } + + c := b[0] + switch { + case c == '"': + if len(b) > 2 && b[1] == '"' && b[2] == '"' { + raw, value, rest, err = p.parseMultilineBasicString(b) + } else { + raw, value, rest, err = p.parseBasicString(b) + } + return String, raw, value, rest, err + case c == '\'': + if len(b) > 2 && b[1] == '\'' && b[2] == '\'' { + raw, value, rest, err = p.parseMultilineLiteralString(b) + } else { + raw, value, rest, err = p.parseLiteralString(b) + } + return String, raw, value, rest, err + case c == 't': + return scanKeyword(b, "true", Bool) + case c == 'f': + return scanKeyword(b, "false", Bool) + case c == 'i': + return scanKeyword(b, "inf", Float) + case c == 'n': + return scanKeyword(b, "nan", Float) + case c == '+' || c == '-': + return scanIntOrFloat(b) + case c >= '0' && c <= '9': + if isDateTimeStart(b) { + return scanDateTime(b) + } + return scanIntOrFloat(b) + default: + return Invalid, nil, nil, nil, NewParserError(b[:1], "unexpected character %#U at start of value", c) + } +} + +// scanKey scans a potentially dotted key without building AST nodes, +// appending the decoded value of each part to dst (pass dst[:0] to reuse a +// buffer). It consumes the whitespace following the key, so the caller can +// directly check for the next expected character ('=', ']', ...). It returns +// the parts, the raw bytes spanning the whole key (from the first part to the +// end of the last one, excluding trailing whitespace, usable as an error +// highlight), the rest of the input, and any error. +// +// It is exposed to the root toml package through internal/parserbridge for the +// fused generic-decode path; it is not part of the public API. +func (p *Parser) scanKey(b []byte, dst [][]byte) (parts [][]byte, raw, rest []byte, err error) { + parts = dst + start := b + for { + _, value, r, err := p.scanSimpleKey(b) + if err != nil { + return nil, nil, nil, err + } + parts = append(parts, value) + + // r points just past the current part: the key spans from start to + // here, ignoring any whitespace that follows. + raw = start[:len(start)-len(r)] + + b = skipWhitespace(r) + if len(b) > 0 && b[0] == '.' { + b = skipWhitespace(b[1:]) + continue + } + return parts, raw, b, nil + } +} + +// parseValue parses a single TOML value, which may be an array or inline table, +// into the parser's arena. It returns the root node of the value and the rest +// of the input. It resets the arena, so any node returned by a previous call to +// parseValue, Expression, or NextExpression is invalidated. +// +// It is exposed to the root toml package through internal/parserbridge for the +// fused generic-decode path; it is not part of the public API. +func (p *Parser) parseValue(b []byte) (*Node, []byte, error) { + p.nodes = p.nodes[:0] + h, rest, err := p.parseVal(b) + if err != nil { + return nil, nil, err + } + return &p.nodes[h-1], rest, nil +} + +func (p *Parser) parseKeyword(b []byte, kw string, kind Kind) (int32, []byte, error) { + k, raw, _, rest, err := scanKeyword(b, kw, kind) + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: k, Raw: p.Range(raw), Data: raw}) + return h, rest, nil +} + +// scanKeyword scans a keyword value (true, false, inf, nan) without building +// an AST node. raw and value are identical (the keyword bytes). +func scanKeyword(b []byte, kw string, kind Kind) (Kind, []byte, []byte, []byte, error) { + if len(b) < len(kw) || string(b[:len(kw)]) != kw { + n := len(kw) + if len(b) < n { + n = len(b) + } + return Invalid, nil, nil, nil, NewParserError(b[:n], "expected keyword %q", kw) + } + return kind, b[:len(kw)], b[:len(kw)], b[len(kw):], nil +} + +// parseValArray parses an array value. b starts at '['. +func (p *Parser) parseValArray(b []byte) (int32, []byte, error) { + arr := p.push(Node{Kind: Array}) + b = b[1:] + + var lastChild int32 + appendChild := func(h int32) { + if lastChild == 0 { + p.at(arr).child = h + } else { + p.at(lastChild).next = h + } + lastChild = h + } + + // Comments inside the array are attached as follows: the first comment + // of a "run" (consecutive comments with no value in between) becomes a + // child of the array, interleaved with values; subsequent comments of the + // run are attached as children of the first one. + var runFirst, runLast int32 + + // afterValue is true when a value has been parsed and a comma (or the + // closing bracket) is expected before the next one. + afterValue := false + for { + b = skipWhitespace(b) + if len(b) == 0 { + return 0, nil, NewParserError(b, "array is incomplete") + } + + switch b[0] { + case ']': + return arr, b[1:], nil + case '\n': + b = b[1:] + continue + case '\r': + if len(b) > 1 && b[1] == '\n' { + b = b[2:] + continue + } + return 0, nil, NewParserError(b[:1], "expected newline but got %#U", b[0]) + case '#': + comment, rest, err := scanComment(b) + if err != nil { + return 0, nil, err + } + if p.KeepComments { + h := p.push(Node{Kind: Comment, Raw: p.Range(comment), Data: comment}) + switch { + case runFirst == 0: + appendChild(h) + runFirst = h + case runLast == runFirst: + p.at(runFirst).child = h + default: + p.at(runLast).next = h + } + runLast = h + } + b = rest + continue + case ',': + if !afterValue { + return 0, nil, NewParserError(b[:1], "expected value but got %#U", b[0]) + } + afterValue = false + b = b[1:] + continue + default: + if afterValue { + return 0, nil, NewParserError(b[:1], "expected ',' or ']' after array value") + } + h, rest, err := p.parseVal(b) + if err != nil { + return 0, nil, err + } + appendChild(h) + afterValue = true + runFirst, runLast = 0, 0 + b = rest + continue + } + } +} + +// parseInlineTable parses an inline table value. b starts at '{'. +// +// Per TOML v1.1.0, inline tables may span multiple lines (whitespace, +// comments and newlines are allowed between elements) and may contain a +// trailing comma. +func (p *Parser) parseInlineTable(b []byte) (int32, []byte, error) { + tbl := p.push(Node{Kind: InlineTable, Raw: p.Range(b[:1])}) + b = b[1:] + + var lastChild int32 + appendChild := func(h int32) { + if lastChild == 0 { + p.at(tbl).child = h + } else { + p.at(lastChild).next = h + } + lastChild = h + } + + // Comments are attached as in arrays: the first comment of a "run" + // (consecutive comments with no key-value in between) becomes a child of + // the table, interleaved with key-values; subsequent comments of the run + // hang off the first one. + var runFirst, runLast int32 + + // afterValue is true when a key-value has been parsed and a comma (or the + // closing brace) is expected before the next one. + afterValue := false + for { + b = skipWhitespace(b) + if len(b) == 0 { + return 0, nil, NewParserError(b, "inline table is incomplete") + } + + switch b[0] { + case '}': + return tbl, b[1:], nil + case '\n': + b = b[1:] + continue + case '\r': + if len(b) > 1 && b[1] == '\n' { + b = b[2:] + continue + } + return 0, nil, NewParserError(b[:1], "expected newline but got %#U", b[0]) + case '#': + comment, rest, err := scanComment(b) + if err != nil { + return 0, nil, err + } + if p.KeepComments { + h := p.push(Node{Kind: Comment, Raw: p.Range(comment), Data: comment}) + switch { + case runFirst == 0: + appendChild(h) + runFirst = h + case runLast == runFirst: + p.at(runFirst).child = h + default: + p.at(runLast).next = h + } + runLast = h + } + b = rest + continue + case ',': + if !afterValue { + return 0, nil, NewParserError(b[:1], "unexpected comma in inline table") + } + afterValue = false + b = b[1:] + continue + default: + if afterValue { + return 0, nil, NewParserError(b[:1], "expected ',' or '}' after inline table key-value") + } + h, rest, err := p.parseKeyval(b) + if err != nil { + return 0, nil, err + } + appendChild(h) + afterValue = true + runFirst, runLast = 0, 0 + b = rest + continue + } + } +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func isHexDigit(c byte) bool { + return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') +} + +// isDateTimeStart reports whether b looks like the start of a date or time +// value instead of a number. Values starting with two digits followed by a +// colon are times; values starting with four digits followed by a dash are +// dates. +func isDateTimeStart(b []byte) bool { + if len(b) >= 3 && isDigit(b[1]) && b[2] == ':' { + return true + } + if len(b) >= 5 && isDigit(b[1]) && isDigit(b[2]) && isDigit(b[3]) && b[4] == '-' { + return true + } + return false +} + +// expectDigits checks that the n first bytes of b are digits. +// parseDateTime parses date and/or time values. b starts with a digit. +// +// The parser is lenient: it scans the characters that can be part of a date +// and/or time value to delimit and classify the token, but leaves the +// validation of its contents to the document consumer. This keeps the value +// in one piece, so that errors about its content can point at the right +// place. +func (p *Parser) parseDateTime(b []byte) (int32, []byte, error) { + kind, raw, _, rest, err := scanDateTime(b) + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: kind, Raw: p.Range(raw), Data: raw}) + return h, rest, nil +} + +// scanDateTime classifies and delimits a date/time value without building an +// AST node. raw and value are identical (the token bytes). +func scanDateTime(b []byte) (Kind, []byte, []byte, []byte, error) { + // Greedily scan the characters that may compose a date/time value. A + // space is part of the value only when it serves as the delimiter + // between the date and the time, which is approximated by requiring a + // digit right after it. + i := 0 + delim := -1 + for i < len(b) { + c := b[i] + if isDigit(c) || c == ':' || c == '-' || c == '+' || c == '.' || c == 'Z' || c == 'z' { + i++ + continue + } + if c == 'T' || c == 't' || (c == ' ' && i+1 < len(b) && isDigit(b[i+1])) { + if delim < 0 { + delim = i + } + i++ + continue + } + break + } + tok := b[:i] + + var kind Kind + switch { + case tok[2] == ':': + kind = LocalTime + case delim < 0: + kind = LocalDate + case bytes.ContainsAny(tok[delim+1:], "Zz+-"): + kind = DateTime + default: + kind = LocalDateTime + } + + return kind, tok, tok, b[i:], nil +} + +// scanDigitsWithUnderscores scans a run of digits potentially separated by +// underscores. b starts right after the first digit of the run. isInRange +// selects the kind of digits. Returns the index after the run. +func scanDigitsWithUnderscores(b []byte, i int, isInRange func(byte) bool) (int, error) { + for i < len(b) { + c := b[i] + if isInRange(c) { + i++ + continue + } + if c == '_' { + if i+1 >= len(b) || !isInRange(b[i+1]) { + end := i + 2 + if end > len(b) { + end = len(b) + } + return 0, NewParserError(b[i:end], "number must have at least one digit between underscores") + } + i += 2 + continue + } + break + } + return i, nil +} + +// parseIntOrFloat parses integer and float values, including the special +// values inf and nan with an optional sign. +func (p *Parser) parseIntOrFloat(b []byte) (int32, []byte, error) { + kind, raw, _, rest, err := scanIntOrFloat(b) + if err != nil { + return 0, nil, err + } + h := p.push(Node{Kind: kind, Raw: p.Range(raw), Data: raw}) + return h, rest, nil +} + +// scanIntOrFloat delimits and classifies an integer or float value (including +// the special floats inf and nan with an optional sign) without building an +// AST node. raw and value are identical (the token bytes). +func scanIntOrFloat(b []byte) (Kind, []byte, []byte, []byte, error) { + i := 0 + if b[i] == '+' || b[i] == '-' { + i++ + } + if i >= len(b) { + return Invalid, nil, nil, nil, NewParserError(b, "expected number after sign") + } + + // special floats + if b[i] == 'i' || b[i] == 'n' { + kw := "inf" + if b[i] == 'n' { + kw = "nan" + } + if len(b) < i+3 || string(b[i:i+3]) != kw { + return Invalid, nil, nil, nil, NewParserError(b[i:i+1], "expected %q", kw) + } + i += 3 + return Float, b[:i], b[:i], b[i:], nil + } + + if !isDigit(b[i]) { + return Invalid, nil, nil, nil, NewParserError(b[i:i+1], "expected digit but got %#U", b[i]) + } + + // radix prefixes + if b[i] == '0' && i+1 < len(b) && (b[i+1] == 'x' || b[i+1] == 'o' || b[i+1] == 'b') { + if i != 0 { + return Invalid, nil, nil, nil, NewParserError(b[:2], "sign is not allowed on numbers with a radix prefix") + } + var isInRange func(byte) bool + switch b[1] { + case 'x': + isInRange = isHexDigit + case 'o': + isInRange = func(c byte) bool { return c >= '0' && c <= '7' } + case 'b': + isInRange = func(c byte) bool { return c == '0' || c == '1' } + } + i = 2 + if i >= len(b) || !isInRange(b[i]) { + return Invalid, nil, nil, nil, NewParserError(b[:2], "radix prefix must be followed by at least one digit") + } + i++ + var err error + i, err = scanDigitsWithUnderscores(b, i, isInRange) + if err != nil { + return Invalid, nil, nil, nil, err + } + return Integer, b[:i], b[:i], b[i:], nil + } + + // decimal integer part + leadingZero := b[i] == '0' + digitsStart := i + i++ + var err error + i, err = scanDigitsWithUnderscores(b, i, isDigit) + if err != nil { + return Invalid, nil, nil, nil, err + } + if leadingZero && i > digitsStart+1 { + return Invalid, nil, nil, nil, NewParserError(b[digitsStart:digitsStart+2], "integers cannot have leading zeroes") + } + + kind := Integer + + // fractional part + if i < len(b) && b[i] == '.' { + i++ + if i >= len(b) || !isDigit(b[i]) { + return Invalid, nil, nil, nil, NewParserError(highlight1(b[i:]), "decimal point must be followed by a digit") + } + i++ + i, err = scanDigitsWithUnderscores(b, i, isDigit) + if err != nil { + return Invalid, nil, nil, nil, err + } + kind = Float + } + + // exponent + if i < len(b) && (b[i] == 'e' || b[i] == 'E') { + i++ + if i < len(b) && (b[i] == '+' || b[i] == '-') { + i++ + } + if i >= len(b) || !isDigit(b[i]) { + return Invalid, nil, nil, nil, NewParserError(highlight1(b[i:]), "exponent must contain at least one digit") + } + i++ + i, err = scanDigitsWithUnderscores(b, i, isDigit) + if err != nil { + return Invalid, nil, nil, nil, err + } + kind = Float + } + + // A letter right after the number means it was meant to be a string that + // was left unquoted (e.g. "20s"). Report that instead of the misleading + // "expected newline" raised later (issue #413). + if i < len(b) { + if c := b[i]; (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + return Invalid, nil, nil, nil, NewParserError(b[i:i+1], "strings must be quoted") + } + } + + return kind, b[:i], b[:i], b[i:], nil +} + +// highlight1 returns a 1-byte highlight at the start of b, or b itself if it +// is empty. +func highlight1(b []byte) []byte { + if len(b) > 0 { + return b[:1] + } + return b +} + +func skipWhitespace(b []byte) []byte { + for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { + b = b[1:] + } + return b +} + +// Word-at-a-time byte scanning helpers. These detect, within an 8-byte word, +// the presence of bytes that need special handling, so that runs of plain +// ASCII characters can be skipped 8 bytes at a time. +const ( + lsb = 0x0101010101010101 + msb = 0x8080808080808080 +) + +// hasByteBelow reports whether any byte of the word x is strictly below n. +// Only meaningful when combined with a check that no byte has its high bit +// set. +func hasByteBelow(x uint64, n uint64) uint64 { + return (x - n*lsb) & ^x & msb +} + +// hasByteEqual reports whether any byte of the word x equals c. Only +// meaningful for bytes without their high bit set. +func hasByteEqual(x uint64, c uint64) uint64 { + y := x ^ (c * lsb) + return (y - lsb) & ^y & msb +} + +// scanComment parses a comment, starting at the '#' character. It returns the +// comment bytes (including '#', excluding the line ending) and the rest of +// the input. +func scanComment(b []byte) ([]byte, []byte, error) { + i := 1 + for i < len(b) { + // Fast path: skip 8 bytes at a time as long as they are all plain + // printable ASCII. + for i+8 <= len(b) { + x := binary.LittleEndian.Uint64(b[i:]) + if (x&msb)|hasByteBelow(x, 0x20)|hasByteEqual(x, 0x7f) != 0 { + break + } + i += 8 + } + if i >= len(b) { + break + } + + c := b[i] + if c >= 0x80 { + var ok bool + i, ok = scanUtf8Run(b, i) + if !ok { + return nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in comment") + } + continue + } + switch { + case c >= 0x20 && c < 0x7f: + i++ + case c == '\n': + return b[:i], b[i:], nil + case c == '\r': + if i+1 < len(b) && b[i+1] == '\n' { + return b[:i], b[i:], nil + } + return nil, nil, NewParserError(b[i:i+1], "carriage returns are not allowed in comments") + case c == '\t': + i++ + default: + return nil, nil, NewParserError(b[i:i+1], "control characters are not allowed in comments") + } + } + return b[:i], b[i:], nil +} + +// parseLiteralString parses a single-line literal string, starting at the +// opening quote. Returns the raw bytes (with quotes), the string value +// (without quotes) and the rest of the input. +func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) { + i := 1 + for { + // Fast path over plain ASCII. + for i+8 <= len(b) { + x := binary.LittleEndian.Uint64(b[i:]) + if (x&msb)|hasByteBelow(x, 0x20)|hasByteEqual(x, '\'')|hasByteEqual(x, 0x7f) != 0 { + break + } + i += 8 + } + if i >= len(b) { + return nil, nil, nil, NewParserError(b[len(b):], "unterminated literal string") + } + + c := b[i] + switch { + case c == '\'': + return b[:i+1], b[1:i], b[i+1:], nil + case c >= 0x20 && c < 0x7f: + i++ + case c == '\t': + i++ + case c == '\n' || c == '\r': + return nil, nil, nil, NewParserError(b[i:i+1], "literal strings cannot have new lines") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "literal strings cannot have control characters") + default: + var ok bool + i, ok = scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in literal string") + } + } + } +} + +// parseMultilineLiteralString parses a multi-line literal string, starting at +// the opening triple quote. +func (p *Parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) { + i := 3 + // trim the newline right after the opening delimiter + if i < len(b) && b[i] == '\n' { + i++ + } else if i+1 < len(b) && b[i] == '\r' && b[i+1] == '\n' { + i += 2 + } + contentStart := i + + for i < len(b) { + c := b[i] + switch { + case c == '\'': + // count consecutive quotes + j := i + for j < len(b) && b[j] == '\'' { + j++ + } + n := j - i + if n >= 3 { + if n > 5 { + return nil, nil, nil, NewParserError(b[i:j], "too many quotes at the end of a multiline literal string") + } + // n-3 quotes belong to the content; the last 3 close the + // string. + contentEnd := i + n - 3 + return b[:j], b[contentStart:contentEnd], b[j:], nil + } + i = j + case c >= 0x20 && c < 0x7f: + i++ + case c == '\t' || c == '\n': + i++ + case c == '\r': + if i+1 < len(b) && b[i+1] == '\n' { + i += 2 + continue + } + return nil, nil, nil, NewParserError(b[i:i+1], "carriage returns must be followed by a newline character") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "multiline literal strings cannot have control characters") + default: + var ok bool + i, ok = scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in multiline literal string") + } + } + } + return nil, nil, nil, NewParserError(b[len(b):], "multiline literal string not terminated by '''") +} + +// parseBasicString parses a single-line basic string, starting at the opening +// quote. The value is a subslice of the input if the string contains no +// escape sequence, or a new allocation otherwise. +func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) { + i := 1 + // First pass: handle strings without escape sequences without allocating. + for { + for i+8 <= len(b) { + x := binary.LittleEndian.Uint64(b[i:]) + if (x&msb)|hasByteBelow(x, 0x20)|hasByteEqual(x, '"')|hasByteEqual(x, '\\')|hasByteEqual(x, 0x7f) != 0 { + break + } + i += 8 + } + if i >= len(b) { + return nil, nil, nil, NewParserError(b[len(b):], "unterminated basic string") + } + + c := b[i] + switch { + case c == '"': + return b[:i+1], b[1:i], b[i+1:], nil + case c == '\\': + // switch to the escape-aware parser, copying what has been + // scanned so far + return p.parseBasicStringEscaped(b, i) + case c >= 0x20 && c < 0x7f: + i++ + case c == '\t': + i++ + case c == '\n' || c == '\r': + return nil, nil, nil, NewParserError(b[i:i+1], "basic strings cannot have new lines") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "basic strings cannot have control characters") + default: + var ok bool + i, ok = scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in basic string") + } + } + } +} + +// findBasicStringEnd returns the index of the quote closing a basic string, +// or -1 if the string is not terminated. i is the index of the first +// character after the opening quote. It does not validate the content: it +// only skips over escape sequences so that escaped quotes do not terminate +// the string. +func findBasicStringEnd(b []byte, i int) int { + for i < len(b) { + switch b[i] { + case '"': + return i + case '\\': + i += 2 + default: + i++ + } + } + return -1 +} + +// parseBasicStringEscaped continues parsing a basic string that contains +// escape sequences. i is the index of the first backslash. +func (p *Parser) parseBasicStringEscaped(b []byte, i int) ([]byte, []byte, []byte, error) { + // Escape sequences only ever shrink, so the content length before + // unescaping is enough to never reallocate. + bufCap := len(b) - 1 + if end := findBasicStringEnd(b, i); end >= 0 { + bufCap = end - 1 + } + buf := make([]byte, i-1, bufCap) + copy(buf, b[1:i]) + + for i < len(b) { + c := b[i] + switch { + case c == '"': + return b[:i+1], buf, b[i+1:], nil + case c == '\\': + i++ + if i >= len(b) { + return nil, nil, nil, NewParserError(b[i-1:], `need a character after \`) + } + var err error + buf, i, err = unescape(buf, b, i) + if err != nil { + return nil, nil, nil, err + } + case c >= 0x20 && c < 0x7f: + buf = append(buf, c) + i++ + case c == '\t': + buf = append(buf, c) + i++ + case c == '\n' || c == '\r': + return nil, nil, nil, NewParserError(b[i:i+1], "basic strings cannot have new lines") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "basic strings cannot have control characters") + default: + j, ok := scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in basic string") + } + buf = append(buf, b[i:j]...) + i = j + } + } + return nil, nil, nil, NewParserError(b[len(b):], "unterminated basic string") +} + +// unescape processes one escape sequence. i is the index of the character +// right after the backslash. It returns the updated buffer and index. +func unescape(buf []byte, b []byte, i int) ([]byte, int, error) { + c := b[i] + switch c { + case '"': + return append(buf, '"'), i + 1, nil + case '\\': + return append(buf, '\\'), i + 1, nil + case 'b': + return append(buf, '\b'), i + 1, nil + case 'f': + return append(buf, '\f'), i + 1, nil + case 'n': + return append(buf, '\n'), i + 1, nil + case 'r': + return append(buf, '\r'), i + 1, nil + case 't': + return append(buf, '\t'), i + 1, nil + case 'e': + // TOML v1.1.0: \e is the escape character (U+001B). + return append(buf, 0x1B), i + 1, nil + case 'x': + // TOML v1.1.0: \xHH is a two-digit hexadecimal code point. + return unescapeUnicode(buf, b, i+1, 2) + case 'u': + return unescapeUnicode(buf, b, i+1, 4) + case 'U': + return unescapeUnicode(buf, b, i+1, 8) + default: + return nil, 0, NewParserError(b[i-1:i+1], "invalid escape character %#U", c) + } +} + +// unescapeUnicode handles \uXXXX and \UXXXXXXXX escape sequences. i is the +// index of the first hex digit. +func unescapeUnicode(buf []byte, b []byte, i int, n int) ([]byte, int, error) { + if i+n > len(b) { + return nil, 0, NewParserError(b[i-2:], "unicode escape sequence is too short") + } + var r uint32 + for k := 0; k < n; k++ { + c := b[i+k] + var d uint32 + switch { + case c >= '0' && c <= '9': + d = uint32(c - '0') + case c >= 'a' && c <= 'f': + d = uint32(c-'a') + 10 + case c >= 'A' && c <= 'F': + d = uint32(c-'A') + 10 + default: + return nil, 0, NewParserError(b[i+k:i+k+1], "invalid hexadecimal digit in unicode escape sequence") + } + r = r<<4 | d + } + if r > utf8.MaxRune || (r >= 0xD800 && r <= 0xDFFF) { + return nil, 0, NewParserError(b[i-2:i+n], "escape sequence is not a valid unicode code point") + } + return utf8.AppendRune(buf, rune(r)), i + n, nil +} + +// parseMultilineBasicString parses a multi-line basic string, starting at the +// opening triple quote. +func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, error) { + i := 3 + // trim the newline right after the opening delimiter + if i < len(b) && b[i] == '\n' { + i++ + } else if i+1 < len(b) && b[i] == '\r' && b[i+1] == '\n' { + i += 2 + } + contentStart := i + + // First pass without allocating, until an escape sequence is found. + for i < len(b) { + c := b[i] + switch { + case c == '"': + j := i + for j < len(b) && b[j] == '"' { + j++ + } + n := j - i + if n >= 3 { + if n > 5 { + return nil, nil, nil, NewParserError(b[i:j], "too many quotes at the end of a multiline basic string") + } + contentEnd := i + n - 3 + return b[:j], b[contentStart:contentEnd], b[j:], nil + } + i = j + case c == '\\': + return p.parseMultilineBasicStringEscaped(b, contentStart, i) + case c >= 0x20 && c < 0x7f: + i++ + case c == '\t' || c == '\n': + i++ + case c == '\r': + if i+1 < len(b) && b[i+1] == '\n' { + i += 2 + continue + } + return nil, nil, nil, NewParserError(b[i:i+1], "carriage returns must be followed by a newline character") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "multiline basic strings cannot have control characters") + default: + var ok bool + i, ok = scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in multiline basic string") + } + } + } + return nil, nil, nil, NewParserError(b[len(b):], `multiline basic string not terminated by """`) +} + +// findMultilineBasicStringEnd returns the index of the first quote of the +// run of quotes closing a multi-line basic string, or -1 if the string is +// not terminated. It does not validate the content: it only skips over +// escape sequences so that escaped quotes do not terminate the string. +func findMultilineBasicStringEnd(b []byte, i int) int { + for { + j := bytes.IndexAny(b[i:], "\"\\") + if j < 0 { + return -1 + } + i += j + if b[i] == '\\' { + i += 2 + if i > len(b) { + return -1 + } + continue + } + j = i + for j < len(b) && b[j] == '"' { + j++ + } + if j-i >= 3 { + return i + } + i = j + } +} + +// parseMultilineBasicStringEscaped continues parsing a multi-line basic +// string that contains escape sequences. i is the index of the first +// backslash; content starts at contentStart. +func (p *Parser) parseMultilineBasicStringEscaped(b []byte, contentStart, i int) ([]byte, []byte, []byte, error) { + // Escape sequences only ever shrink, so the content length before + // unescaping is enough to never reallocate. The closing run of quotes + // can lend up to two quotes to the content. + bufCap := len(b) - contentStart + if end := findMultilineBasicStringEnd(b, i); end >= 0 { + bufCap = end + 2 - contentStart + } + buf := make([]byte, i-contentStart, bufCap) + copy(buf, b[contentStart:i]) + + for i < len(b) { + c := b[i] + switch { + case c == '"': + j := i + for j < len(b) && b[j] == '"' { + j++ + } + n := j - i + if n >= 3 { + if n > 5 { + return nil, nil, nil, NewParserError(b[i:j], "too many quotes at the end of a multiline basic string") + } + buf = append(buf, b[i:i+n-3]...) + return b[:j], buf, b[j:], nil + } + buf = append(buf, b[i:j]...) + i = j + case c == '\\': + i++ + if i >= len(b) { + return nil, nil, nil, NewParserError(b[i-1:], `need a character after \`) + } + // Escaped newline: backslash, optional whitespace, newline, + // then all following whitespace and newlines are trimmed. + if b[i] == ' ' || b[i] == '\t' || b[i] == '\n' || b[i] == '\r' { + j := i + for j < len(b) && (b[j] == ' ' || b[j] == '\t') { + j++ + } + if j < len(b) && b[j] == '\r' { + j++ + } + if j >= len(b) || b[j] != '\n' { + return nil, nil, nil, NewParserError(b[i-1:i+1], "invalid escape character %#U", b[i]) + } + j++ + for j < len(b) && (b[j] == ' ' || b[j] == '\t' || b[j] == '\n' || b[j] == '\r') { + // note: a lone \r not followed by \n will be caught on + // the next iteration of the outer loop. + if b[j] == '\r' { + if j+1 >= len(b) || b[j+1] != '\n' { + break + } + j++ + } + j++ + } + i = j + continue + } + var err error + buf, i, err = unescape(buf, b, i) + if err != nil { + return nil, nil, nil, err + } + case c >= 0x20 && c < 0x7f: + buf = append(buf, c) + i++ + case c == '\t' || c == '\n': + buf = append(buf, c) + i++ + case c == '\r': + if i+1 < len(b) && b[i+1] == '\n' { + buf = append(buf, '\r', '\n') + i += 2 + continue + } + return nil, nil, nil, NewParserError(b[i:i+1], "carriage returns must be followed by a newline character") + case c < 0x80: + return nil, nil, nil, NewParserError(b[i:i+1], "multiline basic strings cannot have control characters") + default: + j, ok := scanUtf8Run(b, i) + if !ok { + return nil, nil, nil, NewParserError(b[i:i+1], "invalid UTF-8 character in multiline basic string") + } + buf = append(buf, b[i:j]...) + i = j + } + } + return nil, nil, nil, NewParserError(b[len(b):], `multiline basic string not terminated by """`) +} + +// scanUtf8Run consumes a run of valid non-ASCII UTF-8 runes starting at +// b[i]. It returns the index of the first byte after the run, and whether +// the run was entirely valid. Processing whole runs amortizes the cost of +// the call compared to validating rune by rune. +func scanUtf8Run(b []byte, i int) (int, bool) { + for i < len(b) { + c := b[i] + switch { + case c < 0x80: + return i, true + case c < 0xC2: + return i, false + case c < 0xE0: + if i+1 >= len(b) || b[i+1]&0xC0 != 0x80 { + return i, false + } + i += 2 + case c < 0xF0: + if i+2 >= len(b) || b[i+2]&0xC0 != 0x80 { + return i, false + } + b1 := b[i+1] + switch c { + case 0xE0: + if b1 < 0xA0 || b1 > 0xBF { + return i, false + } + case 0xED: + // exclude surrogates + if b1 < 0x80 || b1 > 0x9F { + return i, false + } + default: + if b1&0xC0 != 0x80 { + return i, false + } + } + i += 3 + case c < 0xF5: + if i+3 >= len(b) || b[i+2]&0xC0 != 0x80 || b[i+3]&0xC0 != 0x80 { + return i, false + } + b1 := b[i+1] + switch c { + case 0xF0: + if b1 < 0x90 || b1 > 0xBF { + return i, false + } + case 0xF4: + if b1 < 0x80 || b1 > 0x8F { + return i, false + } + default: + if b1&0xC0 != 0x80 { + return i, false + } + } + i += 4 + default: + return i, false + } + } + return i, true +} diff --git a/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go b/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go new file mode 100644 index 000000000..e417040a6 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/v2/unstable/unmarshaler.go @@ -0,0 +1,32 @@ +package unstable + +// Unmarshaler is implemented by types that can unmarshal a TOML description +// of themselves. The input is a valid TOML document containing the relevant +// portion of the parsed document. +// +// For tables (including split tables defined in multiple places), the data +// contains the raw key-value bytes from the original document with adjusted +// table headers to be relative to the unmarshaling target. +type Unmarshaler interface { + UnmarshalTOML(data []byte) error +} + +// RawMessage is a raw encoded TOML value. It implements Unmarshaler and can +// be used to delay TOML decoding or capture raw content. +// +// Example usage: +// +// type Config struct { +// Plugin RawMessage `toml:"plugin"` +// } +// +// var cfg Config +// toml.NewDecoder(r).EnableUnmarshalerInterface().Decode(&cfg) +// // cfg.Plugin now contains the raw TOML bytes for [plugin] +type RawMessage []byte + +// UnmarshalTOML implements Unmarshaler. +func (m *RawMessage) UnmarshalTOML(data []byte) error { + *m = append((*m)[:0], data...) + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1891f6f1d..64eefdfb4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -241,10 +241,16 @@ github.com/cncf/xds/go/xds/data/orca/v3 github.com/cncf/xds/go/xds/service/orca/v3 github.com/cncf/xds/go/xds/type/matcher/v3 github.com/cncf/xds/go/xds/type/v3 +# github.com/containerd/log v0.1.0 +## explicit; go 1.20 +github.com/containerd/log # github.com/containerd/stargz-snapshotter/estargz v0.18.2 ## explicit; go 1.24.0 github.com/containerd/stargz-snapshotter/estargz github.com/containerd/stargz-snapshotter/estargz/errorutil +# github.com/containerd/ttrpc v1.2.8 +## explicit; go 1.22 +github.com/containerd/ttrpc # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew @@ -572,6 +578,12 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runtime-spec v1.3.0 ## explicit github.com/opencontainers/runtime-spec/specs-go +# github.com/pelletier/go-toml/v2 v2.4.0 +## explicit; go 1.21.0 +github.com/pelletier/go-toml/v2 +github.com/pelletier/go-toml/v2/internal/parserbridge +github.com/pelletier/go-toml/v2/internal/tracker +github.com/pelletier/go-toml/v2/unstable # github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 ## explicit; go 1.20 github.com/planetscale/vtprotobuf/protohelpers