├── .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 | 
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 |
--------------------------------------------------------------------------------