diff --git a/python/skaffold.yaml b/python/skaffold.yaml index 9288803a..9ab690a9 100644 --- a/python/skaffold.yaml +++ b/python/skaffold.yaml @@ -43,6 +43,24 @@ profiles: # integration: set of `skaffold debug`-like integration tests - name: integration + patches: + - op: add + path: /build/artifacts/- + value: + image: python39app + context: test/pythonapp + docker: + buildArgs: + PYTHONVERSION: "3.9" + - op: add + path: /build/artifacts/- + value: + image: pydevconnect + context: test/pydevconnect + deploy: + kubectl: + manifests: + - test/k8s-test-pydevd-python39.yaml # release: pushes images to production with :latest - name: release diff --git a/python/test/k8s-test-pydevd-python39.yaml b/python/test/k8s-test-pydevd-python39.yaml new file mode 100644 index 00000000..696d68ae --- /dev/null +++ b/python/test/k8s-test-pydevd-python39.yaml @@ -0,0 +1,77 @@ +apiVersion: v1 +kind: Pod +metadata: + name: python39pod + labels: + app: hello + protocol: pydevd + runtime: python39 +spec: + containers: + - name: python39app + image: python39app + command: ["/dbg/python/launcher", "--mode", "pydevd", "--port", "12345", "--"] + args: ["python", "-m", "flask", "run", "--host=0.0.0.0"] + ports: + - containerPort: 5000 + - containerPort: 12345 + name: pydevd + env: + - name: WRAPPER_VERBOSE + value: debug + readinessProbe: + httpGet: + path: / + port: 5000 + volumeMounts: + - mountPath: /dbg + name: python-debugging-support + initContainers: + - image: skaffold-debug-python + name: install-python-support + resources: {} + volumeMounts: + - mountPath: /dbg + name: python-debugging-support + volumes: + - emptyDir: {} + name: python-debugging-support + +--- +apiVersion: v1 +kind: Service +metadata: + name: hello-pydevd-python39 +spec: + ports: + - name: http + port: 5000 + protocol: TCP + - name: pydevd + port: 12345 + protocol: TCP + selector: + app: hello + protocol: pydevd + runtime: python39 + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: connect-to-python39 + labels: + project: container-debug-support + type: integration-test +spec: + ttlSecondsAfterFinished: 10 + backoffLimit: 1 + template: + spec: + restartPolicy: Never + containers: + - name: verify-python39 + image: pydevconnect + args: ["hello-pydevd-python39:12345"] + + diff --git a/python/test/pydevconnect/Dockerfile b/python/test/pydevconnect/Dockerfile new file mode 100644 index 00000000..4a08e07f --- /dev/null +++ b/python/test/pydevconnect/Dockerfile @@ -0,0 +1,25 @@ +# Copyright 2021 The Skaffold Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This Dockerfile creates a test image for verifying that pydevd is +# running somewhere. + +FROM golang:1.17 as build +COPY . . +RUN CGO_ENABLED=0 go build -o pydevconnect -ldflags '-s -w -extldflags "-static"' pydevconnect.go + +# Now populate the test image +FROM busybox +COPY --from=build /go/pydevconnect / +ENTRYPOINT ["/pydevconnect"] diff --git a/python/test/pydevconnect/pydevconnect.go b/python/test/pydevconnect/pydevconnect.go new file mode 100644 index 00000000..fc419a74 --- /dev/null +++ b/python/test/pydevconnect/pydevconnect.go @@ -0,0 +1,142 @@ +/* +Copyright 2021 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Test utility to connect to a pydevd server to validate that it is working. +// Protocol: https://github.com/fabioz/PyDev.Debugger/blob/main/_pydevd_bundle/pydevd_comm.py +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +const ( + CMD_RUN = 101 + CMD_LIST_THREADS = 102 + CMD_THREAD_CREATE = 103 + CMD_THREAD_KILL = 104 + CMD_THREAD_RUN = 106 + CMD_SET_BREAK = 111 + CMD_WRITE_TO_CONSOLE = 116 + CMD_VERSION = 501 + CMD_RETURN = 502 + CMD_ERROR = 901 +) + +func main() { + if len(os.Args) != 2 { + fmt.Printf("Check that pydevd is running.\n") + fmt.Printf("use: %s host:port\n", os.Args[0]) + os.Exit(1) + } + + var conn net.Conn + for i := 0; i < 60; i++ { + var err error + conn, err = net.Dial("tcp", os.Args[1]) + if err == nil { + break + } + fmt.Printf("(sleeping) unable to connect to %s: %v\n", os.Args[1], err) + time.Sleep(2 * time.Second) + } + + pydb := newPydevdDebugConnection(conn) + + code, response := pydb.makeRequestWithResponse(CMD_VERSION, "pydevconnect") + if code != CMD_VERSION { + log.Fatalf("expected CMD_VERSION (%d) response (%q)", code, response) + } + if decoded, err := url.QueryUnescape(response); err != nil { + log.Fatalf("CMD_VERSION response (%q): decoding error: %v", response, err) + } else { + fmt.Printf("version: %s", decoded) + } + + pydb.makeRequest(CMD_RUN, "test") +} + +type pydevdDebugConnection struct { + conn net.Conn + reader *bufio.Reader + msgID int +} + +func newPydevdDebugConnection(c net.Conn) *pydevdDebugConnection { + return &pydevdDebugConnection{ + conn: c, + reader: bufio.NewReader(c), + msgID: 1, + } +} + +func (c *pydevdDebugConnection) makeRequest(code int, arg string) { + currMsgID := c.msgID + c.msgID += 2 // outgoing requests should have odd msgID + + fmt.Printf("Making request: code=%d msgId=%d arg=%q\n", code, currMsgID, arg) + fmt.Fprintf(c.conn, "%d\t%d\t%s\n", code, currMsgID, arg) +} + +func (c *pydevdDebugConnection) makeRequestWithResponse(code int, arg string) (int, string) { + currMsgID := c.msgID + c.msgID += 2 // outgoing requests should have odd msgID + + fmt.Printf("Making request: code=%d msgId=%d arg=%q\n", code, currMsgID, arg) + fmt.Fprintf(c.conn, "%d\t%d\t%s\n", code, currMsgID, arg) + + for { + response, err := c.reader.ReadString('\n') + if err != nil { + log.Fatalf("error receiving response: %v", err) + } + fmt.Printf("Received response: %q\n", response) + + // check response + tsv := strings.Split(response, "\t") + if len(tsv) != 3 { + log.Fatalf("invalid response: expecting three tab-separated components: %q", response) + } + + code, err = strconv.Atoi(tsv[0]) + if err != nil { + log.Fatalf("could not parse response code: %q", tsv[0]) + } + + responseID, err := strconv.Atoi(tsv[1]) + if err != nil { + log.Fatalf("could not parse response ID: %q", tsv[1]) + } else if responseID == currMsgID { + return code, tsv[2] + } + + // handle commands sent to us + switch code { + case CMD_THREAD_CREATE: + fmt.Printf("CMD_THREAD_CREATE: %s\n", tsv[2:]) + + default: + log.Fatalf("Unknown/unhandled code %d: %q", code, tsv[2:]) + } + } +} diff --git a/python/test/pythonapp/Dockerfile b/python/test/pythonapp/Dockerfile new file mode 100644 index 00000000..93bb8837 --- /dev/null +++ b/python/test/pythonapp/Dockerfile @@ -0,0 +1,15 @@ +ARG PYTHONVERSION +FROM python:${PYTHONVERSION} + +RUN pip install --upgrade pip + +ARG DEBUG=0 +ENV FLASK_DEBUG $DEBUG +ENV FLASK_APP=src/app.py +CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"] + +COPY requirements.txt . +ENV PATH="/home/python/.local/bin:${PATH}" +RUN pip install -r requirements.txt + +COPY src src diff --git a/python/test/pythonapp/requirements.txt b/python/test/pythonapp/requirements.txt new file mode 100644 index 00000000..e3e9a71d --- /dev/null +++ b/python/test/pythonapp/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/python/test/pythonapp/src/app.py b/python/test/pythonapp/src/app.py new file mode 100644 index 00000000..5a53a86b --- /dev/null +++ b/python/test/pythonapp/src/app.py @@ -0,0 +1,7 @@ +from flask import Flask +app = Flask(__name__) + +@app.route('/') +def hello_world(): + print("incoming request") + return 'Hello, World from Flask!\n' \ No newline at end of file