├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── workflows │ └── integration-linux.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── go ├── helper-image │ ├── Dockerfile │ ├── delve-as-options.patch │ └── install.sh ├── skaffold.yaml ├── structure-tests-go.yaml └── test │ ├── goapp │ ├── Dockerfile │ └── main.go │ ├── k8s-test-go118.yaml │ ├── k8s-test-go119.yaml │ ├── k8s-test-go120.yaml │ ├── k8s-test-go121.yaml │ ├── k8s-test-go122.yaml │ ├── k8s-test-go123.yaml │ └── k8s-test-go124.yaml ├── hack ├── buildx.sh ├── cloudbuild-promote.yaml ├── cloudbuild-staging.yaml └── enable-docker-buildkit.sh ├── integration ├── k8s-rbac.yaml ├── kubectl │ └── Dockerfile └── skaffold.yaml ├── netcore ├── helper-image │ ├── Dockerfile │ └── install.sh ├── skaffold.yaml ├── structure-tests-netcore.yaml └── test │ └── k8s-test-netcore.yaml ├── nodejs ├── helper-image │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── install.sh │ ├── wrapper.go │ └── wrapper_test.go ├── skaffold.yaml ├── structure-tests-nodejs.yaml └── test │ ├── k8s-test-nodejs12.yaml │ └── nodejsapp │ ├── Dockerfile │ ├── package.json │ └── src │ ├── index.js │ └── utils │ └── index.js ├── publish.sh ├── python ├── helper-image │ ├── Dockerfile │ ├── install.sh │ ├── launcher │ │ ├── cmd.go │ │ ├── cmd_test.go │ │ ├── env.go │ │ ├── env_test.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── launcher.go │ │ └── launcher_test.go │ ├── pydevd_2_8_0.patch │ └── pydevd_2_9_5.patch ├── skaffold.yaml ├── structure-tests-python.yaml └── test │ ├── k8s-test-pydevd-python39.yaml │ ├── k8s-test-pydevd-python3_10.yaml │ ├── k8s-test-pydevd-python3_11.yaml │ ├── pydevconnect │ ├── Dockerfile │ └── pydevconnect.go │ └── pythonapp │ ├── Dockerfile │ ├── requirements.txt │ └── src │ └── app.py ├── run-its.sh └── skaffold.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @GoogleContainerTools/skaffold-team 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Description of the issue**: 4 | 5 | **Expected behavior**: 6 | 7 | **Steps to reproduce**: 8 | 9 | **Environment**: 10 | 11 | **`skaffold-maven-plugin` Configuration:** 12 | ```xml 13 | PASTE YOUR pom.xml CONFIGURATION HERE 14 | ``` 15 | 16 | --- OR --- 17 | 18 | **`skaffold-gradle-plugin` Configuration:** 19 | ```xml 20 | PASTE YOUR build.gradle CONFIGURATION HERE 21 | ``` 22 | 23 | **Log output**: 24 | 25 | **Additional Information**: 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/integration-linux.yml: -------------------------------------------------------------------------------- 1 | name: integration tests (linux) 2 | 3 | on: 4 | push: 5 | branches: [ duct-tape ] 6 | pull_request: 7 | branches: [ duct-tape ] 8 | workflow_dispatch: 9 | 10 | permissions: read-all 11 | 12 | concurrency: 13 | group: build-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | 18 | test: 19 | runs-on: linux-x64-latest-more-storage 20 | steps: 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: ^1.23 24 | 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - uses: actions/cache@v4 30 | with: 31 | path: ~/go/pkg/mod 32 | key: ${{ runner.os }}-gopkgmod-${{ hashFiles('**/go.sum') }} 33 | restore-keys: | 34 | ${{ runner.os }}-gopkgmod 35 | 36 | - name: Install required tools 37 | run: | 38 | set -ex 39 | mkdir -p $HOME/bin 40 | curl -Lo $HOME/bin/skaffold https://storage.googleapis.com/skaffold/builds/latest/skaffold-linux-amd64 41 | curl -Lo $HOME/bin/container-structure-test https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64 42 | curl -Lo $HOME/bin/kind https://github.com/kubernetes-sigs/kind/releases/download/v0.11.1/kind-linux-amd64 43 | curl -Lo $HOME/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.20.0/bin/linux/amd64/kubectl 44 | chmod +x $HOME/bin/* 45 | echo "$HOME/bin" >> $GITHUB_PATH 46 | 47 | - name: Run nodejs helper tests 48 | run: | 49 | set -ex 50 | (cd nodejs/helper-image; go test .) 51 | 52 | - name: Run python helper tests 53 | run: | 54 | set -ex 55 | (cd python/helper-image/launcher; go test .) 56 | 57 | - name: Run image build 58 | run: | 59 | set -ex 60 | # Create a kind configuration to use the docker daemon's configured registry-mirrors. 61 | docker system info --format '{{printf "apiVersion: kind.x-k8s.io/v1alpha4\nkind: Cluster\ncontainerdConfigPatches:\n"}}{{range $reg, $config := .RegistryConfig.IndexConfigs}}{{if $config.Mirrors}}{{printf "- |-\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"%s\"]\n endpoint = %q\n" $reg $config.Mirrors}}{{end}}{{end}}' > /tmp/kind.config 62 | 63 | # `kind create cluster` is very verbose 64 | kind create cluster --quiet --config /tmp/kind.config 65 | kind get kubeconfig > /tmp/kube.config 66 | 67 | # we had `run-its.sh` in `after_success` but it doesn't cause failures 68 | KUBECONFIG=/tmp/kube.config bash ./run-its.sh 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | target 3 | out 4 | *.iml 5 | *.ipr 6 | *.iws 7 | .idea 8 | .gradle 9 | /.settings 10 | /.classpath 11 | /.project 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Container Debug Runtime Support 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | 1. Set your git user.email property to the address used for step 1. E.g. 21 | ``` 22 | git config --global user.email "janedoe@google.com" 23 | ``` 24 | If you're a Googler or other corporate contributor, 25 | use your corporate email address here, not your personal address. 26 | 2. Fork the repository into your own Github account. 27 | 3. Please include unit tests (and integration tests if applicable) for all new code. 28 | 4. Make sure all existing tests pass. 29 | 5. Associate the change with an existing issue or file a [new issue](../../issues). 30 | 6. Create a pull request! 31 | 32 | 33 | # Development 34 | 35 | This project uses Skaffold's multiple config support to allow 36 | developing for each language runtime separately. 37 | 38 | Each language runtime is broken out to a separate directory 39 | with a `skaffold.yaml` for development of the `duct-tape` initContainer 40 | image. Each image is expected to be standalone and should not require 41 | downloading additional content across the network. To add support for a new 42 | language runtime, an image definition should download the necessary 43 | files into the container image. The image's entrypoint should then 44 | copy those files into place at `/dbg/`. The image should 45 | be added to the respective `skaffold.yaml` and referenced within 46 | `test/k8s-*-installation.yaml`. 47 | 48 | We currently build language support images for both `linux/amd64` and 49 | `linux/arm64`. 50 | 51 | Images are currently published with two names: the short-form (like 52 | `go`) and a longer-form (`skaffold-debug-go`). The short-forms are 53 | marked as deprecated as we intend to move away from, but they are 54 | still used by Skaffold. 55 | 56 | ## Testing 57 | 58 | Integration tests are found under each language runtime directory in 59 | `test/`. These tests build and launch applications as pods that 60 | are similar to the transformed form produced by `skaffold debug`. 61 | To run: 62 | 63 | ```sh 64 | sh run-its.sh 65 | ``` 66 | 67 | You can run this script from a language-runtime location too to 68 | test only that runtime's support: 69 | 70 | ```sh 71 | cd nodejs 72 | sh ../run-its.sh 73 | ``` 74 | 75 | # Staging and Deploying 76 | 77 | To stage a set of images for testing use the following command, 78 | where `$REPO` is the image repository where you plan to host the 79 | images. 80 | ```sh 81 | skaffold build -p release,deprecated-names --default-repo $REPO 82 | ``` 83 | 84 | The `release` profile causes the images to be pushed to the specified 85 | repository and also enables multi-arch builds using buildx (default 86 | `linux/amd64` and `linux/arm64`). The `deprecated-names` profile enables 87 | using the short-form image names (`go`, `netcore`, `nodejs`, `python`) 88 | that are currently used by `skaffold debug`. 89 | 90 | Then configure Skaffold to point to that location: 91 | ```sh 92 | skaffold config set --global debug-helpers-registry $REPO 93 | ``` 94 | 95 | You should then be able to use `skaffold debug` with the 96 | staged images. 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![experimental](https://img.shields.io/badge/stability-experimental-orange.svg) 2 | 3 | # Container Runtime Debugging Support Images (aka Duct Tape) 4 | 5 | This repository defines a set of container images that package 6 | the language runtime dependencies required to enable step-by-step 7 | debugging of apps with 8 | [`skaffold debug`](https://skaffold.dev/docs/how-tos/debug/). 9 | These container images are suitable for use as `initContainer`s on 10 | a pod. When executed, each container image copies these dependencies 11 | to `/dbg/`. 12 | 13 | The idea is that `skaffold debug` will transform k8s manifests to 14 | make available any support files required to debug specific language 15 | runtimes. For example, a Kubernetes podspec would be transformed to 16 | 17 | - create a volume to hold the debugging support files 18 | - run one or more of these images as `initContainer`s to populate 19 | this volume, mounted as `/dbg` 20 | - mount this volume on the applicable containers as `/dbg` 21 | with suitably transformed command-line in the entrypoint and arguments 22 | 23 | Current language runtimes: 24 | 25 | * `go`: provides [Delve](https://github.com/go-delve/delve) 26 | * `python`: provides [`ptvsd`](https://github.com/Microsoft/ptvsd), 27 | a debug adapter that can be used for VS Code and more, for 28 | Python 2.7 and 3.5+ 29 | * `nodejs`: provides a `node` wrapper that propagates `--inspect` 30 | args to the application invokation 31 | * `netcore`: provides `vsdbg` for .NET Core 32 | 33 | ## Distribution 34 | 35 | The latest released images, which are used by `skaffold debug`, are available at: 36 | 37 | gcr.io/k8s-skaffold/skaffold-debug-support 38 | 39 | Images from a particular release are available at: 40 | 41 | gcr.io/k8s-skaffold/skaffold-debug-support/ 42 | 43 | Images from the latest commit to HEAD are available at our staging repository: 44 | 45 | us-central1-docker.pkg.dev/k8s-skaffold/skaffold-staging/skaffold-debug-support 46 | 47 | You can configure Skaffold to use a specific release or the staging 48 | repository with the following: 49 | 50 | skaffold config set --global debug-helpers-registry 51 | 52 | 53 | # Contributing 54 | 55 | See [CONTRIBUTING](CONTRIBUTING.md) for how to contribute! 56 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please use http://g.co/vulnz. We use 4 | http://g.co/vulnz for our intake, and do coordination and disclosure here on 5 | GitHub (including using [GitHub Security Advisory]). The Google Security Team will 6 | respond within 5 working days of your report on g.co/vulnz. 7 | 8 | [GitHub Security Advisory]: https://github.com/GoogleContainerTools/skaffold/security/advisories 9 | -------------------------------------------------------------------------------- /go/helper-image/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOVERSION=1.24 2 | FROM --platform=$BUILDPLATFORM golang:${GOVERSION} AS delve 3 | ARG BUILDPLATFORM 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | ARG DELVE_VERSION=1.24.1 8 | 9 | # Patch delve to make defaults for --check-go-version and --only-same-user 10 | # to be set at build time. We must install patch(1) to apply the patch. 11 | # 12 | # We default --check-go-version to false to support binaries compiled 13 | # with unsupported versions of Go. Delve issues a prominent warning. 14 | # 15 | # We default --only-same-user to false as `kubectl port-forward` 16 | # to dlv port is refused otherwise. 17 | RUN apt-get update && apt-get install -y --no-install-recommends \ 18 | patch 19 | RUN curl --location --output delve.tar.gz https://github.com/go-delve/delve/archive/v$DELVE_VERSION.tar.gz \ 20 | && tar xzf delve.tar.gz \ 21 | && mv delve-$DELVE_VERSION delve-source 22 | COPY delve-*.patch . 23 | RUN patch -p0 -d delve-source < delve-as-options.patch 24 | 25 | # Produce an as-static-as-possible dlv binary to work on musl and glibc 26 | RUN cd delve-source \ 27 | && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /go/dlv -ldflags '-s -w -X github.com/go-delve/delve/cmd/dlv/cmds.checkGoVersionDefault=false -X github.com/go-delve/delve/cmd/dlv/cmds.checkLocalConnUserDefault=false -extldflags "-static"' ./cmd/dlv/ 28 | 29 | # Now populate the duct-tape image with the language runtime debugging support files 30 | # The debian image is about 95MB bigger 31 | FROM busybox 32 | # The install script copies all files in /duct-tape to /dbg 33 | COPY install.sh / 34 | CMD ["/bin/sh", "/install.sh"] 35 | WORKDIR /duct-tape 36 | COPY --from=delve /go/dlv go/bin/ 37 | -------------------------------------------------------------------------------- /go/helper-image/delve-as-options.patch: -------------------------------------------------------------------------------- 1 | diff --git cmd/dlv/cmds/commands.go cmd/dlv/cmds/commands.go 2 | index 374b5451..ad1c6b69 100644 3 | --- cmd/dlv/cmds/commands.go 4 | +++ cmd/dlv/cmds/commands.go 5 | @@ -61,6 +61,8 @@ var ( 6 | // checkLocalConnUser is true if the debugger should check that local 7 | // connections come from the same user that started the headless server 8 | checkLocalConnUser bool 9 | + // checkLocalConnUserDefault sets default for --only-same-user 10 | + checkLocalConnUserDefault = "true" 11 | // tty is used to provide an alternate TTY for the program you wish to debug. 12 | tty string 13 | // disableASLR is used to disable ASLR 14 | @@ -78,6 +80,8 @@ var ( 15 | // used to compile the executable and refuse to work on incompatible 16 | // versions. 17 | checkGoVersion bool 18 | + // checkGoVersionDefault sets default for --check-go-version 19 | + checkGoVersionDefault = "true" 20 | 21 | // rootCommand is the root of the command tree. 22 | rootCommand *cobra.Command 23 | @@ -158,8 +162,8 @@ func New(docCall bool) *cobra.Command { 24 | must(rootCommand.RegisterFlagCompletionFunc("build-flags", cobra.NoFileCompletions)) 25 | rootCommand.PersistentFlags().StringVar(&workingDir, "wd", "", "Working directory for running the program.") 26 | must(rootCommand.MarkPersistentFlagDirname("wd")) 27 | - rootCommand.PersistentFlags().BoolVarP(&checkGoVersion, "check-go-version", "", true, "Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve.") 28 | - rootCommand.PersistentFlags().BoolVarP(&checkLocalConnUser, "only-same-user", "", true, "Only connections from the same user that started this instance of Delve are allowed to connect.") 29 | + rootCommand.PersistentFlags().BoolVarP(&checkGoVersion, "check-go-version", "", parseBool(checkGoVersionDefault), "Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve.") 30 | + rootCommand.PersistentFlags().BoolVarP(&checkLocalConnUser, "only-same-user", "", parseBool(checkLocalConnUserDefault), "Only connections from the same user that started this instance of Delve are allowed to connect.") 31 | rootCommand.PersistentFlags().StringVar(&backend, "backend", "default", `Backend selection (see 'dlv help backend').`) 32 | must(rootCommand.RegisterFlagCompletionFunc("backend", cobra.FixedCompletions([]string{"default", "native", "lldb", "rr"}, cobra.ShellCompDirectiveNoFileComp))) 33 | rootCommand.PersistentFlags().StringArrayVarP(&redirects, "redirect", "r", []string{}, "Specifies redirect rules for target process (see 'dlv help redirect')") 34 | @@ -1249,3 +1253,14 @@ func must(err error) { 35 | log.Fatal(err) 36 | } 37 | } 38 | + 39 | +// parseBool parses a boolean value represented by a string, and panics if there is an error. 40 | +// It is intended for boolean build-time constants that are set with 'go build -ldflags=-X xxx=bool' 41 | +// and should only be a valid value. 42 | +func parseBool(value string) bool { 43 | + b, err := strconv.ParseBool(value) 44 | + if err != nil { 45 | + panic(err) 46 | + } 47 | + return b 48 | +} 49 | -------------------------------------------------------------------------------- /go/helper-image/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -d /dbg ]; then 5 | echo "Error: installation requires a volume mount at /dbg" 1>&2 6 | exit 1 7 | fi 8 | 9 | echo "Installing runtime debugging support files in /dbg" 10 | tar cf - -C /duct-tape . | tar xf - -C /dbg 11 | echo "Installation complete" 12 | -------------------------------------------------------------------------------- /go/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: go 5 | 6 | requires: 7 | - path: ../integration 8 | activeProfiles: 9 | - name: integration 10 | activatedBy: [integration] 11 | 12 | build: 13 | local: 14 | useBuildkit: true 15 | artifacts: 16 | - image: skaffold-debug-go 17 | context: helper-image 18 | custom: 19 | buildCommand: ../../hack/buildx.sh 20 | 21 | test: 22 | - image: skaffold-debug-go 23 | structureTests: [structure-tests-go.yaml] 24 | 25 | deploy: 26 | logs: 27 | prefix: auto 28 | kubectl: 29 | manifests: [] # overwritten in integration profile 30 | 31 | profiles: 32 | # local: never push to remote registries 33 | - name: local 34 | build: 35 | local: 36 | push: false 37 | 38 | # integration: set of `skaffold debug`-like integration tests 39 | - name: integration 40 | patches: 41 | - op: add 42 | path: /build/artifacts/- 43 | value: 44 | image: go118app 45 | context: test/goapp 46 | docker: 47 | buildArgs: 48 | GOVERSION: '1.18' 49 | - op: add 50 | path: /build/artifacts/- 51 | value: 52 | image: go119app 53 | context: test/goapp 54 | docker: 55 | buildArgs: 56 | GOVERSION: '1.19' 57 | - op: add 58 | path: /build/artifacts/- 59 | value: 60 | image: go120app 61 | context: test/goapp 62 | docker: 63 | buildArgs: 64 | GOVERSION: '1.20' 65 | - op: add 66 | path: /build/artifacts/- 67 | value: 68 | image: go121app 69 | context: test/goapp 70 | docker: 71 | buildArgs: 72 | GOVERSION: '1.21' 73 | - op: add 74 | path: /build/artifacts/- 75 | value: 76 | image: go122app 77 | context: test/goapp 78 | docker: 79 | buildArgs: 80 | GOVERSION: '1.22' 81 | - op: add 82 | path: /build/artifacts/- 83 | value: 84 | image: go123app 85 | context: test/goapp 86 | docker: 87 | buildArgs: 88 | GOVERSION: '1.23' 89 | - op: add 90 | path: /build/artifacts/- 91 | value: 92 | image: go124app 93 | context: test/goapp 94 | docker: 95 | buildArgs: 96 | GOVERSION: '1.24' 97 | 98 | deploy: 99 | kubectl: 100 | manifests: 101 | - test/k8s-test-go118.yaml 102 | - test/k8s-test-go119.yaml 103 | - test/k8s-test-go120.yaml 104 | - test/k8s-test-go121.yaml 105 | - test/k8s-test-go122.yaml 106 | - test/k8s-test-go123.yaml 107 | - test/k8s-test-go124.yaml 108 | 109 | # release: pushes images to production with :latest 110 | - name: release 111 | build: 112 | local: 113 | push: true 114 | tagPolicy: 115 | sha256: {} 116 | 117 | # deprecated-names: use short (deprecated) image names: images were 118 | # prefixed with `skaffold-debug-` so they were more easily distinguished 119 | # from other images with similar names. 120 | - name: deprecated-names 121 | patches: 122 | - op: replace 123 | path: /build/artifacts/0/image 124 | from: skaffold-debug-go 125 | value: go 126 | - op: replace 127 | path: /test/0/image 128 | from: skaffold-debug-go 129 | value: go 130 | -------------------------------------------------------------------------------- /go/structure-tests-go.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: 2.0.0 2 | 3 | fileExistenceTests: 4 | - name: 'dlv' 5 | path: '/duct-tape/go/bin/dlv' 6 | shouldExist: true 7 | 8 | commandTests: 9 | - name: "run with no /dbg should fail" 10 | command: "sh" 11 | args: ["/install.sh"] 12 | expectedError: ["Error: installation requires a volume mount at /dbg"] 13 | exitCode: 1 14 | - name: "run with /dbg should install" 15 | setup: [["mkdir", "/dbg"]] 16 | command: "sh" 17 | args: ["/install.sh"] 18 | expectedOutput: ["Installing runtime debugging support files in /dbg", "Installation complete"] 19 | exitCode: 0 20 | -------------------------------------------------------------------------------- /go/test/goapp/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOVERSION 2 | FROM --platform=$BUILDPLATFORM golang:$GOVERSION as builder 3 | ARG BUILDPLATFORM 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | COPY main.go . 8 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -gcflags="all=-N -l" -o /app main.go 9 | 10 | FROM --platform=$BUILDPLATFORM gcr.io/distroless/base 11 | CMD ["./app"] 12 | ENV GOTRACEBACK=single 13 | COPY --from=builder /app . 14 | -------------------------------------------------------------------------------- /go/test/goapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "runtime" 8 | ) 9 | 10 | func main() { 11 | http.HandleFunc("/", hello) 12 | 13 | log.Println("Listening on port 8080") 14 | http.ListenAndServe(":8080", nil) 15 | } 16 | 17 | func hello(w http.ResponseWriter, r *http.Request) { 18 | fmt.Fprintf(w, "Hello from %s/%s!\n", runtime.GOOS, runtime.GOARCH) 19 | } 20 | -------------------------------------------------------------------------------- /go/test/k8s-test-go118.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go118pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go118 10 | spec: 11 | containers: 12 | - name: go118app 13 | image: go118app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go118 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go118 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go118 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go118pod 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go118:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go118 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go118:56286'] 90 | 91 | 92 | -------------------------------------------------------------------------------- /go/test/k8s-test-go119.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go119pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go119 10 | spec: 11 | containers: 12 | - name: go119app 13 | image: go119app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go119 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go119 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go119 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go119pod 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go119:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go119 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go119:56286'] 90 | 91 | 92 | -------------------------------------------------------------------------------- /go/test/k8s-test-go120.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go120pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go120 10 | spec: 11 | containers: 12 | - name: go120app 13 | image: go120app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go120 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go120 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go120 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go120 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go120:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go120 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go120:56286'] 90 | -------------------------------------------------------------------------------- /go/test/k8s-test-go121.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go121pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go121 10 | spec: 11 | containers: 12 | - name: go121app 13 | image: go121app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go121 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go121 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go121 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go121 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go121:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go121 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go121:56286'] 90 | -------------------------------------------------------------------------------- /go/test/k8s-test-go122.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go120pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go120 10 | spec: 11 | containers: 12 | - name: go120app 13 | image: go120app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go120 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go120 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go120 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go120 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go120:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go120 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go120:56286'] 90 | -------------------------------------------------------------------------------- /go/test/k8s-test-go123.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go123pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go123 10 | spec: 11 | containers: 12 | - name: go123app 13 | image: go123app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go123 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go123 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go123 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go123 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go123:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go123 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go123:56286'] 90 | -------------------------------------------------------------------------------- /go/test/k8s-test-go124.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a go app. 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: go124pod 6 | labels: 7 | app: hello 8 | protocol: dlv 9 | runtime: go124 10 | spec: 11 | containers: 12 | - name: go124app 13 | image: go124app 14 | args: 15 | - /dbg/go/bin/dlv 16 | - exec 17 | - --log 18 | - --headless 19 | - --continue 20 | - --accept-multiclient 21 | # listen on 0.0.0.0 as it is exposed as a service 22 | - --listen=0.0.0.0:56286 23 | - --api-version=2 24 | - ./app 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 56286 28 | name: dlv 29 | readinessProbe: 30 | httpGet: 31 | path: / 32 | port: 8080 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: go-debugging-support 36 | initContainers: 37 | - image: skaffold-debug-go 38 | name: install-go-support 39 | resources: {} 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: go-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: go-debugging-support 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: hello-dlv-go124 52 | spec: 53 | ports: 54 | - name: http 55 | port: 8080 56 | protocol: TCP 57 | - name: dlv 58 | port: 56286 59 | protocol: TCP 60 | selector: 61 | app: hello 62 | protocol: dlv 63 | runtime: go124 64 | 65 | --- 66 | apiVersion: batch/v1 67 | kind: Job 68 | metadata: 69 | name: connect-to-go124 70 | labels: 71 | project: container-debug-support 72 | type: integration-test 73 | spec: 74 | ttlSecondsAfterFinished: 10 75 | backoffLimit: 1 76 | template: 77 | spec: 78 | restartPolicy: Never 79 | initContainers: 80 | - name: wait-for-go124 81 | image: kubectl 82 | command: [sh, -c, "while ! curl -s hello-dlv-go124:8080 2>/dev/null; do echo waiting for app; sleep 1; done"] 83 | containers: 84 | - name: dlv-to-go124 85 | image: skaffold-debug-go 86 | command: [sh, -c, ' 87 | (echo bt; echo exit -c) > init.txt; 88 | set -x; 89 | /duct-tape/go/bin/dlv connect --init init.txt hello-dlv-go124:56286'] 90 | -------------------------------------------------------------------------------- /hack/buildx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Skaffold Custom Builder that uses `docker buildx` to perform a 4 | # multi-platform build. 5 | 6 | PLATFORMS=linux/amd64,linux/arm64 7 | 8 | if ! docker buildx inspect skaffold-builder >/dev/null 2>&1; then 9 | echo ">> creating "docker buildx" builder 'skaffold-builder'" 10 | # Docker 3.3.0 require creating a builder within a context 11 | docker context create skaffold 12 | docker buildx create --name skaffold-builder --platform $PLATFORMS skaffold 13 | fi 14 | 15 | loadOrPush=$(if [ "$PUSH_IMAGE" = true ]; then echo --platform $PLATFORMS --push; else echo --load; fi) 16 | 17 | set -x 18 | docker buildx build \ 19 | --progress=plain \ 20 | --builder skaffold-builder \ 21 | $loadOrPush \ 22 | --tag $IMAGE \ 23 | "$BUILD_CONTEXT" 24 | 25 | -------------------------------------------------------------------------------- /hack/cloudbuild-promote.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Use GitHub releases to promote container-debug-support images 3 | 4 | # We use GitHub release triggers to promote the images corresponding to 5 | # the release commit from staging ($_STAGING) to the production location ($_PROD). 6 | # Images are tagged with the $SHORT_SHA and $TAG_NAME where appropriate. 7 | # 8 | # Release tags ($TAG_NAME) are expected to follow a `vN.N` pattern and are 9 | # expected to be distinct. The `vN` indicates a major version (e.g., v1.35 -> v1). 10 | # These release tags are generally expected to be distinct, such that they 11 | # shouldn't be overwritten by accident. 12 | # 13 | # This promotion script copies the long- and short-form images to: 14 | # 15 | # 1. $_PROD/$TAG_NAME tagged with `latest` and `$SHORT_SHA` 16 | # Users can then use a specific release with: 17 | # ``` 18 | # skaffold config set --global debug-helpers-registry $_PROD/$TAG_NAME 19 | # ``` 20 | # 2. $_PROD/$MAJORVER tagged with `latest`, `$TAG_NAME`, and `$SHORT_SHA`, 21 | # when $_IS_LATEST is true and $TAG_NAME has a valid major version 22 | # 3. $_PROD for backward compatibility, tagged with `latest`, `$TAG_NAME`, and `$SHORT_NAME`, 23 | # when $_IS_LATEST is true and the major version is `v1`. 24 | # 25 | # For example, tagging commit 70f0f74 as v1.1 should result in the images being 26 | # copied over as: 27 | # 28 | # 1. gcr.io/k8s-skaffold/skaffold-debug-support/v1.1/:{latest,70f0f74} 29 | # 2. gcr.io/k8s-skaffold/skaffold-debug-support/v1/:{latest,v1.1,70f0f74} 30 | # 3. gcr.io/k8s-skaffold/skaffold-debug-support/:{latest,v1.1,70f0f74} 31 | # 32 | # The last location (3) occurs because the major version is v1. This copy is to maintain 33 | # backwards compatibility with the existing versions of Skaffold. When we bump the 34 | # major version to v2, we will no longer copy images into (3). 35 | # 36 | # To test: 37 | # $ export CLOUDSDK_CORE_PROJECT=bdealwis-playground 38 | # $ gcloud builds submit --config=hack/cloudbuild-promote.yaml \ 39 | # --substitutions=SHORT_SHA=999999,TAG_NAME=v1.23,_STAGING=us-central1-docker.pkg.dev/$CLOUDSDK_CORE_PROJECT/junk/skaffold-debug-support,_PROD=gcr.io/$CLOUDSDK_CORE_PROJECT/skaffold-debug-support 40 | # 41 | # To replace a previous release with rebuilt images: 42 | # $ gcloud builds submit --config=hack/cloudbuild-promote.yaml \ 43 | # --substitutions=SHORT_SHA=xxxx,TAG_NAME=v1.23,_STAGING=us-central1-docker.pkg.dev/$CLOUDSDK_CORE_PROJECT/junk/skaffold-debug-support,_PROD=gcr.io/$CLOUDSDK_CORE_PROJECT/skaffold-debug-support,_IS_LATEST=0 44 | # 45 | options: 46 | #machineType: 'E2_HIGHCPU_8' 47 | 48 | substitutions: 49 | _STAGING: us-central1-docker.pkg.dev/${PROJECT_ID}/skaffold-staging/skaffold-debug-support 50 | _PROD: gcr.io/${PROJECT_ID}/skaffold-debug-support 51 | _RUNTIMES: go netcore nodejs python 52 | _IS_LATEST: "1" 53 | options: 54 | dynamic_substitutions: true 55 | 56 | steps: 57 | ################################################################### 58 | # Validate that $TAG_NAME is an acceptable image component name and tag 59 | # Regexs from https://github.com/opencontainers/distribution-spec/blob/main/spec.md 60 | - id: validate-tag 61 | name: bash 62 | entrypoint: 'bash' 63 | args: 64 | - '-eEuo' 65 | - 'pipefail' 66 | - '-c' 67 | - |- 68 | if [[ "$TAG_NAME" =~ ^[a-z0-9]+([._-][a-z0-9]+)*$ ]] \ 69 | && [[ "$TAG_NAME" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then 70 | echo "Accepted tag" 71 | else 72 | echo "Release tag [$TAG_NAME] is not a valid image name component or image tag" 73 | exit 1 74 | fi 75 | 76 | ################################################################### 77 | # Copy the staged images to release loction in $_PROD/$TAG_NAME. 78 | # This allows users to use a specific release with: 79 | # skaffold config set --global debug-helpers-registry $_PROD/$TAG_NAME 80 | # 81 | # First copy staged images into $_PROD/$TAG_NAME tagged with $SHORT_SHA. 82 | # If this step fails, then nothing irrevocable has occurred. 83 | - id: install-release-images 84 | name: gcr.io/go-containerregistry/gcrane:debug 85 | entrypoint: /busybox/sh 86 | args: 87 | - "-euc" 88 | - |- 89 | for runtime in $_RUNTIMES; do 90 | gcrane copy $_STAGING/$SHORT_SHA/skaffold-debug-$$runtime:latest $_PROD/$TAG_NAME/skaffold-debug-$$runtime:$SHORT_SHA 91 | gcrane copy $_STAGING/$SHORT_SHA/$$runtime:latest $_PROD/$TAG_NAME/$$runtime:$SHORT_SHA 92 | done 93 | # Then install these images by tagging them with `latest`. 94 | - id: promote-release-images 95 | name: gcr.io/go-containerregistry/gcrane:debug 96 | entrypoint: /busybox/sh 97 | args: 98 | - "-euc" 99 | - |- 100 | for runtime in $_RUNTIMES; do 101 | gcrane tag $_PROD/$TAG_NAME/skaffold-debug-$$runtime:$SHORT_SHA latest 102 | gcrane tag $_PROD/$TAG_NAME/$$runtime:$SHORT_SHA latest 103 | done 104 | echo "Images promoted to $_PROD/$TAG_NAME" 105 | 106 | # Promote to major version (e.g., v1.35 -> v1). 107 | # If IS_LATEST=1 copy these tagged images into latest. 108 | - id: promote-to-major-version 109 | name: gcr.io/go-containerregistry/gcrane:debug 110 | entrypoint: /busybox/sh 111 | args: 112 | - "-euc" 113 | - |- 114 | MAJORVER=$$(echo $TAG_NAME | sed -n 's/\(v[0-9][0-9]\)*\.[0-9.]*/\1/p') 115 | if [ -z "$$MAJORVER" ]; then 116 | echo "Skipping rest of promotion: Release tag [${TAG_NAME}] does not have [vN.*] major version" 117 | exit 0 118 | fi 119 | 120 | for runtime in $_RUNTIMES; do 121 | gcrane copy $_STAGING/$SHORT_SHA/skaffold-debug-$$runtime:latest $_PROD/$$MAJORVER/skaffold-debug-$$runtime:$SHORT_SHA 122 | gcrane copy $_STAGING/$SHORT_SHA/$$runtime:latest $_PROD/$$MAJORVER/$$runtime:$SHORT_SHA 123 | if [ "$$MAJORVER" = v1 ]; then 124 | gcrane copy $_STAGING/$SHORT_SHA/skaffold-debug-$$runtime:latest $_PROD/skaffold-debug-$$runtime:$SHORT_SHA 125 | gcrane copy $_STAGING/$SHORT_SHA/$$runtime:latest $_PROD/$$runtime:$SHORT_SHA 126 | fi 127 | done 128 | for runtime in $_RUNTIMES; do 129 | gcrane tag $_PROD/$$MAJORVER/skaffold-debug-$$runtime:$SHORT_SHA $TAG_NAME 130 | gcrane tag $_PROD/$$MAJORVER/$$runtime:$SHORT_SHA $TAG_NAME 131 | if [ "$$MAJORVER" = v1 ]; then 132 | gcrane tag $_PROD/skaffold-debug-$$runtime:$SHORT_SHA $TAG_NAME 133 | gcrane tag $_PROD/$$runtime:$SHORT_SHA $TAG_NAME 134 | fi 135 | done 136 | case "$_IS_LATEST" in 137 | 0|no|NO|false|FALSE) echo "skipping promotion to latest as _IS_LATEST=${_IS_LATEST}"; exit 0;; 138 | esac 139 | 140 | for runtime in $_RUNTIMES; do 141 | gcrane tag $_PROD/$$MAJORVER/skaffold-debug-$$runtime:$SHORT_SHA latest 142 | gcrane tag $_PROD/$$MAJORVER/$$runtime:$SHORT_SHA latest 143 | done 144 | echo "Images promoted to $_PROD/$$MAJORVER as latest" 145 | 146 | if [ "$$MAJORVER" = v1 ]; then 147 | for runtime in $_RUNTIMES; do 148 | gcrane tag $_PROD/skaffold-debug-$$runtime:$SHORT_SHA latest 149 | gcrane tag $_PROD/$$runtime:$SHORT_SHA latest 150 | done 151 | echo "Images promoted to $_PROD as latest" 152 | fi 153 | 154 | timeout: 200s 155 | -------------------------------------------------------------------------------- /hack/cloudbuild-staging.yaml: -------------------------------------------------------------------------------- 1 | # Stage container-debug-support images to the staging location ($_REPO) using 2 | # both long- and short-form names. On a successful build, long- and short-form images 3 | # will be pushed to: 4 | # - $_REPO/$SHORT_SHA tagged with `:latest` 5 | # - $_REPO tagged with `:$SHORT_SHA` and `:latest`, replacing previously staged images 6 | # 7 | # To test: 8 | # $ env CLOUDSDK_CORE_PROJECT=bdealwis-playground \ 9 | # gcloud builds submit --config=hack/cloudbuild-staging.yaml \ 10 | # --substitutions=SHORT_SHA=999999,_REPO=us-central1-docker.pkg.dev/bdealwis-playground/junk/skaffold-debug-support . 11 | options: 12 | machineType: 'E2_HIGHCPU_8' 13 | env: 14 | - DOCKER_CLI_EXPERIMENTAL=enabled 15 | 16 | substitutions: 17 | _REPO: us-central1-docker.pkg.dev/k8s-skaffold/skaffold-staging/skaffold-debug-support 18 | _RUNTIMES: go netcore nodejs python 19 | 20 | steps: 21 | # Update buildx to 0.7.1 and configure docker for multi-platform builds with qemu 22 | - name: gcr.io/cloud-builders/gcloud 23 | entrypoint: 'bash' 24 | args: 25 | - '-eEuo' 26 | - 'pipefail' 27 | - '-c' 28 | - |- 29 | mkdir -p $$HOME/.docker/cli-plugins 30 | curl -sLo $$HOME/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.7.1/buildx-v0.7.1.linux-amd64 31 | chmod +x $$HOME/.docker/cli-plugins/docker-buildx 32 | - name: gcr.io/cloud-builders/docker 33 | args: [info] 34 | # Install tonistiigi's binfmt extensions for arm64 emulation 35 | - name: 'tonistiigi/binfmt@sha256:8de6f2decb92e9001d094534bf8a92880c175bd5dfb4a9d8579f26f09821cfa2' # qemu-6.1.0 36 | args: [--install, "arm64"] 37 | 38 | ################################################################### 39 | # Build the images to separate loction in $_REPO to allow easy testing with images from specific commit: 40 | # skaffold config set --global debug-helpers-registry $_REPO/$SHORT_SHA 41 | - name: gcr.io/k8s-skaffold/skaffold 42 | args: [skaffold, build, -p, release, --default-repo, $_REPO/$SHORT_SHA] 43 | # Copy the images to the short-names for backwards compatibility. 44 | # Ideally we would just re-invoke Skaffold which should push the just-built 45 | # long-form images to the short-form names but that isn't working 46 | # (https://github.com/GoogleContainerTools/skaffold/issues/6957). 47 | - name: gcr.io/go-containerregistry/gcrane:debug 48 | entrypoint: /busybox/sh 49 | args: 50 | - "-euc" 51 | - |- 52 | for runtime in $_RUNTIMES; do 53 | gcrane copy $_REPO/$SHORT_SHA/skaffold-debug-$$runtime:latest $_REPO/$SHORT_SHA/$$runtime:latest 54 | done 55 | 56 | ################################################################### 57 | # Stage the images into $_REPO 58 | # 59 | # First copy images into $_REPO using the $SHORT_SHA as tag 60 | - name: gcr.io/go-containerregistry/gcrane:debug 61 | entrypoint: /busybox/sh 62 | args: 63 | - "-euc" 64 | - |- 65 | for runtime in $_RUNTIMES; do 66 | gcrane copy $_REPO/$SHORT_SHA/skaffold-debug-$$runtime:latest $_REPO/skaffold-debug-$$runtime:$SHORT_SHA 67 | gcrane copy $_REPO/$SHORT_SHA/$$runtime:latest $_REPO/$$runtime:$SHORT_SHA 68 | done 69 | # Then retag long- and short-forms as :latest 70 | - name: gcr.io/go-containerregistry/gcrane:debug 71 | entrypoint: /busybox/sh 72 | args: 73 | - "-euc" 74 | - |- 75 | for runtime in $_RUNTIMES; do 76 | gcrane tag $_REPO/skaffold-debug-$$runtime:$SHORT_SHA latest 77 | gcrane tag $_REPO/$$runtime:$SHORT_SHA latest 78 | done 79 | 80 | # amd64 + arm64 builds typically take about 15 minutes 81 | timeout: 1800s 82 | -------------------------------------------------------------------------------- /hack/enable-docker-buildkit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # https://www.docker.com/blog/multi-arch-build-what-about-travis/ 3 | 4 | echo ">> enabling experimental mode" 5 | if [ -f /etc/docker/daemon.json ]; then 6 | echo "/etc/docker/daemon.json was:" 7 | sed 's/^/> /' /etc/docker/daemon.json 8 | echo "/etc/docker/daemon.json now:" 9 | jq '.+{"experimental":true}' /etc/docker/daemon.json \ 10 | | jq '."registry-mirrors" += ["https://mirror.gcr.io"]' \ 11 | | sudo tee /etc/docker/daemon.json 12 | else 13 | sudo mkdir -vp /etc/docker 14 | echo "/etc/docker/daemon.json now:" 15 | echo '{"experimental":true}' \ 16 | | jq '."registry-mirrors" += ["https://mirror.gcr.io"]' \ 17 | | sudo tee /etc/docker/daemon.json 18 | fi 19 | 20 | if [ -f $HOME/.docker/config.json ]; then 21 | echo "$HOME/.docker/config.json was:" 22 | sed 's/^/> /' $HOME/.docker/config.json 23 | echo "$HOME/.docker/config.json now:" 24 | jq '.+{"experimental":"enabled"}' /etc/docker/daemon.json | tee $HOME/.docker/config.json 25 | else 26 | mkdir -vp $HOME/.docker 27 | echo "$HOME/.docker/config.json now:" 28 | echo '{"experimental":"enabled"}' | tee $HOME/.docker/config.json 29 | fi 30 | 31 | echo ">> updating docker engine" 32 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 33 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 34 | sudo apt-get update 35 | sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce 36 | 37 | echo ">> installing docker-buildx" 38 | mkdir -vp $HOME/.docker/cli-plugins/ 39 | curl --silent -L "https://github.com/docker/buildx/releases/download/v0.5.1/buildx-v0.5.1.linux-amd64" > $HOME/.docker/cli-plugins/docker-buildx 40 | chmod a+x $HOME/.docker/cli-plugins/docker-buildx 41 | -------------------------------------------------------------------------------- /integration/k8s-rbac.yaml: -------------------------------------------------------------------------------- 1 | # Simple RBAC required to enable use of `kubectl port-forward` 2 | # from within a container. 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: default-rbac 7 | subjects: 8 | - kind: ServiceAccount 9 | # Reference to upper's `metadata.name` 10 | name: default 11 | # Reference to upper's `metadata.namespace` 12 | namespace: default 13 | roleRef: 14 | kind: ClusterRole 15 | name: cluster-admin 16 | apiGroup: rbac.authorization.k8s.io 17 | -------------------------------------------------------------------------------- /integration/kubectl/Dockerfile: -------------------------------------------------------------------------------- 1 | # Simple multi-platform image that includes kubectl and curl 2 | FROM --platform=$BUILDPLATFORM curlimages/curl 3 | ARG BUILDPLATFORM 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | # curlimages/curl runs as curl-user and cannot install into /usr/bin 8 | USER root 9 | ADD https://dl.k8s.io/release/v1.20.0/bin/$TARGETOS/$TARGETARCH/kubectl /usr/bin 10 | RUN chmod a+x /usr/bin/kubectl 11 | -------------------------------------------------------------------------------- /integration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: integration 5 | 6 | build: 7 | local: 8 | useBuildkit: true 9 | 10 | deploy: 11 | logs: 12 | prefix: auto 13 | kubectl: 14 | manifests: [] 15 | 16 | profiles: 17 | # integration: set of `skaffold debug`-like integration tests 18 | - name: integration 19 | build: 20 | artifacts: 21 | - image: kubectl 22 | context: kubectl 23 | deploy: 24 | kubectl: 25 | manifests: 26 | - k8s-rbac.yaml 27 | 28 | -------------------------------------------------------------------------------- /netcore/helper-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM curlimages/curl as netcore 2 | ARG BUILDPLATFORM 3 | ARG TARGETPLATFORM 4 | # assume glibc; RuntimeIDs gleaned from the getvsdbgsh script 5 | RUN RuntimeID=$(case "$TARGETPLATFORM" in linux/amd64) echo linux-x64;; linux/arm64) echo linux-arm64;; *) exit 1;; esac); \ 6 | mkdir $HOME/vsdbg && curl -sSL https://aka.ms/getvsdbgsh | sh /dev/stdin -v latest -l $HOME/vsdbg -r $RuntimeID 7 | 8 | # Now populate the duct-tape image with the language runtime debugging support files 9 | # The debian image is about 95MB bigger 10 | FROM --platform=$TARGETPLATFORM busybox 11 | ARG TARGETPLATFORM 12 | 13 | # The install script copies all files in /duct-tape to /dbg 14 | COPY install.sh / 15 | CMD ["/bin/sh", "/install.sh"] 16 | WORKDIR /duct-tape 17 | COPY --from=netcore /home/curl_user/vsdbg/ netcore/ 18 | -------------------------------------------------------------------------------- /netcore/helper-image/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -d /dbg ]; then 5 | echo "Error: installation requires a volume mount at /dbg" 1>&2 6 | exit 1 7 | fi 8 | 9 | echo "Installing runtime debugging support files in /dbg" 10 | tar cf - -C /duct-tape . | tar xf - -C /dbg 11 | echo "Installation complete" 12 | -------------------------------------------------------------------------------- /netcore/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: netcore 5 | 6 | requires: 7 | - path: ../integration 8 | activeProfiles: 9 | - name: integration 10 | activatedBy: [integration] 11 | 12 | build: 13 | local: 14 | useBuildkit: true 15 | artifacts: 16 | - image: skaffold-debug-netcore 17 | context: helper-image 18 | custom: 19 | buildCommand: ../../hack/buildx.sh 20 | 21 | test: 22 | - image: skaffold-debug-netcore 23 | structureTests: [structure-tests-netcore.yaml] 24 | 25 | deploy: 26 | logs: 27 | prefix: auto 28 | kubectl: 29 | manifests: [] 30 | 31 | profiles: 32 | 33 | # local: never push to remote registries 34 | - name: local 35 | build: 36 | local: 37 | push: false 38 | 39 | # integration: set of `skaffold debug`-like integration tests 40 | - name: integration 41 | deploy: 42 | kubectl: 43 | manifests: 44 | - test/k8s-test-netcore.yaml 45 | 46 | # release: pushes images to production with :latest 47 | - name: release 48 | build: 49 | local: 50 | push: true 51 | tagPolicy: 52 | sha256: {} 53 | 54 | # deprecated-names: use short (deprecated) image names: images were 55 | # prefixed with `skaffold-debug-` so they were more easily distinguished 56 | # from other images with similar names. 57 | - name: deprecated-names 58 | patches: 59 | - op: replace 60 | path: /build/artifacts/0/image 61 | from: skaffold-debug-netcore 62 | value: netcore 63 | - op: replace 64 | path: /test/0/image 65 | from: skaffold-debug-netcore 66 | value: netcore 67 | -------------------------------------------------------------------------------- /netcore/structure-tests-netcore.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: 2.0.0 2 | 3 | fileExistenceTests: 4 | - name: 'vsdbg for .net core' 5 | path: '/duct-tape/netcore/vsdbg' 6 | 7 | commandTests: 8 | - name: "run with no /dbg should fail" 9 | command: "sh" 10 | args: ["/install.sh"] 11 | expectedError: ["Error: installation requires a volume mount at /dbg"] 12 | exitCode: 1 13 | - name: "run with /dbg should install" 14 | setup: [["mkdir", "/dbg"]] 15 | command: "sh" 16 | args: ["/install.sh"] 17 | expectedOutput: ["Installing runtime debugging support files in /dbg", "Installation complete"] 18 | exitCode: 0 19 | -------------------------------------------------------------------------------- /netcore/test/k8s-test-netcore.yaml: -------------------------------------------------------------------------------- 1 | # vsdbg is normally invoked via `kubectl exec` so there are no sockets 2 | # to test. This test instead executes `vsdbg`, which speaks debug 3 | # adapter protocol on stdin/stdout, and verifies that it was able to 4 | # launch with a "disconnect" 5 | apiVersion: batch/v1 6 | kind: Job 7 | metadata: 8 | name: netcore-vsdbg-runs 9 | labels: 10 | project: container-debug-support 11 | type: integration-test 12 | spec: 13 | ttlSecondsAfterFinished: 10 14 | backoffLimit: 1 15 | template: 16 | spec: 17 | restartPolicy: Never 18 | initContainers: 19 | - image: skaffold-debug-netcore 20 | name: install-netcore-support 21 | resources: {} 22 | volumeMounts: 23 | - mountPath: /dbg 24 | name: netcore-debugging-support 25 | containers: 26 | - name: netcore-vsdbg 27 | image: ubuntu 28 | args: 29 | - sh 30 | - -c 31 | - | 32 | printf 'Content-Length: 26\r\n\r\n{"command":"disconnect"}\r\n' | /dbg/netcore/vsdbg >/tmp/out 33 | if egrep -q '("success":true.*"command":"disconnect"|"command":"disconnect".*"success":true)' /tmp/out; then 34 | echo "Successfully started vsdbg" 35 | else 36 | echo "ERROR: unable to launch vsdbg" 37 | cat /tmp/out 38 | exit 1 39 | fi 40 | volumeMounts: 41 | - mountPath: /dbg 42 | name: netcore-debugging-support 43 | volumes: 44 | - emptyDir: {} 45 | name: netcore-debugging-support 46 | 47 | -------------------------------------------------------------------------------- /nodejs/helper-image/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOVERSION=1.23 2 | FROM --platform=$BUILDPLATFORM golang:${GOVERSION} as build 3 | ARG BUILDPLATFORM 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | COPY . . 8 | # Produce an as-static-as-possible dlv binary to work on musl and glibc 9 | RUN GOPATH="" CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o node -ldflags '-s -w -extldflags "-static"' wrapper.go 10 | 11 | # Now populate the duct-tape image with the language runtime debugging support files 12 | # The debian image is about 95MB bigger 13 | FROM busybox 14 | # The install script copies all files in /duct-tape to /dbg 15 | COPY install.sh / 16 | CMD ["/bin/sh", "/install.sh"] 17 | WORKDIR /duct-tape 18 | COPY --from=build /go/node nodejs/bin/ 19 | -------------------------------------------------------------------------------- /nodejs/helper-image/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleContainerTools/container-debug-support/nodejs 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/stretchr/objx v0.1.1 // indirect 10 | golang.org/x/sys v0.10.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /nodejs/helper-image/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 4 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 6 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 10 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 11 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 12 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 16 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 18 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 21 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /nodejs/helper-image/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -d /dbg ]; then 5 | echo "Error: installation requires a volume mount at /dbg" 1>&2 6 | exit 1 7 | fi 8 | 9 | echo "Installing runtime debugging support files in /dbg" 10 | tar cf - -C /duct-tape . | tar xf - -C /dbg 11 | echo "Installation complete" 12 | -------------------------------------------------------------------------------- /nodejs/helper-image/wrapper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // A wrapper for node executables to support debugging of application scripts. 18 | // Many NodeJS applications use NodeJS-based launch tools (e.g., npm, 19 | // nodemon), and often use several in combination. This makes it very 20 | // difficult to start debugging the application as `--inspect`s are usually 21 | // intercepted by one of the launch tools. When executing a `node_modules` 22 | // script, this wrapper strips out and propagates `--inspect`-like arguments 23 | // via `NODE_DEBUG`. When executing an app script, this wrapper then inlines 24 | // the `NODE_DEBUG` when found. 25 | // 26 | // A certain set of node_modules scripts are treated as if they are application scripts. 27 | // The WRAPPER_ALLOWED environment variable allows identifying node_modules scripts 28 | // that should be treated as application scripts, meaning that they load and execute 29 | // the user's scripts directly. 30 | package main 31 | 32 | import ( 33 | "context" 34 | "fmt" 35 | "io" 36 | "os" 37 | "os/exec" 38 | "path/filepath" 39 | "strings" 40 | 41 | shell "github.com/kballard/go-shellquote" 42 | "github.com/sirupsen/logrus" 43 | ) 44 | 45 | // the next.js launcher loads user scripts directly 46 | var allowedNodeModules = []string{"node_modules/.bin/next"} 47 | 48 | // nodeContext allows manipulating the launch context for node. 49 | type nodeContext struct { 50 | program string 51 | args []string 52 | env map[string]string 53 | } 54 | 55 | func main() { 56 | env := envToMap(os.Environ()) 57 | logrus.SetLevel(logrusLevel(env)) 58 | 59 | logrus.Debugln("Launched: ", os.Args) 60 | 61 | // suppress npm warnings when node on PATH isn't the node used for npm 62 | env["npm_config_scripts_prepend_node_path"] = "false" 63 | nc := nodeContext{program: os.Args[0], args: os.Args[1:], env: env} 64 | if err := run(&nc, os.Stdin, os.Stdout, os.Stderr); err != nil { 65 | logrus.Fatal(err) 66 | } 67 | } 68 | 69 | func isEnabled(env map[string]string) bool { 70 | v, found := env["WRAPPER_ENABLED"] 71 | return !found || (v != "0" && v != "false" && v != "no") 72 | } 73 | 74 | func logrusLevel(env map[string]string) logrus.Level { 75 | v := env["WRAPPER_VERBOSE"] 76 | if v != "" { 77 | if l, err := logrus.ParseLevel(v); err == nil { 78 | return l 79 | } 80 | logrus.Warnln("Unknown logging level: WRAPPER_VERBOSE=", v) 81 | } 82 | return logrus.WarnLevel 83 | } 84 | 85 | func run(nc *nodeContext, stdin io.Reader, stdout, stderr io.Writer) error { 86 | if err := nc.unwrap(); err != nil { 87 | return fmt.Errorf("could not unwrap: %w", err) 88 | } 89 | logrus.Debugln("unwrapped: ", nc.program) 90 | 91 | if !isEnabled(nc.env) { 92 | logrus.Info("wrapper disabled") 93 | return nc.exec(stdin, stdout, stderr) 94 | } 95 | 96 | // script may be "" such as when the script is piped in through stdin 97 | script := findScript(nc.args) 98 | if script != "" { 99 | // Use an absolute path in case we're being run within a node_modules directory 100 | // If there's an error, then hand off immediately to the real node. 101 | if abs, err := filepath.Abs(script); err == nil { 102 | script = abs 103 | } else { 104 | logrus.Warn("could not access script: ", err) 105 | return nc.exec(stdin, stdout, stderr) 106 | } 107 | } 108 | logrus.Debugln("script: ", script) 109 | 110 | // If NODE_DEBUG is set then our parent process was this wrapper, and 111 | // NODE_DEBUG contains the --inspect* argument provided back then. 112 | nodeDebugOption, hasNodeDebug := nc.env["NODE_DEBUG"] 113 | if hasNodeDebug { 114 | logrus.Debugln("found NODE_DEBUG=", nodeDebugOption) 115 | } 116 | 117 | // If we're about to execute the application script, install the NODE_DEBUG 118 | // arguments if found and go 119 | if script == "" || isApplicationScript(script) || isAllowedNodeModule(script, nc.env) { 120 | if hasNodeDebug { 121 | nc.stripInspectArgs() // top-level debug options win 122 | nc.addNodeArg(nodeDebugOption) 123 | delete(nc.env, "NODE_DEBUG") 124 | } 125 | return nc.exec(stdin, stdout, stderr) 126 | } 127 | 128 | // We're executing a node module: strip any --inspect args and propagate 129 | inspectArg := nc.stripInspectArgs() 130 | if inspectArg != "" { 131 | logrus.Debugf("Stripped %q as not an app script", inspectArg) 132 | if !hasNodeDebug { 133 | logrus.Debugln("Setting NODE_DEBUG=", inspectArg) 134 | nc.env["NODE_DEBUG"] = inspectArg 135 | } 136 | } 137 | 138 | // nodemon needs special handling as `nodemon --inspect` will use spawn to invoke a 139 | // child node, which picks up this wrapped node. Otherwise nodemon uses fork to launch 140 | // the actual application script file directly, which circumvents the use of this node wrapper. 141 | nc.handleNodemon() 142 | 143 | return nc.exec(stdin, stdout, stderr) 144 | } 145 | 146 | // unwrap looks for the real node executable (not this wrapper). 147 | func (nc *nodeContext) unwrap() error { 148 | if nc == nil { 149 | return fmt.Errorf("nil context") 150 | } 151 | 152 | // Here we try to find the original program. When a program is 153 | // resolved from the PATH, most shells will set argv[0] to the 154 | // command and so it won't appear to exist and so the first file 155 | // resolved in the PATH should be this program. 156 | origInfo, err := os.Stat(nc.program) 157 | origFound := err == nil 158 | if err != nil && !os.IsNotExist(err) { 159 | return fmt.Errorf("unable to stat %q: %v", nc.program, err) 160 | } 161 | 162 | path := nc.env["PATH"] 163 | base := filepath.Base(nc.program) 164 | for _, dir := range strings.Split(path, string(os.PathListSeparator)) { 165 | p := filepath.Join(dir, base) 166 | if pInfo, err := os.Stat(p); err == nil { 167 | if !origFound { 168 | // the original nc.program was not resolved, meaning this 169 | // it had been resolved in the PATH, so treat this first 170 | // instance as the original file and continue searching 171 | logrus.Debugln("unwrap: presumed wrapper at ", p) 172 | origInfo = pInfo 173 | origFound = true 174 | } else if !os.SameFile(origInfo, pInfo) { 175 | logrus.Debugf("unwrap: replacing %s -> %s", nc.program, p) 176 | nc.program = p 177 | return nil 178 | } 179 | } 180 | } 181 | return fmt.Errorf("could not find %q in PATH", base) 182 | } 183 | 184 | // stripInspectArgs removes all `--inspect*` args from both the command-line and from 185 | // NODE_OPTIONS. It returns the last inspect arg or "" if there were no inspect arguments. 186 | func (nc *nodeContext) stripInspectArgs() string { 187 | foundOption := "" 188 | if options, found := nc.env["NODE_OPTIONS"]; found { 189 | if args, err := shell.Split(options); err != nil { 190 | logrus.Warnf("NODE_OPTIONS cannot be split: %v", err) 191 | } else { 192 | args, inspectArg := stripInspectArg(args) 193 | if inspectArg != "" { 194 | logrus.Debugf("Found %q in NODE_OPTIONS", inspectArg) 195 | nc.env["NODE_OPTIONS"] = shell.Join(args...) 196 | foundOption = inspectArg 197 | } 198 | } 199 | } 200 | strippedArgs, inspectArg := stripInspectArg(nc.args) 201 | if inspectArg != "" { 202 | logrus.Debugf("Found %q in command-line", inspectArg) 203 | nc.args = strippedArgs 204 | foundOption = inspectArg 205 | } 206 | return foundOption 207 | } 208 | 209 | func (nc *nodeContext) handleNodemon() { 210 | if nodeDebug, found := nc.env["NODE_DEBUG"]; found { 211 | // look for the nodemon script (if it appears) and insert the --inspect argument 212 | for i, arg := range nc.args { 213 | if len(arg) > 0 && arg[0] != '-' && strings.Contains(arg, "/nodemon") { 214 | nc.args = append(nc.args, "") 215 | copy(nc.args[i+2:], nc.args[i+1:]) 216 | nc.args[i+1] = nodeDebug 217 | delete(nc.env, "NODE_DEBUG") 218 | logrus.Debugf("special handling for nodemon: %q", nc.args) 219 | return 220 | } 221 | } 222 | } 223 | } 224 | 225 | func (nc *nodeContext) addNodeArg(nodeArg string) { 226 | // find the script location and insert the provided argument 227 | for i, arg := range nc.args { 228 | if len(arg) > 0 && arg[0] != '-' { 229 | nc.args = append(nc.args, "") 230 | copy(nc.args[i+1:], nc.args[i:]) 231 | nc.args[i] = nodeArg 232 | logrus.Debugf("added node arg: %q", nc.args) 233 | return 234 | } 235 | } 236 | // script not found so add at end 237 | nc.args = append(nc.args, nodeArg) 238 | } 239 | 240 | // exec runs the command, and returns an error should one occur. 241 | func (nc *nodeContext) exec(in io.Reader, out, err io.Writer) error { 242 | logrus.Debugf("exec: %s %v (env: %v)", nc.program, nc.args, nc.env) 243 | cmd := exec.CommandContext(context.Background(), nc.program, nc.args...) 244 | cmd.Env = envFromMap(nc.env) 245 | cmd.Stdin = in 246 | cmd.Stdout = out 247 | cmd.Stderr = err 248 | return cmd.Run() 249 | } 250 | 251 | // findScript returns the path to the node script that will be executed. 252 | // Returns an empty string if no script was found. 253 | func findScript(args []string) string { 254 | // a bit of a hack, but all node options are of the form `--arg=option` 255 | for _, arg := range args { 256 | if len(arg) > 0 && arg[0] != '-' { 257 | return arg 258 | } 259 | } 260 | return "" 261 | } 262 | 263 | // isApplicationScript return true if the script appears to be an application 264 | // script, or false if a library (node_modules) script or `npm` (special case). 265 | func isApplicationScript(path string) bool { 266 | // We could consider checking if the parent's base name is `bin`? 267 | return !strings.HasPrefix(path, "node_modules/") && !strings.Contains(path, "/node_modules/") && 268 | !strings.HasSuffix(path, "/bin/npm") 269 | } 270 | 271 | // isAllowedNodeModule returns true if the script is an allowed node_module, meaning 272 | // one that is or directly launches the user's code. 273 | func isAllowedNodeModule(path string, env map[string]string) bool { 274 | allowedList := allowedNodeModules 275 | if v, found := env["WRAPPER_ALLOWED"]; found { 276 | split := strings.Split(v, " ") 277 | allowedList = append(allowedList, split...) 278 | } 279 | for _, allowed := range allowedList { 280 | if strings.HasSuffix(path, allowed) { 281 | logrus.Infof("script %q matches %q from allowed node_modules", path, allowed) 282 | return true 283 | } 284 | } 285 | return false 286 | } 287 | 288 | // envToMap turns a set of VAR=VALUE strings to a map. 289 | func envToMap(entries []string) map[string]string { 290 | m := make(map[string]string) 291 | for _, entry := range entries { 292 | kv := strings.SplitN(entry, "=", 2) 293 | m[kv[0]] = kv[1] 294 | } 295 | return m 296 | } 297 | 298 | // envToMap turns a map of variable:value pairs into a set of VAR=VALUE strings. 299 | func envFromMap(env map[string]string) []string { 300 | var m []string 301 | for k, v := range env { 302 | m = append(m, k+"="+v) 303 | } 304 | return m 305 | } 306 | 307 | // stripInspectArg searches and removes all node `--inspect` style arguments, returning the 308 | // altered arguments and the inspect argument. 309 | func stripInspectArg(args []string) ([]string, string) { 310 | // inspect directives are always a single argument: `node --inspect 9226` causes node to load 9226 as a file 311 | var newArgs []string 312 | inspectArg := "" // default case: no inspect arg found 313 | 314 | for i, arg := range args { 315 | if strings.HasPrefix(arg, "--inspect") { 316 | // todo: we should coalesce --inspect-port=xxx 317 | inspectArg = arg 318 | continue 319 | } 320 | 321 | // if at end of node options, copy remaining arguments 322 | // "--" marks end of node options 323 | if arg == "--" || len(arg) == 0 || arg[0] != '-' { 324 | newArgs = append(newArgs, args[i:]...) 325 | break 326 | } 327 | newArgs = append(newArgs, arg) 328 | } 329 | return newArgs, inspectArg 330 | } 331 | -------------------------------------------------------------------------------- /nodejs/helper-image/wrapper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "reflect" 26 | "runtime" 27 | "sort" 28 | "strings" 29 | "testing" 30 | ) 31 | 32 | func TestIsEnabled(t *testing.T) { 33 | tests := []struct { 34 | env map[string]string 35 | expected bool 36 | }{ 37 | { 38 | env: nil, 39 | expected: true, 40 | }, 41 | { 42 | env: map[string]string{"WRAPPER_ENABLED": "1"}, 43 | expected: true, 44 | }, 45 | { 46 | env: map[string]string{"WRAPPER_ENABLED": "true"}, 47 | expected: true, 48 | }, 49 | { 50 | env: map[string]string{"WRAPPER_ENABLED": "yes"}, 51 | expected: true, 52 | }, 53 | { 54 | env: map[string]string{"WRAPPER_ENABLED": ""}, 55 | expected: true, 56 | }, 57 | { 58 | env: map[string]string{"WRAPPER_ENABLED": "0"}, 59 | expected: false, 60 | }, 61 | { 62 | env: map[string]string{"WRAPPER_ENABLED": "no"}, 63 | expected: false, 64 | }, 65 | { 66 | env: map[string]string{"WRAPPER_ENABLED": "false"}, 67 | expected: false, 68 | }, 69 | } 70 | for _, test := range tests { 71 | t.Run(fmt.Sprintf("env: %v", test.env), func(t *testing.T) { 72 | result := isEnabled(test.env) 73 | if test.expected != result { 74 | t.Errorf("expected %v but got %v", test.expected, result) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestFindScript(t *testing.T) { 81 | tests := []struct { 82 | description string 83 | args []string 84 | expected string 85 | }{ 86 | { 87 | description: "no args", 88 | args: []string{}, 89 | expected: "", 90 | }, 91 | { 92 | description: "single script", 93 | args: []string{"index.js"}, 94 | expected: "index.js", 95 | }, 96 | { 97 | description: "options but no script", 98 | args: []string{"-i", "--help"}, 99 | expected: "", 100 | }, 101 | { 102 | description: "options and script", 103 | args: []string{"-i", "--help", "index.js"}, 104 | expected: "index.js", 105 | }, 106 | { 107 | description: "options, script, and arguments", 108 | args: []string{"-i", "--help", "index.js", "arg1", "arg2"}, 109 | expected: "index.js", 110 | }, 111 | { 112 | description: "options, script path, and arguments", 113 | args: []string{"-i", "--help", "node_modules/index.js", "arg1", "arg2"}, 114 | expected: "node_modules/index.js", 115 | }, 116 | } 117 | 118 | for _, test := range tests { 119 | t.Run(test.description, func(t *testing.T) { 120 | result := findScript(test.args) 121 | if result != test.expected { 122 | t.Errorf("expected %s but got %s", test.expected, result) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestIsApplicationScript(t *testing.T) { 129 | tests := []struct { 130 | script string 131 | expected bool 132 | }{ 133 | {"index.js", true}, 134 | {"/usr/local/bin/npm", false}, 135 | {"node_modules/nodemon/nodemon.js", false}, 136 | {"lib/node_modules/nodemon/nodemon.js", false}, 137 | } 138 | 139 | for _, test := range tests { 140 | t.Run(test.script, func(t *testing.T) { 141 | result := isApplicationScript(test.script) 142 | if result != test.expected { 143 | t.Errorf("expected %v but got %v", test.expected, result) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestIsAllowedNodeModule(t *testing.T) { 150 | tests := []struct { 151 | script string 152 | env map[string]string 153 | expected bool 154 | }{ 155 | {"./node_modules/lib/script.js", nil, false}, 156 | {"./node_modules/.bin/next", nil, true}, 157 | {"node_modules/nodemon/nodemon.js", nil, false}, 158 | {"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "foo bar"}, false}, 159 | {"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "nodemon/nodemon.js"}, true}, 160 | {"node_modules/nodemon/nodemon.js", map[string]string{"WRAPPER_ALLOWED": "foo nodemon/nodemon.js bar"}, true}, 161 | } 162 | 163 | for _, test := range tests { 164 | t.Run(test.script, func(t *testing.T) { 165 | result := isAllowedNodeModule(test.script, test.env) 166 | if result != test.expected { 167 | t.Errorf("expected %v but got %v", test.expected, result) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func TestEnvFromMap(t *testing.T) { 174 | tests := []struct { 175 | description string 176 | env map[string]string 177 | expected []string 178 | }{ 179 | {"nil", nil, nil}, 180 | {"empty", map[string]string{}, nil}, 181 | {"single", map[string]string{"a": "b"}, []string{"a=b"}}, 182 | {"multiple", map[string]string{"a": "b", "c": "d"}, []string{"a=b", "c=d"}}, 183 | } 184 | 185 | for _, test := range tests { 186 | t.Run(test.description, func(t *testing.T) { 187 | result := envFromMap(test.env) 188 | sort.Strings(result) 189 | if len(result) != len(test.expected) { 190 | t.Errorf("expected %v but got %v", test.expected, result) 191 | } else { 192 | for i := 0; i < len(result); i++ { 193 | if result[i] != test.expected[i] { 194 | t.Errorf("expected %v but got %v", test.expected[i], result[i]) 195 | } 196 | } 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestEnvToMap(t *testing.T) { 203 | tests := []struct { 204 | description string 205 | env []string 206 | expected map[string]string 207 | }{ 208 | {"nil", nil, nil}, 209 | {"empty", []string{}, nil}, 210 | {"single", []string{"a=b"}, map[string]string{"a": "b"}}, 211 | {"multiple", []string{"a=b", "c=d"}, map[string]string{"a": "b", "c": "d"}}, 212 | {"collisions", []string{"a=b", "a=d"}, map[string]string{"a": "d"}}, 213 | } 214 | 215 | for _, test := range tests { 216 | t.Run(test.description, func(t *testing.T) { 217 | result := envToMap(test.env) 218 | if len(result) != len(test.expected) { 219 | t.Errorf("expected %v but got %v", test.expected, result) 220 | } else { 221 | for k, v := range result { 222 | if v != test.expected[k] { 223 | t.Errorf("for %v expected %v but got %v", k, test.expected[k], v) 224 | } 225 | } 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func TestStripInspectArg(t *testing.T) { 232 | tests := []struct { 233 | description string 234 | args []string 235 | newArgs []string 236 | inspectArg string 237 | }{ 238 | {"nil", nil, nil, ""}, 239 | {"no args", []string{}, []string{}, ""}, 240 | {"no inspect args", []string{"--foo", "bar"}, []string{"--foo", "bar"}, ""}, 241 | {"lone <<>> removed", []string{"<<>>"}, []string{}, "<<>>"}, 242 | {"<<>> at beginning removed", []string{"<<>>", "--foo", "bar"}, []string{"--foo", "bar"}, "<<>>"}, 243 | {"<<>> mid way removed", []string{"-c", "<<>>", "--foo", "bar"}, []string{"-c", "--foo", "bar"}, "<<>>"}, 244 | {"<<>> after script untouched", []string{"--foo", "bar", "<<>>"}, []string{"--foo", "bar", "<<>>"}, ""}, 245 | } 246 | 247 | for _, test := range tests { 248 | // run the test for the difference inspect variants 249 | for _, inspect := range []string{"--inspect", "--inspect=9224", "--inspect-brk", "--inspect-brk=3452"} { 250 | test.description = strings.ReplaceAll(test.description, "<<>>", inspect) 251 | for i := range test.args { 252 | if test.args[i] == "<<>>" { 253 | test.args[i] = inspect 254 | } 255 | } 256 | for i := range test.newArgs { 257 | if test.newArgs[i] == "<<>>" { 258 | test.newArgs[i] = inspect 259 | } 260 | } 261 | if test.inspectArg == "<<>>" { 262 | test.inspectArg = inspect 263 | } 264 | } 265 | t.Run(test.description, func(t *testing.T) { 266 | newArgs, inspectArg := stripInspectArg(test.args) 267 | if len(newArgs) != len(test.newArgs) { 268 | t.Errorf("expected %v but got %v", test.newArgs, newArgs) 269 | } else { 270 | for i := 0; i < len(newArgs); i++ { 271 | if newArgs[i] != test.newArgs[i] { 272 | t.Errorf("expected %v but got %v", test.newArgs[i], newArgs[i]) 273 | } 274 | } 275 | } 276 | if inspectArg != test.inspectArg { 277 | t.Errorf("expected %v but got %v", test.inspectArg, inspectArg) 278 | } 279 | }) 280 | } 281 | } 282 | 283 | func TestNodeContext_unwrap(t *testing.T) { 284 | name := "foo" // ensure no code explicitly looks for "node" 285 | 286 | root, err := ioutil.TempDir("", "nc") 287 | if err != nil { 288 | t.Error(err) 289 | } 290 | originalNode := filepath.Join(root, name) 291 | if err := ioutil.WriteFile(originalNode, []byte{}, 0555); err != nil { 292 | t.Error(err) 293 | } 294 | binPath := filepath.Join(root, "bin") 295 | if err := os.Mkdir(binPath, 0777); err != nil { 296 | t.Error(err) 297 | } 298 | binNode := filepath.Join(binPath, name) 299 | if err := ioutil.WriteFile(binNode, []byte{}, 0555); err != nil { 300 | t.Error(err) 301 | } 302 | sbinPath := filepath.Join(root, "sbin") 303 | if err := os.Mkdir(sbinPath, 0777); err != nil { 304 | t.Error(err) 305 | } 306 | sbinNode := filepath.Join(sbinPath, name) 307 | if err := ioutil.WriteFile(sbinNode, []byte{}, 0555); err != nil { 308 | t.Error(err) 309 | } 310 | 311 | t.Cleanup(func() { os.RemoveAll(root) }) 312 | 313 | tests := []struct { 314 | description string 315 | input nodeContext 316 | noErr bool 317 | expected string 318 | }{ 319 | { 320 | description: "no PATH leaves unchanged", 321 | input: nodeContext{program: originalNode}, 322 | noErr: false, 323 | expected: originalNode, 324 | }, 325 | { 326 | description: "empty PATH leaves unchanged", 327 | input: nodeContext{program: originalNode, env: map[string]string{"PATH": ""}}, 328 | noErr: false, 329 | expected: originalNode, 330 | }, 331 | { 332 | description: "no other node leaves unchanged", 333 | input: nodeContext{program: originalNode, env: map[string]string{"PATH": root}}, 334 | noErr: false, 335 | expected: originalNode, 336 | }, 337 | { 338 | description: "first other node wins", 339 | input: nodeContext{program: originalNode, env: map[string]string{"PATH": root + string(os.PathListSeparator) + binPath + string(os.PathListSeparator) + sbinPath}}, 340 | noErr: true, 341 | expected: binNode, 342 | }, 343 | { 344 | description: "first node wins when original not found", 345 | input: nodeContext{program: name, env: map[string]string{"PATH": root + string(os.PathListSeparator) + binPath + string(os.PathListSeparator) + sbinPath}}, 346 | noErr: true, 347 | expected: binNode, 348 | }, 349 | } 350 | 351 | for _, test := range tests { 352 | t.Run(test.description, func(t *testing.T) { 353 | copy := test.input 354 | err := copy.unwrap() 355 | noErr := err == nil 356 | if noErr != test.noErr { 357 | t.Errorf("unexpected unwrap() error return: %q", err) 358 | } 359 | if copy.program != test.expected { 360 | t.Errorf("expected %v but got %v", test.expected, copy.program) 361 | } 362 | }) 363 | } 364 | } 365 | 366 | func TestNodeContext_StripInspectArgs(t *testing.T) { 367 | tests := []struct { 368 | description string 369 | input nodeContext 370 | expected nodeContext 371 | arg string 372 | }{ 373 | { 374 | description: "no inspect", 375 | input: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io"}}, 376 | expected: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io"}}, 377 | arg: "", 378 | }, 379 | { 380 | description: "inspect in args", 381 | input: nodeContext{args: []string{"--inspect", "--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io"}}, 382 | expected: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io"}}, 383 | arg: "--inspect", 384 | }, 385 | { 386 | description: "inspect in NODE_OPTIONS", 387 | input: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io --inspect-brk"}}, 388 | expected: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_OPTIONS": "--trace-sync-io"}}, 389 | arg: "--inspect-brk", 390 | }, 391 | } 392 | 393 | for _, test := range tests { 394 | t.Run(test.description, func(t *testing.T) { 395 | copy := test.input 396 | arg := copy.stripInspectArgs() 397 | if arg != test.arg { 398 | t.Errorf("expected inspect args = %v but got %v", test.arg, arg) 399 | } 400 | if !reflect.DeepEqual(copy, test.expected) { 401 | t.Errorf("expected %v but got %v", test.expected, copy) 402 | } 403 | }) 404 | } 405 | } 406 | 407 | func TestNodeContext_HandleNodemon(t *testing.T) { 408 | tests := []struct { 409 | description string 410 | input nodeContext 411 | expected nodeContext 412 | }{ 413 | { 414 | description: "no nodemon", 415 | input: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_DEBUG": "--inspect=3333"}}, 416 | expected: nodeContext{args: []string{"--no-warnings", "index.js"}, env: map[string]string{"NODE_DEBUG": "--inspect=3333"}}, 417 | }, 418 | { 419 | description: "nodemon no args", 420 | input: nodeContext{args: []string{"--no-warnings", "./node_modules/nodemon/bin/nodemon.js"}, env: map[string]string{"NODE_DEBUG": "--inspect=3333"}}, 421 | expected: nodeContext{args: []string{"--no-warnings", "./node_modules/nodemon/bin/nodemon.js", "--inspect=3333"}, env: map[string]string{}}, 422 | }, 423 | { 424 | description: "nodemon with args", 425 | input: nodeContext{args: []string{"--no-warnings", "./node_modules/nodemon/bin/nodemon.js", "-v", "index.js"}, env: map[string]string{"NODE_DEBUG": "--inspect=3333"}}, 426 | expected: nodeContext{args: []string{"--no-warnings", "./node_modules/nodemon/bin/nodemon.js", "--inspect=3333", "-v", "index.js"}, env: map[string]string{}}, 427 | }, 428 | } 429 | 430 | for _, test := range tests { 431 | t.Run(test.description, func(t *testing.T) { 432 | copy := test.input 433 | copy.handleNodemon() 434 | if !reflect.DeepEqual(copy, test.expected) { 435 | t.Errorf("mismatch\nexpected: %v\n but got: %v", test.expected, copy) 436 | } 437 | }) 438 | } 439 | } 440 | 441 | func TestNodeContext_AddNodeArg(t *testing.T) { 442 | tests := []struct { 443 | description string 444 | input nodeContext 445 | arg string 446 | expected nodeContext 447 | }{ 448 | { 449 | description: "nil args", 450 | input: nodeContext{}, 451 | arg: "abc", 452 | expected: nodeContext{args: []string{"abc"}}, 453 | }, 454 | { 455 | description: "no script", 456 | input: nodeContext{args: []string{"--no-warnings"}}, 457 | arg: "abc", 458 | expected: nodeContext{args: []string{"--no-warnings", "abc"}}, 459 | }, 460 | { 461 | description: "after options before script", 462 | input: nodeContext{args: []string{"--no-warnings", "index.js"}}, 463 | arg: "abc", 464 | expected: nodeContext{args: []string{"--no-warnings", "abc", "index.js"}}, 465 | }, 466 | } 467 | 468 | for _, test := range tests { 469 | t.Run(test.description, func(t *testing.T) { 470 | copy := test.input 471 | copy.addNodeArg(test.arg) 472 | if !reflect.DeepEqual(copy, test.expected) { 473 | t.Errorf("expected %v but got %v", test.expected, copy) 474 | } 475 | }) 476 | } 477 | } 478 | 479 | func TestIntegration(t *testing.T) { 480 | if runtime.GOOS == "windows" { 481 | t.Skip("we only support nix") 482 | } 483 | // Setup: create two directories with nodeBin. The first directory's 484 | // nodeBin should never be invoked. 485 | root, err := ioutil.TempDir("", "node") 486 | if err != nil { 487 | t.Error(err) 488 | } 489 | firstNodeDir := filepath.Join(root, "first") 490 | if err := os.Mkdir(firstNodeDir, 0777); err != nil { 491 | t.Error(err) 492 | } 493 | firstNode := filepath.Join(firstNodeDir, "nodeBin") 494 | if err := ioutil.WriteFile(firstNode, []byte{}, 0555); err != nil { 495 | t.Errorf("could not create node script: %v", err) 496 | } 497 | 498 | actualNodeDir := filepath.Join(root, "actual") 499 | if err := os.Mkdir(actualNodeDir, 0777); err != nil { 500 | t.Error(err) 501 | } 502 | actualNode := filepath.Join(actualNodeDir, "nodeBin") 503 | 504 | script := `#!/bin/sh 505 | if [ -n "$NODE_DEBUG" ]; then 506 | echo "NODE_DEBUG=$NODE_DEBUG" 507 | fi 508 | if [ -n "$NODE_OPTIONS" ]; then 509 | echo "NODE_OPTIONS=$NODE_OPTIONS" 510 | fi 511 | for arg in "$@"; do 512 | echo "$arg" 513 | done 514 | ` 515 | if err := ioutil.WriteFile(actualNode, []byte(script), 0555); err != nil { 516 | t.Errorf("could not create node script: %v", err) 517 | } 518 | t.Cleanup(func() { os.RemoveAll(root) }) 519 | 520 | tests := []struct { 521 | description string 522 | args []string 523 | env map[string]string 524 | expected string 525 | }{ 526 | // app scripts are terminal: commands should only affected if NODE_DEBUG is defined 527 | { 528 | description: "app script: passed through", 529 | args: []string{"script.js"}, 530 | expected: "script.js\n", 531 | }, 532 | { 533 | description: "app script: inspect arg passed through", 534 | args: []string{"--inspect", "script.js"}, 535 | expected: "--inspect\nscript.js\n", 536 | }, 537 | { 538 | description: "app script: inspect as app args left alone", 539 | args: []string{"script.js", "--inspect"}, 540 | expected: "script.js\n--inspect\n", 541 | }, 542 | { 543 | description: "app script with NODE_OPTIONS='--inspect': passed through", 544 | args: []string{"script.js"}, 545 | env: map[string]string{"NODE_OPTIONS": "--inspect"}, 546 | expected: "NODE_OPTIONS=--inspect\nscript.js\n", 547 | }, 548 | { 549 | description: "app script with NODE_OPTIONS='--foo --inspect --bar': passed through", 550 | args: []string{"script.js"}, 551 | env: map[string]string{"NODE_OPTIONS": "--foo --inspect --bar"}, 552 | expected: "NODE_OPTIONS=--foo --inspect --bar\nscript.js\n", 553 | }, 554 | { 555 | description: "app script with NODE_DEBUG='--inspect': installed", 556 | args: []string{"script.js"}, 557 | env: map[string]string{"NODE_DEBUG": "--inspect"}, 558 | expected: "--inspect\nscript.js\n", 559 | }, 560 | 561 | // node_module scripts should have --inspect stripped and propagated, 562 | // and NODE_DEBUG should never be overwritten, EXCEPT for allowed modules 563 | { 564 | description: "node_modules script: passed through", 565 | args: []string{"node_modules/script.js"}, 566 | expected: "node_modules/script.js\n", 567 | }, 568 | { 569 | description: "node_modules script: inspect as app args left alone", 570 | args: []string{"node_modules/script.js", "--inspect"}, 571 | expected: "node_modules/script.js\n--inspect\n", 572 | }, 573 | { 574 | description: "node_modules script: inspect left alone with WRAPPER_ENABLED=0", 575 | args: []string{"--inspect=9229", "./node_nodules/script.js"}, 576 | env: map[string]string{"WRAPPER_ENABLED": "0"}, 577 | expected: "--inspect=9229\n./node_nodules/script.js\n", 578 | }, 579 | { 580 | description: "node_modules script: inspect left alone for next.js launcher", 581 | args: []string{"--inspect=9229", "./node_nodules/.bin/next"}, 582 | expected: "--inspect=9229\n./node_nodules/.bin/next\n", 583 | }, 584 | { 585 | description: "node_modules script: inspect left alone with WRAPPER_ALLOWED=script.js", 586 | args: []string{"--inspect=9229", "./node_nodules/script.js"}, 587 | env: map[string]string{"WRAPPER_ALLOWED": "script.js"}, 588 | expected: "--inspect=9229\n./node_nodules/script.js\n", 589 | }, 590 | { 591 | description: "node_modules script with inspect: seeds NODE_DEBUG", 592 | args: []string{"--inspect", "node_modules/script.js"}, 593 | expected: "NODE_DEBUG=--inspect\nnode_modules/script.js\n", 594 | }, 595 | { 596 | description: "node_modules script with NODE_OPTIONS='--inspect': seeds NODE_DEBUG", 597 | args: []string{"node_modules/script.js"}, 598 | env: map[string]string{"NODE_OPTIONS": "--inspect"}, 599 | expected: "NODE_DEBUG=--inspect\nnode_modules/script.js\n", 600 | }, 601 | { 602 | description: "node_modules script with NODE_OPTIONS='--foo --inspect --bar': seeds NODE_DEBUG", 603 | args: []string{"node_modules/script.js"}, 604 | env: map[string]string{"NODE_OPTIONS": "--foo --inspect --bar"}, 605 | expected: "NODE_DEBUG=--inspect\nNODE_OPTIONS=--foo --bar\nnode_modules/script.js\n", 606 | }, 607 | { 608 | description: "node_modules script with NODE_DEBUG='--inspect': passed through", 609 | args: []string{"node_modules/script.js"}, 610 | env: map[string]string{"NODE_DEBUG": "--inspect"}, 611 | expected: "NODE_DEBUG=--inspect\nnode_modules/script.js\n", 612 | }, 613 | { 614 | description: "node_modules script with NODE_DEBUG='--inspect' and inspect-brk arg: NODE_DEBUG wins", 615 | args: []string{"--inspect-brk", "node_modules/script.js"}, 616 | env: map[string]string{"NODE_DEBUG": "--inspect"}, 617 | expected: "NODE_DEBUG=--inspect\nnode_modules/script.js\n", 618 | }, 619 | { 620 | description: "node_modules script with NODE_DEBUG='--inspect' and inspect-brk NODE_OPTIONS: NODE_DEBUG wins", 621 | args: []string{"node_modules/script.js"}, 622 | env: map[string]string{"NODE_DEBUG": "--inspect", "NODE_OPTIONS": "--inspect-brk"}, 623 | expected: "NODE_DEBUG=--inspect\nnode_modules/script.js\n", 624 | }, 625 | { 626 | description: "nodemon script with NODE_DEBUG='--inspect': added to nodemon", 627 | args: []string{"node_modules/nodemon/nodemon.js"}, 628 | env: map[string]string{"NODE_DEBUG": "--inspect"}, 629 | expected: "node_modules/nodemon/nodemon.js\n--inspect\n", 630 | }, 631 | } 632 | 633 | for _, test := range tests { 634 | t.Run(test.description, func(t *testing.T) { 635 | env := map[string]string{"PATH": firstNodeDir + string(os.PathListSeparator) + actualNodeDir + string(os.PathListSeparator) + os.Getenv("PATH")} 636 | for k, v := range test.env { 637 | env[k] = v 638 | } 639 | 640 | nc := nodeContext{program: "nodeBin", args: test.args, env: env} 641 | var in bytes.Buffer 642 | var out bytes.Buffer 643 | if err := run(&nc, &in, &out, &out); err != nil { 644 | t.Errorf("node exec failed: %v", err) 645 | } 646 | if nc.program != actualNode { 647 | t.Errorf("unwrap resolved to %q but wanted %q", nc.program, actualNode) 648 | } 649 | if out.String() != test.expected { 650 | t.Errorf("output mismatch\nexpected: %q\n but got: %q", test.expected, out.String()) 651 | } 652 | }) 653 | } 654 | 655 | } 656 | -------------------------------------------------------------------------------- /nodejs/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: nodejs 5 | 6 | requires: 7 | - path: ../integration 8 | activeProfiles: 9 | - name: integration 10 | activatedBy: [integration] 11 | 12 | build: 13 | local: 14 | useBuildkit: true 15 | artifacts: 16 | - image: skaffold-debug-nodejs 17 | context: helper-image 18 | custom: 19 | buildCommand: ../../hack/buildx.sh 20 | 21 | test: 22 | - image: skaffold-debug-nodejs 23 | structureTests: [structure-tests-nodejs.yaml] 24 | # Disabled custom test pending Skaffold #5665 and #5666 25 | ##custom: 26 | ## - command: "cd helper-image; go test ." 27 | ## dependencies: 28 | ## paths: ["helper-image/*.go", "helper-image/go.*"] 29 | 30 | deploy: 31 | logs: 32 | prefix: auto 33 | kubectl: 34 | manifests: [] 35 | 36 | profiles: 37 | 38 | # local: never push to remote registries 39 | - name: local 40 | build: 41 | local: 42 | push: false 43 | 44 | # integration: set of `skaffold debug`-like integration tests 45 | - name: integration 46 | patches: 47 | - op: add 48 | path: /build/artifacts/- 49 | value: 50 | image: nodejs12app 51 | context: test/nodejsapp 52 | docker: 53 | buildArgs: 54 | NODEVERSION: 12.16.0 55 | deploy: 56 | kubectl: 57 | manifests: 58 | - test/k8s-test-nodejs12.yaml 59 | 60 | # release: pushes images to production with :latest 61 | - name: release 62 | build: 63 | local: 64 | push: true 65 | tagPolicy: 66 | sha256: {} 67 | 68 | # deprecated-names: use short (deprecated) image names: images were 69 | # prefixed with `skaffold-debug-` so they were more easily distinguished 70 | # from other images with similar names. 71 | - name: deprecated-names 72 | patches: 73 | - op: replace 74 | path: /build/artifacts/0/image 75 | from: skaffold-debug-nodejs 76 | value: nodejs 77 | - op: replace 78 | path: /test/0/image 79 | from: skaffold-debug-nodejs 80 | value: nodejs 81 | 82 | -------------------------------------------------------------------------------- /nodejs/structure-tests-nodejs.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: 2.0.0 2 | 3 | fileExistenceTests: 4 | - name: 'node wrapper' 5 | path: '/duct-tape/nodejs/bin/node' 6 | 7 | commandTests: 8 | - name: "run with no /dbg should fail" 9 | command: "sh" 10 | args: ["/install.sh"] 11 | expectedError: ["Error: installation requires a volume mount at /dbg"] 12 | exitCode: 1 13 | - name: "run with /dbg should install" 14 | setup: [["mkdir", "/dbg"]] 15 | command: "sh" 16 | args: ["/install.sh"] 17 | expectedOutput: ["Installing runtime debugging support files in /dbg", "Installation complete"] 18 | exitCode: 0 19 | -------------------------------------------------------------------------------- /nodejs/test/k8s-test-nodejs12.yaml: -------------------------------------------------------------------------------- 1 | # This test approximates `skaffold debug` for a nodejs app. 2 | # Must use `kubectl port-forward` as we can't seem to expose 3 | # the node inspector using a service. 4 | apiVersion: v1 5 | kind: Pod 6 | metadata: 7 | name: nodejs12pod 8 | labels: 9 | app: hello 10 | protocol: devtools 11 | runtime: nodejs12 12 | spec: 13 | containers: 14 | - name: nodejs12app 15 | image: nodejs12app 16 | env: 17 | - name: NODE_DEBUG 18 | value: "--inspect=9229" 19 | - name: PATH 20 | value: "/dbg/nodejs/bin:/usr/local/bin:/usr/bin:/bin" 21 | ports: 22 | - containerPort: 3000 23 | - containerPort: 9229 24 | name: devtools 25 | readinessProbe: 26 | httpGet: 27 | path: / 28 | port: 3000 29 | volumeMounts: 30 | - mountPath: /dbg 31 | name: node-debugging-support 32 | initContainers: 33 | - image: skaffold-debug-nodejs 34 | name: install-node-support 35 | resources: {} 36 | volumeMounts: 37 | - mountPath: /dbg 38 | name: node-debugging-support 39 | volumes: 40 | - emptyDir: {} 41 | name: node-debugging-support 42 | 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: hello-devtools-nodejs12 48 | spec: 49 | ports: 50 | - name: http 51 | port: 3000 52 | protocol: TCP 53 | # Can't seem access node inspector via a service 54 | #- name: devtools 55 | # port: 9229 56 | # protocol: TCP 57 | selector: 58 | app: hello 59 | protocol: devtools 60 | runtime: nodejs12 61 | 62 | --- 63 | apiVersion: batch/v1 64 | kind: Job 65 | metadata: 66 | name: connect-to-nodejs12 67 | labels: 68 | project: container-debug-support 69 | type: integration-test 70 | spec: 71 | ttlSecondsAfterFinished: 10 72 | backoffLimit: 1 73 | template: 74 | spec: 75 | restartPolicy: Never 76 | initContainers: 77 | # wait for the normal app to be available 78 | - name: wait-for-nodejs12pod 79 | image: kubectl 80 | command: [sh, -c, "while ! curl -s hello-devtools-nodejs12:3000 2>/dev/null; do echo waiting for app; sleep 1; done"] 81 | containers: 82 | - name: verify-nodejs12 83 | image: kubectl 84 | command: [sh, -c, 'kubectl port-forward pod/nodejs12pod 9229 & sleep 5; curl -is localhost:9229/json/version'] 85 | 86 | 87 | -------------------------------------------------------------------------------- /nodejs/test/nodejsapp/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODEVERSION 2 | FROM node:${NODEVERSION}-alpine 3 | 4 | USER node 5 | RUN mkdir /home/node/app 6 | WORKDIR /home/node/app 7 | 8 | EXPOSE 3000 9 | ARG ENV=production 10 | ENV NODE_ENV $ENV 11 | CMD npm run $NODE_ENV 12 | 13 | COPY --chown=node:node package* ./ 14 | RUN npm install 15 | COPY --chown=node:node . . 16 | -------------------------------------------------------------------------------- /nodejs/test/nodejsapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "production": "node src/index.js", 8 | "development": "nodemon src/index.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.16.4", 12 | "semver": "^7.5.4" 13 | }, 14 | "devDependencies": { 15 | "nodemon": "^2.0.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nodejs/test/nodejsapp/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { echo } = require('./utils'); 3 | const os = require('os'); 4 | 5 | const app = express() 6 | const port = 3000 7 | 8 | app.get('/', (req, res) => res.send(echo(`Hello from ${os.platform()}/${os.arch()}!\n`))) 9 | 10 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 11 | -------------------------------------------------------------------------------- /nodejs/test/nodejsapp/src/utils/index.js: -------------------------------------------------------------------------------- 1 | function echo(string) { 2 | return string; 3 | } 4 | 5 | module.exports = { 6 | echo 7 | } -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # note: we build the first build with --cache-artifacts=false to 5 | # avoid cache poisoning from single-platform builds 6 | # https://github.com/GoogleContainerTools/skaffold/issues/5504 7 | 8 | # publish with longer image names 9 | skaffold build -p release --default-repo gcr.io/k8s-skaffold/skaffold-debug-support --cache-artifacts=false 10 | skaffold build -p release --default-repo gcr.io/gcp-dev-tools/duct-tape 11 | 12 | # the github project packages is a backup location; will need to 13 | # migrate to ghcr.io/googlecontainertools at some point 14 | skaffold build -p release --default-repo docker.pkg.github.com/googlecontainertools/skaffold 15 | 16 | # publish with shorter (deprecated) image names 17 | skaffold build -p release,deprecated-names --default-repo gcr.io/k8s-skaffold/skaffold-debug-support 18 | skaffold build -p release,deprecated-names --default-repo gcr.io/gcp-dev-tools/duct-tape 19 | -------------------------------------------------------------------------------- /python/helper-image/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Skaffold Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This Dockerfile creates a debug helper base image for Python. 16 | # It provides installations of debugpy, ptvsd, pydevd, and pydevd-pycharm 17 | # for Python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10. 18 | # - Apache Beam is based around Python 3.5 19 | # - Many ML/NLP images are based on Python 3.5 and 3.6 20 | # 21 | # debugpy and ptvsd are well-structured packages installed in separate 22 | # directories under # /dbg/python/lib/pythonX.Y/site-packages and 23 | # that do not interfere with each other. 24 | # 25 | # pydevd and pydevd-pycharm install a script in .../bin and both install 26 | # .py files directly in .../lib/pythonX.Y/site-packages. To avoid 27 | # interference we install pydevd and pydevd-pycharm under /dbg/python/pydevd/pythonX.Y 28 | # and /dbg/python/pydevd-pycharm/pythonX.Y 29 | 30 | FROM python:2.7 as python27 31 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 32 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python2.7 pip install --user pydevd==2.8.0 --no-warn-script-location 33 | COPY pydevd_2_8_0.patch ./pydevd.patch 34 | RUN patch -p0 -d /dbgpy/pydevd/python2.7/lib/python2.7/site-packages < pydevd.patch 35 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python2.7 pip install --user pydevd-pycharm --no-warn-script-location 36 | 37 | FROM python:3.5 as python35 38 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 39 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.5 pip install --user pydevd==2.8.0 --no-warn-script-location 40 | COPY pydevd_2_8_0.patch ./pydevd.patch 41 | RUN patch -p0 -d /dbgpy/pydevd/python3.5/lib/python3.5/site-packages < pydevd.patch 42 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.5 pip install --user pydevd-pycharm --no-warn-script-location 43 | 44 | FROM python:3.6 as python36 45 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 46 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.6 pip install --user pydevd==2.9.5 --no-warn-script-location 47 | COPY pydevd_2_9_5.patch ./pydevd.patch 48 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.6/lib/python3.6/site-packages < pydevd.patch 49 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.6 pip install --user pydevd-pycharm --no-warn-script-location 50 | 51 | FROM python:3.7 as python37 52 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 53 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.7 pip install --user pydevd==2.9.5 --no-warn-script-location 54 | COPY pydevd_2_9_5.patch ./pydevd.patch 55 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.7/lib/python3.7/site-packages < pydevd.patch 56 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.7 pip install --user pydevd-pycharm --no-warn-script-location 57 | 58 | FROM python:3.8 as python38 59 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 60 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.8 pip install --user pydevd==2.9.5 --no-warn-script-location 61 | COPY pydevd_2_9_5.patch ./pydevd.patch 62 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.8/lib/python3.8/site-packages < pydevd.patch 63 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.8 pip install --user pydevd-pycharm --no-warn-script-location 64 | 65 | FROM python:3.9 as python39 66 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 67 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.9 pip install --user pydevd==2.9.5 --no-warn-script-location 68 | COPY pydevd_2_9_5.patch ./pydevd.patch 69 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.9/lib/python3.9/site-packages < pydevd.patch 70 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.9 pip install --user pydevd-pycharm --no-warn-script-location 71 | 72 | FROM python:3.10 as python3_10 73 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 74 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.10 pip install --user pydevd==2.9.5 --no-warn-script-location 75 | COPY pydevd_2_9_5.patch ./pydevd.patch 76 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.10/lib/python3.10/site-packages < pydevd.patch 77 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.10 pip install --user pydevd-pycharm --no-warn-script-location 78 | 79 | FROM python:3.11 as python3_11 80 | RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy 81 | RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.11 pip install --user pydevd==2.9.5 --no-warn-script-location 82 | COPY pydevd_2_9_5.patch ./pydevd.patch 83 | RUN patch --binary -p0 -d /dbgpy/pydevd/python3.11/lib/python3.11/site-packages < pydevd.patch 84 | RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.11 pip install --user pydevd-pycharm --no-warn-script-location 85 | 86 | FROM --platform=$BUILDPLATFORM golang:1.23 as build 87 | ARG BUILDPLATFORM 88 | ARG TARGETOS 89 | ARG TARGETARCH 90 | COPY launcher/ . 91 | # Produce an as-static-as-possible wrapper binary to work on musl and glibc 92 | RUN GOPATH="" CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ 93 | go build -o launcher -ldflags '-s -w -extldflags "-static"' . 94 | 95 | # Now populate the duct-tape image with the language runtime debugging support files 96 | # The debian image is about 95MB bigger 97 | FROM --platform=$TARGETPLATFORM busybox 98 | ARG TARGETPLATFORM 99 | 100 | # The install script copies all files in /duct-tape to /dbg 101 | COPY install.sh / 102 | CMD ["/bin/sh", "/install.sh"] 103 | WORKDIR /duct-tape 104 | COPY --from=python27 /dbgpy/ python/ 105 | COPY --from=python35 /dbgpy/ python/ 106 | COPY --from=python36 /dbgpy/ python/ 107 | COPY --from=python37 /dbgpy/ python/ 108 | COPY --from=python38 /dbgpy/ python/ 109 | COPY --from=python39 /dbgpy/ python/ 110 | COPY --from=python3_10 /dbgpy/ python/ 111 | COPY --from=python3_11 /dbgpy/ python/ 112 | COPY --from=build /go/launcher python/ 113 | -------------------------------------------------------------------------------- /python/helper-image/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -d /dbg ]; then 5 | echo "Error: installation requires a volume mount at /dbg" 1>&2 6 | exit 1 7 | fi 8 | 9 | echo "Installing runtime debugging support files in /dbg" 10 | tar cf - -C /duct-tape . | tar xf - -C /dbg 11 | echo "Installation complete" 12 | -------------------------------------------------------------------------------- /python/helper-image/launcher/cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "os/exec" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | // for testing 28 | var newCommand = createCommand 29 | var newConsoleCommand = createConsoleCommand 30 | 31 | // commander is a subset of exec.Cmd 32 | type commander interface { 33 | Run() error 34 | Output() ([]byte, error) 35 | CombinedOutput() ([]byte, error) 36 | } 37 | 38 | // ensures Cmd satisfies the commander interface 39 | var _ commander = (*exec.Cmd)(nil) 40 | 41 | // createCommand creates a normal exec.Cmd object 42 | func createCommand(ctx context.Context, cmdline []string, env env) commander { 43 | logrus.Debugf("command: %v (env: %s)", cmdline, env) 44 | cmd := exec.CommandContext(ctx, cmdline[0], cmdline[1:]...) 45 | cmd.Env = env.AsPairs() 46 | return cmd 47 | } 48 | 49 | // createConsoleCommand creates an exec.Cmd object that connects to os.Stdin, os.Stdout, os.Stderr 50 | func createConsoleCommand(ctx context.Context, cmdline []string, env env) commander { 51 | logrus.Debugf("command(stdin/out/err): %v (env: %s)", cmdline, env) 52 | cmd := exec.CommandContext(ctx, cmdline[0], cmdline[1:]...) 53 | cmd.Stdin = os.Stdin 54 | cmd.Stdout = os.Stdout 55 | cmd.Stderr = os.Stderr 56 | cmd.Env = env.AsPairs() 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /python/helper-image/launcher/cmd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "os/exec" 22 | "testing" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | ) 26 | 27 | func TestCmd(t *testing.T) { 28 | RunCmdOut([]string{"hello"}, "abc"). 29 | AndRunCmdFail([]string{"ls"}, 1). 30 | Setup(t) 31 | 32 | if out, err := newCommand(nil, []string{"hello"}, nil).Output(); err != nil { 33 | t.Error("command should not have failed") 34 | } else if string(out) != "abc" { 35 | t.Error("output should have been abc") 36 | } 37 | if newCommand(nil, []string{"ls"}, nil).Run() == nil { 38 | t.Error("command should have failed") 39 | } 40 | } 41 | 42 | type fakeCmd struct { 43 | mode string 44 | cmdline []string 45 | exitCode int 46 | output string 47 | } 48 | 49 | // ensures fakeCmd satisfies the commander interface 50 | var _ commander = (*fakeCmd)(nil) 51 | 52 | func (f *fakeCmd) Run() error { 53 | _t.Helper() 54 | if f.mode != "Run" { 55 | _t.Errorf("Command%v: expected %s() not Run()", f.cmdline, f.mode) 56 | } 57 | if f.exitCode == 0 { 58 | return nil 59 | } 60 | // doesn't seem to be an easy way to set the exitcode 61 | return &exec.ExitError{} 62 | } 63 | 64 | func (f *fakeCmd) Output() ([]byte, error) { 65 | _t.Helper() 66 | if f.mode != "Output" { 67 | _t.Errorf("Command%v: expected %v() not Output()", f.cmdline, f.mode) 68 | } 69 | if f.exitCode == 0 { 70 | return []byte(f.output), nil 71 | } 72 | // doesn't seem to be an easy way to set the exitcode 73 | return []byte(f.output), &exec.ExitError{} 74 | } 75 | 76 | func (f *fakeCmd) CombinedOutput() ([]byte, error) { 77 | _t.Helper() 78 | if f.mode != "Output" { 79 | _t.Errorf("Command%v: expected %v() not CombinedOutput()", f.cmdline, f.mode) 80 | } 81 | if f.exitCode == 0 { 82 | return []byte(f.output), nil 83 | } 84 | // doesn't seem to be an easy way to set the exitcode 85 | return []byte(f.output), &exec.ExitError{} 86 | } 87 | 88 | type commands []*fakeCmd 89 | 90 | var ( 91 | _cmdStack commands 92 | _t *testing.T 93 | ) 94 | 95 | func fakeCommand(_ context.Context, cmdline []string, env env) commander { 96 | _t.Helper() 97 | if len(_cmdStack) == 0 { 98 | _t.Fatalf("test expected no further commands: %v", cmdline) 99 | } 100 | current := _cmdStack[0] 101 | _cmdStack = _cmdStack[1:] 102 | if diff := cmp.Diff(current.cmdline, cmdline); diff != "" { 103 | _t.Errorf("cmdlines differ (-got, +want): %s", diff) 104 | } 105 | return current 106 | } 107 | 108 | func (c commands) Setup(t *testing.T) { 109 | _t = t 110 | _cmdStack = c 111 | 112 | oldCommand := newCommand 113 | oldConsoleCommand := newConsoleCommand 114 | newCommand = fakeCommand 115 | newConsoleCommand = fakeCommand 116 | _t.Cleanup(func() { 117 | newCommand = oldCommand 118 | newConsoleCommand = oldConsoleCommand 119 | }) 120 | } 121 | 122 | func RunCmd(cmdline []string) commands { 123 | return commands{}.AndRunCmd(cmdline) 124 | } 125 | 126 | func RunCmdFail(cmdline []string, exitCode int) commands { 127 | return commands{}.AndRunCmdFail(cmdline, exitCode) 128 | } 129 | 130 | func RunCmdOut(cmdline []string, output string) commands { 131 | return commands{}.AndRunCmdOut(cmdline, output) 132 | } 133 | 134 | func RunCmdOutFail(cmdline []string, output string, exitCode int) commands { 135 | return commands{}.AndRunCmdOutFail(cmdline, output, exitCode) 136 | } 137 | 138 | func (c commands) AndRunCmd(cmdline []string) commands { 139 | c = append(c, &fakeCmd{mode: "Run", cmdline: cmdline}) 140 | return c 141 | } 142 | 143 | func (c commands) AndRunCmdFail(cmdline []string, exitCode int) commands { 144 | c = append(c, &fakeCmd{mode: "Run", cmdline: cmdline, exitCode: exitCode}) 145 | return c 146 | } 147 | 148 | func (c commands) AndRunCmdOut(cmdline []string, output string) commands { 149 | c = append(c, &fakeCmd{mode: "Output", cmdline: cmdline, output: output}) 150 | return c 151 | } 152 | 153 | func (c commands) AndRunCmdOutFail(cmdline []string, output string, exitCode int) commands { 154 | c = append(c, &fakeCmd{mode: "Output", cmdline: cmdline, output: output, exitCode: exitCode}) 155 | return c 156 | } 157 | -------------------------------------------------------------------------------- /python/helper-image/launcher/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "sort" 23 | "strings" 24 | ) 25 | 26 | type env map[string]string 27 | 28 | // EnvFromPairs turns a set of VAR=VALUE strings to a map. 29 | func EnvFromPairs(entries []string) env { 30 | e := make(env) 31 | for _, entry := range entries { 32 | kv := strings.SplitN(entry, "=", 2) 33 | e[kv[0]] = kv[1] 34 | } 35 | return e 36 | } 37 | 38 | // AsPairs turns a map of variable:value pairs into a set of VAR=VALUE string pairs. 39 | func (e env) AsPairs() []string { 40 | var m []string 41 | for k, v := range e { 42 | m = append(m, k+"="+v) 43 | } 44 | return m 45 | } 46 | 47 | // AppendFilepath appands a path to a environment variable. 48 | func (e env) AppendFilepath(key string, path string) { 49 | v := e[key] 50 | if v != "" { 51 | v = v + string(filepath.ListSeparator) + path 52 | } else { 53 | v = path 54 | } 55 | e[key] = v 56 | } 57 | 58 | func (e env) String() string { 59 | var keys []string 60 | for k, _ := range e { 61 | keys = append(keys, k) 62 | } 63 | sort.Strings(keys) 64 | 65 | var s string 66 | for _, k := range keys { 67 | s += fmt.Sprintf("%s=%q ", k, e[k]) 68 | } 69 | return s 70 | } 71 | -------------------------------------------------------------------------------- /python/helper-image/launcher/env_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "path/filepath" 21 | "sort" 22 | "testing" 23 | ) 24 | 25 | func TestEnvAsPairs(t *testing.T) { 26 | tests := []struct { 27 | description string 28 | env map[string]string 29 | expected []string 30 | }{ 31 | {"nil", nil, nil}, 32 | {"empty", map[string]string{}, nil}, 33 | {"single", map[string]string{"a": "b"}, []string{"a=b"}}, 34 | {"multiple", map[string]string{"a": "b", "c": "d"}, []string{"a=b", "c=d"}}, 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.description, func(t *testing.T) { 39 | result := env(test.env).AsPairs() 40 | sort.Strings(result) 41 | if len(result) != len(test.expected) { 42 | t.Errorf("expected %v but got %v", test.expected, result) 43 | } else { 44 | for i := 0; i < len(result); i++ { 45 | if result[i] != test.expected[i] { 46 | t.Errorf("expected %v but got %v", test.expected[i], result[i]) 47 | } 48 | } 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestEnvFromPairs(t *testing.T) { 55 | tests := []struct { 56 | description string 57 | env []string 58 | expected map[string]string 59 | }{ 60 | {"nil", nil, nil}, 61 | {"empty", []string{}, nil}, 62 | {"single", []string{"a=b"}, map[string]string{"a": "b"}}, 63 | {"multiple", []string{"a=b", "c=d"}, map[string]string{"a": "b", "c": "d"}}, 64 | {"collisions", []string{"a=b", "a=d"}, map[string]string{"a": "d"}}, 65 | } 66 | 67 | for _, test := range tests { 68 | t.Run(test.description, func(t *testing.T) { 69 | result := EnvFromPairs(test.env) 70 | if len(result) != len(test.expected) { 71 | t.Errorf("expected %v but got %v", test.expected, result) 72 | } else { 73 | for k, v := range result { 74 | if v != test.expected[k] { 75 | t.Errorf("for %v expected %v but got %v", k, test.expected[k], v) 76 | } 77 | } 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestEnvAppendFilepath(t *testing.T) { 84 | tests := []struct { 85 | description string 86 | env env 87 | key string 88 | value string 89 | expected map[string]string 90 | }{ 91 | {"empty", env{}, "PATH", "value", env{"PATH": "value"}}, 92 | {"existing value", env{"PATH": "other"}, "PATH", "value", env{"PATH": "other" + string(filepath.ListSeparator) + "value"}}, 93 | {"other value unchanged", env{"PYTHONPATH": "other"}, "PATH", "value", env{"PATH": "value", "PYTHONPATH": "other"}}, 94 | {"existing value with other value unchanged", env{"PATH": "other", "PYTHONPATH": "other"}, "PATH", "value", env{"PATH": "other" + string(filepath.ListSeparator) + "value", "PYTHONPATH": "other"}}, 95 | } 96 | 97 | for _, test := range tests { 98 | t.Run(test.description, func(t *testing.T) { 99 | result := test.env // not a copy but that's ok for this test 100 | result.AppendFilepath(test.key, test.value) 101 | if len(result) != len(test.expected) { 102 | t.Errorf("expected %v but got %v", test.expected, result) 103 | } else { 104 | for k, v := range result { 105 | if v != test.expected[k] { 106 | t.Errorf("for %v expected %v but got %v", k, test.expected[k], v) 107 | } 108 | } 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /python/helper-image/launcher/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleContainerTools/container-debug-support/python 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.4 7 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect 9 | github.com/sirupsen/logrus v1.9.3 10 | github.com/stretchr/objx v0.1.1 // indirect 11 | golang.org/x/sys v0.10.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /python/helper-image/launcher/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 5 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 7 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 9 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 13 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 14 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 15 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 19 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 22 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 25 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 27 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /python/helper-image/launcher/launcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // A `skaffold debug` launcher for Python. 18 | // 19 | // Configuring a Python app for debugging is quirky. There are 20 | // four debugging backends: 21 | // 22 | // - pydevd: the stock Python debugging backend 23 | // - pydevd-pycharm: PyDev with modifications for IntelliJ/PyCharm 24 | // - ptvsd: wraps pydevd with the debug-adapter protocol (obsolete) 25 | // - debugpy: new and improved ptvsd 26 | // 27 | // Each has pyx libraries which are specific to particular versions of Python. 28 | // 29 | // Further complicating matters is that a number of Python packages 30 | // use launcher scripts (e.g., gunicorn), and so we can't simply run 31 | // `python -m ptvsd -- gunicorn` as ptvsd/debugpy/etc don't look for 32 | // the script file in the PATH. 33 | // 34 | // Another wrinkle is that we cannot just provide a `python` wrapper 35 | // executable that will hand off to the real `python` as `pip install 36 | // hard-codes the python binary location in launcher scripts. And it's 37 | // not that unusual to have a `python`, `python3`, and `python2` 38 | // scripts that invoke different python installations. 39 | // 40 | // And hence the introduction of this debug launcher. 41 | // 42 | // This launcher is expected to be invoked as follows: 43 | // 44 | // launcher --mode \ 45 | // --port p [--wait] -- original-command-line ... 46 | // 47 | // This launcher determines the python executable based on 48 | // `original-command-line`, unwrapping any python scripts, and 49 | // configures the debugging back-end. 50 | // The launcher configures the PYTHONPATH to point to the appropriate 51 | // installation pydevd/debugpy/ptvsd for the corresponding python binary. 52 | // 53 | // debugpy and ptvsd are pretty straightforward translations of the 54 | // launcher command-line `python -m debugpy`. 55 | // 56 | // pydevd is more involved as pydevd does not support loading modules 57 | // from the command-line (e.g., `python -m flask`). This launcher 58 | // instead creates a small module-loader script using runpy. 59 | // So `launcher --mode pydevd --port 5678 -- python -m flask app.py` 60 | // will create a temp file named `skaffold_pydevd_launch.py`: 61 | // ``` 62 | // import sys 63 | // import runpy 64 | // runpy.run_module('flask', run_name="__main__",alter_sys=True) 65 | // ``` 66 | // and will then invoke: 67 | // ``` 68 | // 69 | // python -m pydevd --server --port 5678 --continue \ 70 | // --file /tmp/pydevd716531212/skaffold_pydevd_launch.py 71 | // 72 | // ``` 73 | // 74 | // The launcher can be configured through several environment 75 | // variables: 76 | // 77 | // - Set `WRAPPER_ENABLED=false` to disable the launcher: the 78 | // launcher will execute the original-command-line as-is. 79 | // - Set `WRAPPER_SKIP_ENV=true` to avoid setting PYTHONPATH 80 | // to point to bundled debugging backends: this is useful if 81 | // your app already includes `debugpy`. 82 | // - Set `WRAPPER_PYTHON_VERSION=3.9` to avoid trying to determine 83 | // the python version by executing `python -V` 84 | // - Set `WRAPPER_VERBOSE` to one of `error`, `warn`, `info`, `debug`, 85 | // or `trace` to reduce or increase the verbosity 86 | package main 87 | 88 | import ( 89 | "context" 90 | "errors" 91 | "flag" 92 | "fmt" 93 | "io" 94 | "io/ioutil" 95 | "os" 96 | "os/exec" 97 | "path/filepath" 98 | "strconv" 99 | "strings" 100 | 101 | shell "github.com/kballard/go-shellquote" 102 | "github.com/sirupsen/logrus" 103 | ) 104 | 105 | var ( 106 | // dbgRoot is the location where the skaffold-debug helpers should be installed. 107 | // The python helpers should be in dbgRoot + "/python" 108 | dbgRoot = "/dbg" 109 | ) 110 | 111 | const ( 112 | ModeDebugpy string = "debugpy" 113 | ModePtvsd string = "ptvsd" 114 | ModePydevd string = "pydevd" 115 | ModePydevdPycharm string = "pydevd-pycharm" 116 | ) 117 | 118 | // pythonContext represents the launch context. 119 | type pythonContext struct { 120 | debugMode string 121 | port uint 122 | wait bool 123 | 124 | args []string 125 | env env 126 | 127 | major, minor int // python version 128 | } 129 | 130 | func main() { 131 | ctx := context.Background() 132 | env := EnvFromPairs(os.Environ()) 133 | logrus.SetLevel(logrusLevel(env)) 134 | logrus.Trace("launcher args:", os.Args[1:]) 135 | 136 | pc := pythonContext{env: env} 137 | flag.StringVar(&dbgRoot, "helpers", "/dbg", "base location for skaffold-debug helpers") 138 | flag.StringVar(&pc.debugMode, "mode", "", "debugger mode: debugpy, ptvsd, pydevd, pydevd-pycharm") 139 | flag.UintVar(&pc.port, "port", 9999, "port to listen for remote debug connections") 140 | flag.BoolVar(&pc.wait, "wait", false, "wait for debugger connection on start") 141 | 142 | flag.Parse() 143 | if err := validateDebugMode(pc.debugMode); err != nil { 144 | logrus.Fatal(err) 145 | } 146 | 147 | if len(flag.Args()) == 0 { 148 | logrus.Fatal("expected python command-line args") 149 | } 150 | pc.args = flag.Args() 151 | logrus.Debug("app command-line: ", pc.args) 152 | 153 | if !pc.prepare(ctx) { 154 | logrus.Info("launching original command: ", flag.Args()) 155 | cmd := newConsoleCommand(ctx, flag.Args(), env) 156 | run(cmd) 157 | } else { 158 | pc.launch(ctx) 159 | } 160 | // NOTREACHED 161 | } 162 | 163 | // validateDebugMode ensures the provided mode is a supported mode. 164 | func validateDebugMode(mode string) error { 165 | switch mode { 166 | case ModeDebugpy, ModePtvsd, ModePydevd, ModePydevdPycharm: 167 | return nil 168 | default: 169 | return fmt.Errorf("unknown debugger mode %q; expecting one of %v", mode, []string{ModeDebugpy, ModePtvsd, ModePydevd, ModePydevdPycharm}) 170 | } 171 | } 172 | 173 | func run(cmd commander) { 174 | if err := cmd.Run(); err != nil { 175 | var ee exec.ExitError 176 | if errors.Is(err, &ee) { 177 | os.Exit(ee.ExitCode()) 178 | } 179 | logrus.Fatal("error launching python debugging: ", err) 180 | } 181 | os.Exit(0) 182 | // NOTREACHED 183 | } 184 | 185 | // prepare sets up the debugging command line. Return true if successful or false if setup could not be completed. 186 | func (pc *pythonContext) prepare(ctx context.Context) bool { 187 | if !isEnabled(pc.env) { 188 | logrus.Infof("wrapper disabled") 189 | return false 190 | } 191 | if pc.alreadyConfigured() { 192 | logrus.Infof("already configured for debugging") 193 | return false 194 | } 195 | 196 | // rewrite the command-line by expanding script shebangs to run python and launch the app 197 | if err := pc.unwrapLauncher(ctx); err != nil { 198 | logrus.Warn("unable to determine launcher: ", err) 199 | return false 200 | } 201 | if err := pc.isPythonLauncher(ctx); err != nil { 202 | logrus.Warn("not a python launcher: ", err) 203 | return false 204 | } 205 | 206 | // set PYTHONPATH to point to the appropriate library for the given python version. 207 | if err := pc.updateEnv(ctx); err != nil { 208 | logrus.Warn("unable to configure environment: ", err) 209 | return false 210 | } 211 | // so pc.args[0] should be the python interpreter 212 | 213 | if err := pc.updateCommandLine(ctx); err != nil { 214 | logrus.Warn("unable to setup launcher: ", err) 215 | return false 216 | } 217 | return true 218 | } 219 | 220 | func (pc *pythonContext) launch(ctx context.Context) { 221 | cmd := newConsoleCommand(ctx, pc.args, pc.env) 222 | run(cmd) 223 | // NOTREACHED 224 | } 225 | 226 | // alreadyConfigured tries to determine if the python command-line is already configured 227 | // for debugging. 228 | func (pc *pythonContext) alreadyConfigured() bool { 229 | // TODO: consider handling `#!/usr/bin/env python` too, though `pip install` seems 230 | // to hard-code the python location instead. 231 | if filepath.Base(pc.args[0]) == "pydevd" { 232 | logrus.Debug("already configured to use pydevd") 233 | return true 234 | } 235 | if strings.HasPrefix(filepath.Base(pc.args[0]), "python") && len(pc.args) > 1 { 236 | if (pc.args[1] == "-m" && len(pc.args) > 2 && pc.args[2] == "debugpy") || pc.args[1] == "-mdebugpy" { 237 | logrus.Debug("already configured to use debugpy") 238 | return true 239 | } 240 | if (pc.args[1] == "-m" && len(pc.args) > 2 && pc.args[2] == "ptvsd") || pc.args[1] == "-mptvsd" { 241 | logrus.Debug("already configured to use ptvsd") 242 | return true 243 | } 244 | if (pc.args[1] == "-m" && len(pc.args) > 2 && pc.args[2] == "pydevd") || pc.args[1] == "-mpydevd" { 245 | logrus.Debug("already configured to use pydevd") 246 | return true 247 | } 248 | } 249 | return false 250 | } 251 | 252 | // unwrapLauncher attempts to expand the command-line in the given script, 253 | // providing that it does not look like a `python` launcher. 254 | // TODO: Windows .cmd and .bat files? 255 | func (pc *pythonContext) unwrapLauncher(_ context.Context) error { 256 | p := pc.args[0] 257 | 258 | _, err := os.Stat(p) 259 | if err != nil { 260 | if !os.IsNotExist(err) { 261 | return fmt.Errorf("could not access launcher %q: %w", p, err) 262 | } 263 | // try looking through PATH 264 | l, err := exec.LookPath(p) 265 | if err != nil { 266 | return fmt.Errorf("could not find launcher %q: %w", p, err) 267 | } 268 | p = l 269 | } 270 | if strings.HasPrefix(filepath.Base(p), "python") { 271 | logrus.Debugf("no further unwrapping required: launcher appears to be python: %q", p) 272 | return nil 273 | } 274 | f, err := os.Open(p) 275 | if err != nil { 276 | return fmt.Errorf("could not open launcher %q: %w", p, err) 277 | } 278 | defer f.Close() 279 | 280 | shebang := make([]byte, 1024) 281 | if n, err := f.Read(shebang); err == io.EOF || n < 2 { 282 | logrus.Debugf("%q has no shebang", p) 283 | return nil 284 | } else if err != nil { 285 | return fmt.Errorf("error reading file header from %q: %w", p, err) 286 | } else if string(shebang[0:2]) != "#!" { 287 | logrus.Debugf("%q appears to be a binary", p) 288 | return nil 289 | } 290 | cl := strings.SplitN(string(shebang[2:]), "\n", 2)[0] 291 | logrus.Tracef("%q has shebang %q", p, cl) 292 | s, err := shell.Split(cl) 293 | if err != nil { 294 | logrus.Warnf("%q shebang %q seems odd: %v", p, cl, err) 295 | s = []string{cl} 296 | } 297 | pc.args[0] = p // ensure script is full path if resolved in PATH 298 | pc.args = append(s, pc.args...) 299 | logrus.Debugf("expanded command-line: %q -> %v", p, pc.args) 300 | return nil 301 | } 302 | 303 | func (pc *pythonContext) isPythonLauncher(ctx context.Context) error { 304 | major, minor, err := determinePythonMajorMinor(ctx, pc.args[0], pc.env) 305 | pc.major = major 306 | pc.minor = minor 307 | return err 308 | } 309 | 310 | func (pc *pythonContext) updateEnv(ctx context.Context) error { 311 | // Perhaps we should check PYTHONPATH or ~/.local to see if the user has already 312 | // installed one of our supported debug libraries 313 | if pc.env["WRAPPER_SKIP_ENV"] != "" { 314 | logrus.Debug("Skipping environment configuration by request") 315 | return nil 316 | } 317 | 318 | _, err := os.Stat(dbgRoot) 319 | if err != nil { 320 | if os.IsNotExist(err) { 321 | logrus.Warnf("skaffold-debug helpers not found at %q", dbgRoot) 322 | return nil 323 | } 324 | return fmt.Errorf("skaffold-debug helpers are inaccessible at %q: %w", dbgRoot, err) 325 | } 326 | 327 | if pc.env == nil { 328 | pc.env = env{} 329 | } 330 | // The skaffold-debug-python helper image places pydevd and debugpy in /dbg/python/lib/pythonM.N, 331 | // but separates pydevd and pydevd-pycharm in separate directories to avoid possible leakage. 332 | var libraryPath string 333 | switch pc.debugMode { 334 | case ModePtvsd, ModeDebugpy: 335 | libraryPath = fmt.Sprintf(dbgRoot+"/python/lib/python%d.%d/site-packages", pc.major, pc.minor) 336 | 337 | case ModePydevd: 338 | libraryPath = fmt.Sprintf(dbgRoot+"/python/pydevd/python%d.%d/lib/python%d.%d/site-packages", pc.major, pc.minor, pc.major, pc.minor) 339 | 340 | case ModePydevdPycharm: 341 | libraryPath = fmt.Sprintf(dbgRoot+"/python/pydevd-pycharm/python%d.%d/lib/python%d.%d/site-packages", pc.major, pc.minor, pc.major, pc.minor) 342 | } 343 | if libraryPath != "" { 344 | if !pathExists(libraryPath) { 345 | // Warn as the user may have installed debugpy themselves 346 | logrus.Warnf("Debugging support for Python %d.%d not found: may require manually installing %q", pc.major, pc.minor, pc.debugMode) 347 | } 348 | // Append to ensure user-configured values are found first. 349 | pc.env.AppendFilepath("PYTHONPATH", libraryPath) 350 | } 351 | return nil 352 | } 353 | 354 | func (pc *pythonContext) updateCommandLine(ctx context.Context) error { 355 | // TODO(#76): we're assuming the `-m module` argument comes first 356 | var cmdline []string 357 | switch pc.debugMode { 358 | case ModePtvsd: 359 | cmdline = append(cmdline, pc.args[0]) 360 | cmdline = append(cmdline, "-m", "ptvsd", "--host", "localhost", "--port", strconv.Itoa(int(pc.port))) 361 | if pc.wait { 362 | cmdline = append(cmdline, "--wait") 363 | } 364 | cmdline = append(cmdline, pc.args[1:]...) 365 | pc.args = cmdline 366 | 367 | case ModeDebugpy: 368 | cmdline = append(cmdline, pc.args[0]) 369 | cmdline = append(cmdline, "-m", "debugpy", "--listen", strconv.Itoa(int(pc.port))) 370 | if pc.wait { 371 | cmdline = append(cmdline, "--wait-for-client") 372 | } 373 | // debugpy expects the `-m` module argument to be separate 374 | for i, arg := range pc.args[1:] { 375 | if i == 0 && arg != "-m" && strings.HasPrefix(arg, "-m") { 376 | cmdline = append(cmdline, "-m", strings.TrimPrefix(arg, "-m")) 377 | } else { 378 | cmdline = append(cmdline, arg) 379 | } 380 | } 381 | pc.args = cmdline 382 | 383 | case ModePydevd, ModePydevdPycharm: 384 | // Appropriate location to resolve pydevd is set in updateEnv 385 | cmdline = append(cmdline, pc.args[0]) 386 | cmdline = append(cmdline, "-m", "pydevd", "--server", "--port", strconv.Itoa(int(pc.port))) 387 | if !pc.wait { 388 | cmdline = append(cmdline, "--continue") 389 | } 390 | 391 | // --file is expected as last pydev argument, but it must be a file, and so launching with 392 | // a module requires some special handling. 393 | cmdline = append(cmdline, "--file") 394 | file, args, err := handlePydevModule(pc.args[1:]) 395 | if err != nil { 396 | return err 397 | } 398 | cmdline = append(cmdline, file) 399 | cmdline = append(cmdline, args...) 400 | pc.args = cmdline 401 | } 402 | return nil 403 | } 404 | 405 | func determinePythonMajorMinor(ctx context.Context, launcherBin string, env env) (major, minor int, err error) { 406 | var versionString string 407 | if env["WRAPPER_PYTHON_VERSION"] != "" { 408 | versionString = env["WRAPPER_PYTHON_VERSION"] 409 | logrus.Debugf("Python version from WRAPPER_PYTHON_VERSION=%q", versionString) 410 | } else { 411 | logrus.Debugf("trying to determine python version from %q", launcherBin) 412 | cmd := newCommand(ctx, []string{launcherBin, "-V"}, env) 413 | out, err := cmd.CombinedOutput() 414 | if err != nil { 415 | return -1, -1, fmt.Errorf("unable to determine python version from %q: %w", launcherBin, err) 416 | } 417 | versionString = string(out) 418 | logrus.Debugf("'%s -V' = %q", launcherBin, versionString) 419 | if !strings.HasPrefix(versionString, "Python ") { 420 | return -1, -1, fmt.Errorf("launcher is not a python interpreter: %q", launcherBin) 421 | } 422 | versionString = versionString[len("Python "):] 423 | } 424 | 425 | v := strings.Split(strings.TrimSpace(versionString), ".") 426 | major, err = strconv.Atoi(v[0]) 427 | if err == nil { 428 | minor, err = strconv.Atoi(v[1]) 429 | } 430 | return 431 | } 432 | 433 | // handlePydevModule applies special pydevd handling for a python module. When a module is 434 | // found, we write out a python script that uses runpy to invoke the module. 435 | func handlePydevModule(args []string) (string, []string, error) { 436 | switch { 437 | case len(args) == 0: 438 | return "", nil, fmt.Errorf("no python command-line specified") // shouldn't happen 439 | case !strings.HasPrefix(args[0], "-"): 440 | // this is a file 441 | return args[0], args[1:], nil 442 | case !strings.HasPrefix(args[0], "-m"): 443 | // this is some other command-line flag 444 | return "", nil, fmt.Errorf("expected python module: %q", args) 445 | } 446 | module := args[0][2:] 447 | remaining := args[1:] 448 | if module == "" { 449 | if len(args) == 1 { 450 | return "", nil, fmt.Errorf("missing python module: %q", args) 451 | } 452 | module = args[1] 453 | remaining = args[2:] 454 | } 455 | 456 | snippet := strings.ReplaceAll(`import sys 457 | import runpy 458 | runpy.run_module('{module}', run_name="__main__",alter_sys=True) 459 | `, `{module}`, module) 460 | 461 | // write out the temp location as other locations may not be writable 462 | d, err := ioutil.TempDir("", "pydevd*") 463 | if err != nil { 464 | return "", nil, err 465 | } 466 | // use a skaffold-specific file name to ensure no possibility of it matching a user import 467 | f := filepath.Join(d, "skaffold_pydevd_launch.py") 468 | if err := ioutil.WriteFile(f, []byte(snippet), 0755); err != nil { 469 | return "", nil, err 470 | } 471 | return f, remaining, nil 472 | } 473 | 474 | func isEnabled(env env) bool { 475 | v, found := env["WRAPPER_ENABLED"] 476 | return !found || (v != "0" && v != "false" && v != "no") 477 | } 478 | 479 | func logrusLevel(env env) logrus.Level { 480 | v := env["WRAPPER_VERBOSE"] 481 | if v != "" { 482 | if l, err := logrus.ParseLevel(v); err == nil { 483 | return l 484 | } 485 | logrus.Warnln("Unknown logging level: WRAPPER_VERBOSE=", v) 486 | } 487 | return logrus.WarnLevel 488 | } 489 | 490 | func pathExists(path string) bool { 491 | _, err := os.Stat(path) 492 | if err == nil || !os.IsNotExist(err) { 493 | return true 494 | } 495 | return false 496 | } 497 | -------------------------------------------------------------------------------- /python/helper-image/launcher/launcher_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/google/go-cmp/cmp" 28 | "github.com/google/go-cmp/cmp/cmpopts" 29 | ) 30 | 31 | func TestValidateDebugMode(t *testing.T) { 32 | tests := []struct { 33 | mode string 34 | shouldErr bool 35 | }{ 36 | {"debugpy", false}, 37 | {"ptvsd", false}, 38 | {"pydevd", false}, 39 | {"pydevd-pycharm", false}, 40 | {"", true}, 41 | {"pydev", true}, // the 'd' is important 42 | {"pydev-pycharm", true}, // the 'd' is important 43 | } 44 | for _, test := range tests { 45 | t.Run(test.mode, func(t *testing.T) { 46 | result := validateDebugMode(test.mode) 47 | if test.shouldErr && result == nil { 48 | t.Error("should have errored") 49 | } else if !test.shouldErr && result != nil { 50 | t.Error("should not have errored") 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestIsEnabled(t *testing.T) { 57 | tests := []struct { 58 | env env 59 | expected bool 60 | }{ 61 | { 62 | env: nil, 63 | expected: true, 64 | }, 65 | { 66 | env: env{"WRAPPER_ENABLED": "1"}, 67 | expected: true, 68 | }, 69 | { 70 | env: env{"WRAPPER_ENABLED": "true"}, 71 | expected: true, 72 | }, 73 | { 74 | env: env{"WRAPPER_ENABLED": "yes"}, 75 | expected: true, 76 | }, 77 | { 78 | env: env{"WRAPPER_ENABLED": ""}, 79 | expected: true, 80 | }, 81 | { 82 | env: env{"WRAPPER_ENABLED": "0"}, 83 | expected: false, 84 | }, 85 | { 86 | env: env{"WRAPPER_ENABLED": "no"}, 87 | expected: false, 88 | }, 89 | { 90 | env: env{"WRAPPER_ENABLED": "false"}, 91 | expected: false, 92 | }, 93 | } 94 | for _, test := range tests { 95 | t.Run(fmt.Sprintf("env: %v", test.env), func(t *testing.T) { 96 | result := isEnabled(test.env) 97 | if test.expected != result { 98 | t.Errorf("expected %v but got %v", test.expected, result) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestAlreadyConfigured(t *testing.T) { 105 | tests := []struct { 106 | description string 107 | pc pythonContext 108 | expected bool 109 | }{ 110 | {"non-python", pythonContext{args: []string{"/app"}}, false}, 111 | {"python with no debug", pythonContext{args: []string{"python", "app.py"}}, false}, 112 | {"misconfigured python module", pythonContext{args: []string{"python", "-m"}}, false}, 113 | {"python with app module", pythonContext{args: []string{"python", "-mapp"}}, false}, 114 | {"python with app module 2", pythonContext{args: []string{"python", "-m", "app"}}, false}, 115 | {"configured for pydevd", pythonContext{args: []string{"pydevd", "--server", "app"}}, true}, 116 | {"configured for pydevd", pythonContext{args: []string{"/dbg/pydevd/bin/pydevd", "--server", "app"}}, true}, 117 | {"configured for pydevd", pythonContext{args: []string{"python", "-mpydevd", "--server", "app"}}, true}, 118 | {"configured for pydevd", pythonContext{args: []string{"python3.8", "-m", "pydevd", "--server", "app"}}, true}, 119 | {"python with debugpy module", pythonContext{args: []string{"python", "-mdebugpy"}}, true}, 120 | {"versioned python with debugpy module", pythonContext{args: []string{"/usr/bin/python3.9", "-m", "debugpy"}}, true}, 121 | {"python with ptvsd module", pythonContext{args: []string{"python", "-mptvsd"}}, true}, 122 | {"versioned python with ptvsd module", pythonContext{args: []string{"/usr/bin/python3.9", "-m", "ptvsd"}}, true}, 123 | } 124 | for _, test := range tests { 125 | t.Run(test.description, func(t *testing.T) { 126 | result := test.pc.alreadyConfigured() 127 | if test.expected != result { 128 | t.Errorf("expected %v but got %v", test.expected, result) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func TestUnwrapLauncher(t *testing.T) { 135 | tests := []struct { 136 | description string 137 | filename string 138 | contents []byte 139 | shouldErr bool 140 | expected []string 141 | }{ 142 | { 143 | description: "non-existent file", 144 | filename: "d03$-n0t-3x1$t", 145 | shouldErr: true, 146 | }, 147 | { 148 | description: "empty file", 149 | contents: nil, 150 | expected: nil, 151 | }, 152 | { 153 | description: "non-shebang", 154 | contents: []byte{0, 1, 2, 3, 4, 5, 6, 7}, 155 | expected: nil, 156 | }, 157 | { 158 | description: "python script", 159 | contents: []byte("#!/bin/python\nprint \"Hello World\""), 160 | expected: []string{"/bin/python"}, 161 | }, 162 | { 163 | description: "script with args", 164 | contents: []byte("#!/bin/sh -x\necho \"Hello World\""), 165 | expected: []string{"/bin/sh", "-x"}, 166 | }, 167 | } 168 | 169 | for _, test := range tests { 170 | t.Run(test.description, func(t *testing.T) { 171 | n := test.filename 172 | if n == "" { 173 | f, err := ioutil.TempFile(t.TempDir(), "script*") 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | if _, err := f.Write(test.contents); err != nil { 178 | t.Fatal("error creating temp file", err) 179 | } 180 | f.Close() 181 | n = f.Name() 182 | } 183 | // for script files, the shebang should be extracted and parsed, and then 184 | // prepended to the current command-line. 185 | pc := pythonContext{args: []string{n, "arg1", "arg2"}} 186 | expected := []string{n, "arg1", "arg2"} 187 | if test.expected != nil { 188 | expected = append(test.expected, expected...) 189 | } 190 | err := pc.unwrapLauncher(nil) 191 | if test.shouldErr && err == nil { 192 | t.Error("expected an error") 193 | } 194 | if !test.shouldErr && err != nil { 195 | t.Error("should not error:", err) 196 | } else if diff := cmp.Diff(pc.args, expected); diff != "" { 197 | t.Errorf("%T differ (-got, +want): %s", test.expected, diff) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func TestDeterminePythonMajorMinor(t *testing.T) { 204 | tests := []struct { 205 | description string 206 | env env 207 | commands commands 208 | shouldErr bool 209 | major int 210 | minor int 211 | }{ 212 | {description: "2.7", commands: RunCmdOut([]string{"python", "-V"}, "Python 2.7.8"), major: 2, minor: 7}, 213 | {description: "2.7 and newline", commands: RunCmdOut([]string{"python", "-V"}, "Python 2.7.2\n"), major: 2, minor: 7}, 214 | {description: "3.9 and newline", commands: RunCmdOut([]string{"python", "-V"}, "Python 3.9.14\n"), major: 3, minor: 9}, 215 | {description: "4.13 from env", env: env{"WRAPPER_PYTHON_VERSION": "4.13.8888"}, major: 4, minor: 13}, 216 | {description: "error", commands: RunCmdOutFail([]string{"python", "-V"}, "", 1), shouldErr: true, major: -1, minor: -1}, 217 | } 218 | 219 | for _, test := range tests { 220 | t.Run(test.description, func(t *testing.T) { 221 | test.commands.Setup(t) 222 | major, minor, err := determinePythonMajorMinor(context.TODO(), "python", test.env) 223 | if test.shouldErr && err == nil { 224 | t.Error("expected an error") 225 | } else if !test.shouldErr && err != nil { 226 | t.Error("unexpected error:", err) 227 | } 228 | if test.major != major { 229 | t.Errorf("expected major %d but got %d", test.major, major) 230 | } 231 | if test.minor != minor { 232 | t.Errorf("expected minor %d but got %d", test.minor, minor) 233 | } 234 | }) 235 | } 236 | } 237 | 238 | func TestPrepare(t *testing.T) { 239 | dbgRoot = t.TempDir() 240 | 241 | tests := []struct { 242 | description string 243 | pc pythonContext 244 | commands commands 245 | shouldFail bool 246 | expected pythonContext 247 | }{ 248 | { 249 | description: "debugpy", 250 | pc: pythonContext{debugMode: "debugpy", port: 2345, wait: false, args: []string{"python", "app.py"}, env: nil}, 251 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 252 | AndRunCmd([]string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}), 253 | expected: pythonContext{debugMode: "debugpy", port: 2345, wait: false, major: 3, minor: 7, args: []string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 254 | }, 255 | { 256 | description: "debugpy with module", 257 | pc: pythonContext{debugMode: "debugpy", port: 2345, wait: false, args: []string{"python", "-m", "gunicorn", "app:app"}, env: nil}, 258 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 259 | AndRunCmd([]string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}), 260 | expected: pythonContext{debugMode: "debugpy", port: 2345, wait: false, major: 3, minor: 7, args: []string{"python", "-m", "debugpy", "--listen", "2345", "-m", "gunicorn", "app:app"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 261 | }, 262 | { 263 | description: "debugpy with module (no space)", 264 | pc: pythonContext{debugMode: "debugpy", port: 2345, wait: false, args: []string{"python", "-mgunicorn", "app:app"}, env: nil}, 265 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 266 | AndRunCmd([]string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}), 267 | expected: pythonContext{debugMode: "debugpy", port: 2345, wait: false, major: 3, minor: 7, args: []string{"python", "-m", "debugpy", "--listen", "2345", "-m", "gunicorn", "app:app"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 268 | }, 269 | { 270 | description: "debugpy with wait", 271 | pc: pythonContext{debugMode: "debugpy", port: 2345, wait: true, args: []string{"python", "app.py"}, env: nil}, 272 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 273 | AndRunCmd([]string{"python", "-m", "debugpy", "--listen", "2345", "--wait-for-client", "app.py"}), 274 | expected: pythonContext{debugMode: "debugpy", port: 2345, wait: true, major: 3, minor: 7, args: []string{"python", "-m", "debugpy", "--listen", "2345", "--wait-for-client", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 275 | }, 276 | { 277 | description: "ptvsd", 278 | pc: pythonContext{debugMode: "ptvsd", port: 2345, wait: false, args: []string{"python", "app.py"}, env: nil}, 279 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 280 | AndRunCmd([]string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "app.py"}), 281 | expected: pythonContext{debugMode: "ptvsd", port: 2345, wait: false, major: 3, minor: 7, args: []string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 282 | }, 283 | { 284 | description: "ptvsd with wait", 285 | pc: pythonContext{debugMode: "ptvsd", port: 2345, wait: true, args: []string{"python", "app.py"}, env: nil}, 286 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 287 | AndRunCmd([]string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "--wait", "app.py"}), 288 | expected: pythonContext{debugMode: "ptvsd", port: 2345, wait: true, major: 3, minor: 7, args: []string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "--wait", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/lib/python3.7/site-packages"}}, 289 | }, 290 | { 291 | description: "pydevd", 292 | pc: pythonContext{debugMode: "pydevd", port: 2345, wait: false, args: []string{"python", "app.py"}, env: nil}, 293 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 294 | AndRunCmd([]string{"python", "-m", "pydevd", "--server", "--port", "2345", "--continue", "--file", "app.py"}), 295 | expected: pythonContext{debugMode: "pydevd", port: 2345, wait: false, major: 3, minor: 7, args: []string{"python", "-m", "pydevd", "--server", "--port", "2345", "--continue", "--file", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/pydevd/python3.7/lib/python3.7/site-packages"}}, 296 | }, 297 | { 298 | description: "pydevd with wait", 299 | pc: pythonContext{debugMode: "pydevd", port: 2345, wait: true, args: []string{"python", "app.py"}, env: nil}, 300 | commands: RunCmdOut([]string{"python", "-V"}, "Python 3.7.4\n"). 301 | AndRunCmd([]string{"python", "-m", "pydevd", "--server", "--port", "2345", "--file", "app.py"}), 302 | expected: pythonContext{debugMode: "pydevd", port: 2345, wait: true, major: 3, minor: 7, args: []string{"python", "-m", "pydevd", "--server", "--port", "2345", "--file", "app.py"}, env: env{"PYTHONPATH": dbgRoot + "/python/pydevd/python3.7/lib/python3.7/site-packages"}}, 303 | }, 304 | { 305 | description: "WRAPPER_ENABLED=false", 306 | pc: pythonContext{debugMode: "pydevd", port: 2345, wait: true, args: []string{"python", "app.py"}, env: map[string]string{"WRAPPER_ENABLED": "false"}}, 307 | shouldFail: true, 308 | expected: pythonContext{debugMode: "pydevd", port: 2345, wait: true, args: []string{"python", "app.py"}, env: map[string]string{"WRAPPER_ENABLED": "false"}}, 309 | }, 310 | { 311 | description: "already configured with debugpy", 312 | pc: pythonContext{debugMode: "debugpy", port: 2345, wait: false, args: []string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}}, 313 | shouldFail: true, 314 | expected: pythonContext{debugMode: "debugpy", port: 2345, wait: false, args: []string{"python", "-m", "debugpy", "--listen", "2345", "app.py"}}, 315 | }, 316 | { 317 | description: "already configured with ptvsd", 318 | pc: pythonContext{debugMode: "ptvsd", port: 2345, wait: true, args: []string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "--wait", "app.py"}}, 319 | shouldFail: true, 320 | expected: pythonContext{debugMode: "ptvsd", port: 2345, wait: true, args: []string{"python", "-m", "ptvsd", "--host", "localhost", "--port", "2345", "--wait", "app.py"}}, 321 | }, 322 | { 323 | description: "already configured with pydevd", 324 | pc: pythonContext{debugMode: "pydevd", port: 2345, wait: true, args: []string{"python", "-m", "pydevd", "--server", "--port", "2345", "--file", "app.py"}}, 325 | shouldFail: true, 326 | expected: pythonContext{debugMode: "pydevd", port: 2345, wait: true, args: []string{"python", "-m", "pydevd", "--server", "--port", "2345", "--file", "app.py"}}, 327 | }, 328 | } 329 | 330 | for _, test := range tests { 331 | t.Run(test.description, func(t *testing.T) { 332 | test.commands.Setup(t) 333 | pc := test.pc 334 | result := pc.prepare(context.TODO()) 335 | 336 | if test.shouldFail && result == true { 337 | t.Error("prepare() should have failed") 338 | } else if !test.shouldFail && !result { 339 | t.Error("prepare() should have succeeded") 340 | } else if diff := cmp.Diff(test.expected, pc, cmp.AllowUnexported(test.expected)); diff != "" { 341 | _t.Errorf("%T differ (-got, +want): %s", pc, diff) 342 | } 343 | }) 344 | } 345 | } 346 | 347 | func TestPathExists(t *testing.T) { 348 | if pathExists(filepath.Join("this", "should", "not", "exist")) { 349 | t.Error("pathExists should have failed on non-existent path") 350 | } 351 | if !pathExists(t.TempDir()) { 352 | t.Error("pathExists failed on real path") 353 | } 354 | } 355 | 356 | func TestHandlePydevModule(t *testing.T) { 357 | tmp := os.TempDir() 358 | 359 | tests := []struct { 360 | description string 361 | args []string 362 | shouldErr bool 363 | module string 364 | file string 365 | remaining []string 366 | }{ 367 | { 368 | description: "plain file", 369 | args: []string{"app.py"}, 370 | file: "app.py", 371 | }, 372 | { 373 | description: "-mmodule", 374 | args: []string{"-mmodule"}, 375 | file: filepath.Join(tmp, "*", "skaffold_pydevd_launch.py"), 376 | }, 377 | { 378 | description: "-m module", 379 | args: []string{"-m", "module"}, 380 | file: filepath.Join(tmp, "*", "skaffold_pydevd_launch.py"), 381 | }, 382 | { 383 | description: "- should error", 384 | args: []string{"-", "module"}, 385 | shouldErr: true, 386 | }, 387 | { 388 | description: "-x should error", 389 | args: []string{"-x", "module"}, 390 | shouldErr: true, 391 | }, 392 | { 393 | description: "lone -m should error", 394 | args: []string{"-m"}, 395 | shouldErr: true, 396 | }, 397 | { 398 | description: "no args should error", 399 | shouldErr: true, 400 | }, 401 | } 402 | for _, test := range tests { 403 | t.Run(test.description, func(t *testing.T) { 404 | file, args, err := handlePydevModule(test.args) 405 | if test.shouldErr { 406 | if err == nil { 407 | t.Error("Expected an error") 408 | } 409 | } else { 410 | if !fileMatch(t, test.file, file) { 411 | t.Errorf("Wanted %q but got %q", test.file, file) 412 | } 413 | if diff := cmp.Diff(args, test.remaining, cmpopts.EquateEmpty()); diff != "" { 414 | t.Errorf("remaining args %T differ (-got, +want): %s", test.remaining, diff) 415 | } 416 | } 417 | }) 418 | } 419 | } 420 | 421 | func fileMatch(t *testing.T, glob, file string) bool { 422 | if file == glob { 423 | return true 424 | } 425 | matches, err := filepath.Glob(glob) 426 | if err != nil { 427 | t.Errorf("Failed to expand globe %q: %v", glob, err) 428 | return false 429 | } 430 | for _, m := range matches { 431 | if file == m { 432 | return true 433 | } 434 | } 435 | return false 436 | } 437 | -------------------------------------------------------------------------------- /python/helper-image/pydevd_2_8_0.patch: -------------------------------------------------------------------------------- 1 | diff --git _pydevd_bundle/pydevd_command_line_handling.py _pydevd_bundle/pydevd_command_line_handling.py 2 | index 2afae09..2985a35 100644 3 | --- _pydevd_bundle/pydevd_command_line_handling.py 4 | +++ _pydevd_bundle/pydevd_command_line_handling.py 5 | @@ -69,6 +69,7 @@ ACCEPTED_ARG_HANDLERS = [ 6 | ArgHandlerWithParam('client-access-token'), 7 | 8 | ArgHandlerBool('server'), 9 | + ArgHandlerBool('continue'), 10 | ArgHandlerBool('DEBUG_RECORD_SOCKET_READS'), 11 | ArgHandlerBool('multiproc'), # Used by PyCharm (reuses connection: ssh tunneling) 12 | ArgHandlerBool('multiprocess'), # Used by PyDev (creates new connection to ide) 13 | diff --git pydevd.py pydevd.py 14 | index 4639778..9ecfec0 100644 15 | --- pydevd.py 16 | +++ pydevd.py 17 | @@ -1376,6 +1376,8 @@ class PyDB(object): 18 | 19 | def run(self): 20 | host = SetupHolder.setup['client'] 21 | + if host is None: 22 | + host = '' 23 | port = SetupHolder.setup['port'] 24 | 25 | self._server_socket = create_server_socket(host=host, port=port) 26 | @@ -2240,7 +2242,7 @@ class PyDB(object): 27 | from _pydev_bundle.pydev_monkey import patch_thread_modules 28 | patch_thread_modules() 29 | 30 | - def run(self, file, globals=None, locals=None, is_module=False, set_trace=True): 31 | + def run(self, file, globals=None, locals=None, is_module=False, set_trace=True, wait=True): 32 | module_name = None 33 | entry_point_fn = '' 34 | if is_module: 35 | @@ -2322,7 +2324,8 @@ class PyDB(object): 36 | sys.path.insert(0, os.path.split(os_path_abspath(file))[0]) 37 | 38 | if set_trace: 39 | - self.wait_for_ready_to_run() 40 | + if wait: 41 | + self.wait_for_ready_to_run() 42 | 43 | # call prepare_to_run when we already have all information about breakpoints 44 | self.prepare_to_run() 45 | @@ -3276,14 +3279,21 @@ def main(): 46 | 47 | apply_debugger_options(setup) 48 | 49 | + wait = True 50 | + if setup['continue']: 51 | + wait = False 52 | + 53 | try: 54 | - debugger.connect(host, port) 55 | + if wait: 56 | + debugger.connect(host, port) 57 | + else: 58 | + debugger.create_wait_for_connection_thread() 59 | except: 60 | sys.stderr.write("Could not connect to %s: %s\n" % (host, port)) 61 | pydev_log.exception() 62 | sys.exit(1) 63 | 64 | - globals = debugger.run(setup['file'], None, None, is_module) 65 | + globals = debugger.run(setup['file'], None, None, is_module, wait=wait) 66 | 67 | if setup['cmd-line']: 68 | debugger.wait_for_commands(globals) 69 | -------------------------------------------------------------------------------- /python/helper-image/pydevd_2_9_5.patch: -------------------------------------------------------------------------------- 1 | diff --git _pydevd_bundle/pydevd_command_line_handling.py _pydevd_bundle/pydevd_command_line_handling.py 2 | index b46c98b..cc858b9 100644 3 | --- _pydevd_bundle/pydevd_command_line_handling.py 4 | +++ _pydevd_bundle/pydevd_command_line_handling.py 5 | @@ -76,6 +76,7 @@ ACCEPTED_ARG_HANDLERS = [ 6 | ArgHandlerWithParam('log-level', int, None), 7 | 8 | ArgHandlerBool('server'), 9 | + ArgHandlerBool('continue'), 10 | ArgHandlerBool('multiproc'), # Used by PyCharm (reuses connection: ssh tunneling) 11 | ArgHandlerBool('multiprocess'), # Used by PyDev (creates new connection to ide) 12 | ArgHandlerBool('save-signatures'), 13 | diff --git pydevd.py pydevd.py 14 | index ae865b1..8751621 100644 15 | --- pydevd.py 16 | +++ pydevd.py 17 | @@ -1453,6 +1453,8 @@ class PyDB(object): 18 | 19 | def run(self): 20 | host = SetupHolder.setup['client'] 21 | + if host is None: 22 | + host = '' 23 | port = SetupHolder.setup['port'] 24 | 25 | self._server_socket = create_server_socket(host=host, port=port) 26 | @@ -2391,7 +2393,7 @@ class PyDB(object): 27 | from _pydev_bundle.pydev_monkey import patch_thread_modules 28 | patch_thread_modules() 29 | 30 | - def run(self, file, globals=None, locals=None, is_module=False, set_trace=True): 31 | + def run(self, file, globals=None, locals=None, is_module=False, set_trace=True, wait=True): 32 | module_name = None 33 | entry_point_fn = '' 34 | if is_module: 35 | @@ -2473,7 +2475,8 @@ class PyDB(object): 36 | sys.path.insert(0, os.path.split(os_path_abspath(file))[0]) 37 | 38 | if set_trace: 39 | - self.wait_for_ready_to_run() 40 | + if wait: 41 | + self.wait_for_ready_to_run() 42 | 43 | # call prepare_to_run when we already have all information about breakpoints 44 | self.prepare_to_run() 45 | @@ -3472,14 +3475,21 @@ def main(): 46 | 47 | apply_debugger_options(setup) 48 | 49 | + wait = True 50 | + if setup['continue']: 51 | + wait = False 52 | + 53 | try: 54 | - debugger.connect(host, port) 55 | + if wait: 56 | + debugger.connect(host, port) 57 | + else: 58 | + debugger.create_wait_for_connection_thread() 59 | except: 60 | sys.stderr.write("Could not connect to %s: %s\n" % (host, port)) 61 | pydev_log.exception() 62 | sys.exit(1) 63 | 64 | - globals = debugger.run(setup['file'], None, None, is_module) 65 | + globals = debugger.run(setup['file'], None, None, is_module, wait=wait) 66 | 67 | if setup['cmd-line']: 68 | debugger.wait_for_commands(globals) 69 | -------------------------------------------------------------------------------- /python/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: python 5 | 6 | requires: 7 | - path: ../integration 8 | activeProfiles: 9 | - name: integration 10 | activatedBy: [integration] 11 | 12 | build: 13 | local: 14 | useBuildkit: true 15 | artifacts: 16 | - image: skaffold-debug-python 17 | context: helper-image 18 | custom: 19 | buildCommand: ../../hack/buildx.sh 20 | 21 | test: 22 | - image: skaffold-debug-python 23 | structureTests: [structure-tests-python.yaml] 24 | # Disabled custom test pending Skaffold #5665 and #5666 25 | ##custom: 26 | ## - command: "cd helper-image/launcher; go test ." 27 | ## dependencies: 28 | ## paths: ["helper-image/launcher/*.go"] 29 | 30 | deploy: 31 | logs: 32 | prefix: auto 33 | kubectl: 34 | manifests: [] 35 | 36 | profiles: 37 | 38 | # local: never push to remote registries 39 | - name: local 40 | build: 41 | local: 42 | push: false 43 | 44 | # integration: set of `skaffold debug`-like integration tests 45 | - name: integration 46 | patches: 47 | - op: add 48 | path: /build/artifacts/- 49 | value: 50 | image: python39app 51 | context: test/pythonapp 52 | docker: 53 | buildArgs: 54 | PYTHONVERSION: "3.9" 55 | - op: add 56 | path: /build/artifacts/- 57 | value: 58 | image: python3_10app 59 | context: test/pythonapp 60 | docker: 61 | buildArgs: 62 | PYTHONVERSION: "3.10" 63 | - op: add 64 | path: /build/artifacts/- 65 | value: 66 | image: python3_11app 67 | context: test/pythonapp 68 | docker: 69 | buildArgs: 70 | PYTHONVERSION: "3.11" 71 | - op: add 72 | path: /build/artifacts/- 73 | value: 74 | image: pydevconnect 75 | context: test/pydevconnect 76 | deploy: 77 | kubectl: 78 | manifests: 79 | - test/k8s-test-pydevd-python39.yaml 80 | - test/k8s-test-pydevd-python3_10.yaml 81 | - test/k8s-test-pydevd-python3_11.yaml 82 | 83 | # release: pushes images to production with :latest 84 | - name: release 85 | build: 86 | local: 87 | push: true 88 | tagPolicy: 89 | sha256: {} 90 | 91 | # deprecated-names: use short (deprecated) image names: images were 92 | # prefixed with `skaffold-debug-` so they were more easily distinguished 93 | # from other images with similar names. 94 | - name: deprecated-names 95 | patches: 96 | - op: replace 97 | path: /build/artifacts/0/image 98 | from: skaffold-debug-python 99 | value: python 100 | - op: replace 101 | path: /test/0/image 102 | from: skaffold-debug-python 103 | value: python 104 | -------------------------------------------------------------------------------- /python/structure-tests-python.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: 2.0.0 2 | 3 | fileExistenceTests: 4 | - name: 'ptvsd for python 2.7' 5 | path: '/duct-tape/python/lib/python2.7/site-packages/ptvsd/__init__.py' 6 | - name: 'debugpy for python 2.7' 7 | path: '/duct-tape/python/lib/python2.7/site-packages/debugpy/__init__.py' 8 | - name: 'pydevd for python 2.7' 9 | path: '/duct-tape/python/pydevd/python2.7/lib/python2.7/site-packages/pydevd.py' 10 | - name: 'pydevd-pycharm for python 2.7' 11 | path: '/duct-tape/python/pydevd-pycharm/python2.7/lib/python2.7/site-packages/pydevd.py' 12 | 13 | - name: 'ptvsd for python 3.5' 14 | path: '/duct-tape/python/lib/python3.5/site-packages/ptvsd/__init__.py' 15 | - name: 'debugpy for python 3.5' 16 | path: '/duct-tape/python/lib/python3.5/site-packages/debugpy/__init__.py' 17 | - name: 'pydevd for python 3.5' 18 | path: '/duct-tape/python/pydevd/python3.5/lib/python3.5/site-packages/pydevd.py' 19 | - name: 'pydevd-pycharm for python 3.5' 20 | path: '/duct-tape/python/pydevd-pycharm/python3.5/lib/python3.5/site-packages/pydevd.py' 21 | 22 | - name: 'ptvsd for python 3.6' 23 | path: '/duct-tape/python/lib/python3.6/site-packages/ptvsd/__init__.py' 24 | - name: 'debugpy for python 3.6' 25 | path: '/duct-tape/python/lib/python3.6/site-packages/debugpy/__init__.py' 26 | - name: 'pydevd for python 3.6' 27 | path: '/duct-tape/python/pydevd/python3.6/lib/python3.6/site-packages/pydevd.py' 28 | - name: 'pydevd-pycharm for python 3.6' 29 | path: '/duct-tape/python/pydevd-pycharm/python3.6/lib/python3.6/site-packages/pydevd.py' 30 | 31 | - name: 'ptvsd for python 3.7' 32 | path: '/duct-tape/python/lib/python3.7/site-packages/ptvsd/__init__.py' 33 | - name: 'debugpy for python 3.7' 34 | path: '/duct-tape/python/lib/python3.7/site-packages/debugpy/__init__.py' 35 | - name: 'pydevd for python 3.7' 36 | path: '/duct-tape/python/pydevd/python3.7/lib/python3.7/site-packages/pydevd.py' 37 | - name: 'pydevd-pycharm for python 3.7' 38 | path: '/duct-tape/python/pydevd-pycharm/python3.7/lib/python3.7/site-packages/pydevd.py' 39 | 40 | - name: 'ptvsd for python 3.8' 41 | path: '/duct-tape/python/lib/python3.8/site-packages/ptvsd/__init__.py' 42 | - name: 'debugpy for python 3.8' 43 | path: '/duct-tape/python/lib/python3.8/site-packages/debugpy/__init__.py' 44 | - name: 'pydevd for python 3.8' 45 | path: '/duct-tape/python/pydevd/python3.8/lib/python3.8/site-packages/pydevd.py' 46 | - name: 'pydevd-pycharm for python 3.8' 47 | path: '/duct-tape/python/pydevd-pycharm/python3.8/lib/python3.8/site-packages/pydevd.py' 48 | 49 | - name: 'ptvsd for python 3.9' 50 | path: '/duct-tape/python/lib/python3.9/site-packages/ptvsd/__init__.py' 51 | - name: 'debugpy for python 3.9' 52 | path: '/duct-tape/python/lib/python3.9/site-packages/debugpy/__init__.py' 53 | - name: 'pydevd for python 3.9' 54 | path: '/duct-tape/python/pydevd/python3.9/lib/python3.9/site-packages/pydevd.py' 55 | - name: 'pydevd-pycharm for python 3.9' 56 | path: '/duct-tape/python/pydevd-pycharm/python3.9/lib/python3.9/site-packages/pydevd.py' 57 | 58 | - name: 'ptvsd for python 3.10' 59 | path: '/duct-tape/python/lib/python3.10/site-packages/ptvsd/__init__.py' 60 | - name: 'debugpy for python 3.10' 61 | path: '/duct-tape/python/lib/python3.10/site-packages/debugpy/__init__.py' 62 | - name: 'pydevd for python 3.10' 63 | path: '/duct-tape/python/pydevd/python3.10/lib/python3.10/site-packages/pydevd.py' 64 | - name: 'pydevd-pycharm for python 3.10' 65 | path: '/duct-tape/python/pydevd-pycharm/python3.10/lib/python3.10/site-packages/pydevd.py' 66 | 67 | - name: 'python launcher' 68 | path: '/duct-tape/python/launcher' 69 | isExecutableBy: any 70 | 71 | commandTests: 72 | - name: "run with no /dbg should fail" 73 | command: "sh" 74 | args: ["/install.sh"] 75 | expectedError: ["Error: installation requires a volume mount at /dbg"] 76 | exitCode: 1 77 | - name: "run with /dbg should install" 78 | setup: [["mkdir", "/dbg"]] 79 | command: "sh" 80 | args: ["/install.sh"] 81 | expectedOutput: ["Installing runtime debugging support files in /dbg", "Installation complete"] 82 | exitCode: 0 83 | -------------------------------------------------------------------------------- /python/test/k8s-test-pydevd-python39.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: python39pod 5 | labels: 6 | app: hello 7 | protocol: pydevd 8 | runtime: python39 9 | spec: 10 | containers: 11 | - name: python39app 12 | image: python39app 13 | command: ["/dbg/python/launcher", "--mode", "pydevd", "--port", "12345", "--"] 14 | args: ["python", "-m", "flask", "run", "--host=0.0.0.0"] 15 | ports: 16 | - containerPort: 5000 17 | - containerPort: 12345 18 | name: pydevd 19 | env: 20 | - name: WRAPPER_VERBOSE 21 | value: debug 22 | readinessProbe: 23 | httpGet: 24 | path: / 25 | port: 5000 26 | volumeMounts: 27 | - mountPath: /dbg 28 | name: python-debugging-support 29 | initContainers: 30 | - image: skaffold-debug-python 31 | name: install-python-support 32 | resources: {} 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: python-debugging-support 36 | volumes: 37 | - emptyDir: {} 38 | name: python-debugging-support 39 | 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: hello-pydevd-python39 45 | spec: 46 | ports: 47 | - name: http 48 | port: 5000 49 | protocol: TCP 50 | - name: pydevd 51 | port: 12345 52 | protocol: TCP 53 | selector: 54 | app: hello 55 | protocol: pydevd 56 | runtime: python39 57 | 58 | --- 59 | apiVersion: batch/v1 60 | kind: Job 61 | metadata: 62 | name: connect-to-python39 63 | labels: 64 | project: container-debug-support 65 | type: integration-test 66 | spec: 67 | ttlSecondsAfterFinished: 10 68 | backoffLimit: 1 69 | template: 70 | spec: 71 | restartPolicy: Never 72 | containers: 73 | - name: verify-python39 74 | image: pydevconnect 75 | args: ["hello-pydevd-python39:12345"] 76 | 77 | 78 | -------------------------------------------------------------------------------- /python/test/k8s-test-pydevd-python3_10.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: python3-10pod 5 | labels: 6 | app: hello 7 | protocol: pydevd 8 | runtime: python3_10 9 | spec: 10 | containers: 11 | - name: python3-10app 12 | image: python3_10app 13 | command: ["/dbg/python/launcher", "--mode", "pydevd", "--port", "12345", "--"] 14 | args: ["python", "-m", "flask", "run", "--host=0.0.0.0"] 15 | ports: 16 | - containerPort: 5000 17 | - containerPort: 12345 18 | name: pydevd 19 | env: 20 | - name: WRAPPER_VERBOSE 21 | value: debug 22 | readinessProbe: 23 | httpGet: 24 | path: / 25 | port: 5000 26 | volumeMounts: 27 | - mountPath: /dbg 28 | name: python-debugging-support 29 | initContainers: 30 | - image: skaffold-debug-python 31 | name: install-python-support 32 | resources: {} 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: python-debugging-support 36 | volumes: 37 | - emptyDir: {} 38 | name: python-debugging-support 39 | 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: hello-pydevd-python3-10 45 | spec: 46 | ports: 47 | - name: http 48 | port: 5000 49 | protocol: TCP 50 | - name: pydevd 51 | port: 12345 52 | protocol: TCP 53 | selector: 54 | app: hello 55 | protocol: pydevd 56 | runtime: python3_10 57 | 58 | --- 59 | apiVersion: batch/v1 60 | kind: Job 61 | metadata: 62 | name: connect-to-python3-10 63 | labels: 64 | project: container-debug-support 65 | type: integration-test 66 | spec: 67 | ttlSecondsAfterFinished: 10 68 | backoffLimit: 1 69 | template: 70 | spec: 71 | restartPolicy: Never 72 | containers: 73 | - name: verify-python3-10 74 | image: pydevconnect 75 | args: ["hello-pydevd-python3-10:12345"] 76 | 77 | 78 | -------------------------------------------------------------------------------- /python/test/k8s-test-pydevd-python3_11.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: python3-11pod 5 | labels: 6 | app: hello 7 | protocol: pydevd 8 | runtime: python3_11 9 | spec: 10 | containers: 11 | - name: python3-11app 12 | image: python3_11app 13 | command: ["/dbg/python/launcher", "--mode", "pydevd", "--port", "12345", "--"] 14 | args: ["python", "-m", "flask", "run", "--host=0.0.0.0"] 15 | ports: 16 | - containerPort: 5000 17 | - containerPort: 12345 18 | name: pydevd 19 | env: 20 | - name: WRAPPER_VERBOSE 21 | value: debug 22 | readinessProbe: 23 | httpGet: 24 | path: / 25 | port: 5000 26 | volumeMounts: 27 | - mountPath: /dbg 28 | name: python-debugging-support 29 | initContainers: 30 | - image: skaffold-debug-python 31 | name: install-python-support 32 | resources: {} 33 | volumeMounts: 34 | - mountPath: /dbg 35 | name: python-debugging-support 36 | volumes: 37 | - emptyDir: {} 38 | name: python-debugging-support 39 | 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: hello-pydevd-python3-11 45 | spec: 46 | ports: 47 | - name: http 48 | port: 5000 49 | protocol: TCP 50 | - name: pydevd 51 | port: 12345 52 | protocol: TCP 53 | selector: 54 | app: hello 55 | protocol: pydevd 56 | runtime: python3_11 57 | 58 | --- 59 | apiVersion: batch/v1 60 | kind: Job 61 | metadata: 62 | name: connect-to-python3-11 63 | labels: 64 | project: container-debug-support 65 | type: integration-test 66 | spec: 67 | ttlSecondsAfterFinished: 10 68 | backoffLimit: 1 69 | template: 70 | spec: 71 | restartPolicy: Never 72 | containers: 73 | - name: verify-python3-11 74 | image: pydevconnect 75 | args: ["hello-pydevd-python3-11:12345"] 76 | 77 | 78 | -------------------------------------------------------------------------------- /python/test/pydevconnect/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Skaffold Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This Dockerfile creates a test image for verifying that pydevd is 16 | # running somewhere. 17 | 18 | FROM golang:1.23 as build 19 | COPY . . 20 | RUN CGO_ENABLED=0 go build -o pydevconnect -ldflags '-s -w -extldflags "-static"' pydevconnect.go 21 | 22 | # Now populate the test image 23 | FROM busybox 24 | COPY --from=build /go/pydevconnect / 25 | ENTRYPOINT ["/pydevconnect"] 26 | -------------------------------------------------------------------------------- /python/test/pydevconnect/pydevconnect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Skaffold Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Test utility to connect to a pydevd server to validate that it is working. 18 | // Protocol: https://github.com/fabioz/PyDev.Debugger/blob/main/_pydevd_bundle/pydevd_comm.py 19 | package main 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "log" 25 | "net" 26 | "net/url" 27 | "os" 28 | "strconv" 29 | "strings" 30 | "time" 31 | ) 32 | 33 | const ( 34 | CMD_RUN = 101 35 | CMD_LIST_THREADS = 102 36 | CMD_THREAD_CREATE = 103 37 | CMD_THREAD_KILL = 104 38 | CMD_THREAD_RUN = 106 39 | CMD_SET_BREAK = 111 40 | CMD_WRITE_TO_CONSOLE = 116 41 | CMD_VERSION = 501 42 | CMD_RETURN = 502 43 | CMD_ERROR = 901 44 | ) 45 | 46 | func main() { 47 | if len(os.Args) != 2 { 48 | fmt.Printf("Check that pydevd is running.\n") 49 | fmt.Printf("use: %s host:port\n", os.Args[0]) 50 | os.Exit(1) 51 | } 52 | 53 | var conn net.Conn 54 | for i := 0; i < 60; i++ { 55 | var err error 56 | conn, err = net.Dial("tcp", os.Args[1]) 57 | if err == nil { 58 | break 59 | } 60 | fmt.Printf("(sleeping) unable to connect to %s: %v\n", os.Args[1], err) 61 | time.Sleep(2 * time.Second) 62 | } 63 | 64 | pydb := newPydevdDebugConnection(conn) 65 | 66 | code, response := pydb.makeRequestWithResponse(CMD_VERSION, "pydevconnect") 67 | if code != CMD_VERSION { 68 | log.Fatalf("expected CMD_VERSION (%d) response (%q)", code, response) 69 | } 70 | if decoded, err := url.QueryUnescape(response); err != nil { 71 | log.Fatalf("CMD_VERSION response (%q): decoding error: %v", response, err) 72 | } else { 73 | fmt.Printf("version: %s", decoded) 74 | } 75 | 76 | pydb.makeRequest(CMD_RUN, "test") 77 | } 78 | 79 | type pydevdDebugConnection struct { 80 | conn net.Conn 81 | reader *bufio.Reader 82 | msgID int 83 | } 84 | 85 | func newPydevdDebugConnection(c net.Conn) *pydevdDebugConnection { 86 | return &pydevdDebugConnection{ 87 | conn: c, 88 | reader: bufio.NewReader(c), 89 | msgID: 1, 90 | } 91 | } 92 | 93 | func (c *pydevdDebugConnection) makeRequest(code int, arg string) { 94 | currMsgID := c.msgID 95 | c.msgID += 2 // outgoing requests should have odd msgID 96 | 97 | fmt.Printf("Making request: code=%d msgId=%d arg=%q\n", code, currMsgID, arg) 98 | fmt.Fprintf(c.conn, "%d\t%d\t%s\n", code, currMsgID, arg) 99 | } 100 | 101 | func (c *pydevdDebugConnection) makeRequestWithResponse(code int, arg string) (int, string) { 102 | currMsgID := c.msgID 103 | c.msgID += 2 // outgoing requests should have odd msgID 104 | 105 | fmt.Printf("Making request: code=%d msgId=%d arg=%q\n", code, currMsgID, arg) 106 | fmt.Fprintf(c.conn, "%d\t%d\t%s\n", code, currMsgID, arg) 107 | 108 | for { 109 | response, err := c.reader.ReadString('\n') 110 | if err != nil { 111 | log.Fatalf("error receiving response: %v", err) 112 | } 113 | fmt.Printf("Received response: %q\n", response) 114 | 115 | // check response 116 | tsv := strings.Split(response, "\t") 117 | if len(tsv) != 3 { 118 | log.Fatalf("invalid response: expecting three tab-separated components: %q", response) 119 | } 120 | 121 | code, err = strconv.Atoi(tsv[0]) 122 | if err != nil { 123 | log.Fatalf("could not parse response code: %q", tsv[0]) 124 | } 125 | 126 | responseID, err := strconv.Atoi(tsv[1]) 127 | if err != nil { 128 | log.Fatalf("could not parse response ID: %q", tsv[1]) 129 | } else if responseID == currMsgID { 130 | return code, tsv[2] 131 | } 132 | 133 | // handle commands sent to us 134 | switch code { 135 | case CMD_THREAD_CREATE: 136 | fmt.Printf("CMD_THREAD_CREATE: %s\n", tsv[2:]) 137 | 138 | default: 139 | log.Fatalf("Unknown/unhandled code %d: %q", code, tsv[2:]) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /python/test/pythonapp/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHONVERSION 2 | FROM python:${PYTHONVERSION} 3 | 4 | RUN pip install --upgrade pip 5 | 6 | ARG DEBUG=0 7 | ENV FLASK_DEBUG $DEBUG 8 | ENV FLASK_APP=src/app.py 9 | CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"] 10 | 11 | COPY requirements.txt . 12 | ENV PATH="/home/python/.local/bin:${PATH}" 13 | RUN pip install -r requirements.txt 14 | 15 | COPY src src 16 | -------------------------------------------------------------------------------- /python/test/pythonapp/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | -------------------------------------------------------------------------------- /python/test/pythonapp/src/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def hello_world(): 6 | print("incoming request") 7 | return 'Hello, World from Flask!\n' -------------------------------------------------------------------------------- /run-its.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run Integration Tests 3 | # Can be called either from the top-level directory or from any of 4 | # the language-specific images. 5 | # 6 | # $ sh run-its.sh 7 | # $ (cd go; sh ../run-its.sh) 8 | # 9 | # Integration tests are set up as a set of Jobs. This script launches 10 | # a set of Pods and the Jobs, and then waits for the Jobs to complete. 11 | 12 | set -euo pipefail 13 | 14 | countTestJobs() { 15 | kubectl get job.batch -o name -l project=container-debug-support,type=integration-test \ 16 | | wc -l 17 | } 18 | 19 | echo ">> Building test images [$(date)]" 20 | skaffold build -p integration 21 | 22 | echo ">> Launching test jobs and pods [$(date)]" 23 | skaffold run -p integration --tail & 24 | skaffoldPid=$! 25 | 26 | trap "echo '>> Tearing down test jobs [$(date)]'; kill $skaffoldPid; skaffold delete -p integration" 0 1 3 15 27 | 28 | echo ">> Waiting for test jobs to start [$(date)]" 29 | jobcount=0 30 | while [ $jobcount -eq 0 -o $jobcount -ne $(countTestJobs) ]; do 31 | jobcount=$(countTestJobs) 32 | sleep 5 33 | done 34 | 35 | echo ">> Monitoring for test job completion [$(date)]" 36 | kubectl wait --for=condition=complete job.batch --timeout=120s \ 37 | -l project=container-debug-support,type=integration-test 38 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta13 2 | kind: Config 3 | metadata: 4 | name: container-debug-support 5 | 6 | .YamlAnchors: 7 | _profiles: &profiles 8 | activeProfiles: 9 | - name: local 10 | activatedBy: [local] 11 | - name: integration 12 | activatedBy: [integration] 13 | - name: release 14 | activatedBy: [release] 15 | - name: deprecated-names 16 | activatedBy: [deprecated-names] 17 | 18 | requires: 19 | - path: go 20 | <<: *profiles 21 | - path: netcore 22 | <<: *profiles 23 | - path: nodejs 24 | <<: *profiles 25 | - path: python 26 | <<: *profiles 27 | 28 | profiles: 29 | # local: never push to remote registries 30 | - name: local 31 | 32 | # integration: set of `skaffold debug`-like integration tests 33 | - name: integration 34 | 35 | # release: pushes images to production with :latest 36 | - name: release 37 | 38 | # deprecated-names: use short (deprecated) image names: images were 39 | # prefixed with `skaffold-debug-` so they were more easily distinguished 40 | # from other images with similar names. 41 | - name: deprecated-names 42 | --------------------------------------------------------------------------------