diff --git a/template/.gitignore b/template/.gitignore index c8d82e0..73c98d4 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -56,7 +56,8 @@ env .envrc.local # vs code -.vscode/ +.vscode/* +!.vscode/launch.json .history/ # terraform files diff --git a/template/.vscode/launch.json b/template/.vscode/launch.json new file mode 100644 index 0000000..8fa74c4 --- /dev/null +++ b/template/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Django (debugpy in k8s)", + "type": "debugpy", + "request": "attach", + "connect": { "host": "127.0.0.1", "port": 5678 }, + "pathMappings": [ + { "localRoot": "${workspaceFolder}/backend", "remoteRoot": "/app/src" } + ], + "justMyCode": false + } + ] +} diff --git a/template/Tiltfile b/template/Tiltfile index 98f0a07..eada5f5 100644 --- a/template/Tiltfile +++ b/template/Tiltfile @@ -55,7 +55,7 @@ syncback( {% if copier__create_nextjs_frontend %} k8s_resource(workload='frontend', port_forwards=3000) {% endif %} -k8s_resource(workload='backend', port_forwards=8000) +k8s_resource(workload='backend', port_forwards=[8000, 5678]) k8s_resource(workload='mailhog', port_forwards=8025) k8s_resource(workload='postgres', port_forwards=5432) {% if copier__use_celery %} diff --git a/template/backend/{{copier__project_slug}}/utils/debugger.py b/template/backend/{{copier__project_slug}}/utils/debugger.py index f47c098..02bed8f 100644 --- a/template/backend/{{copier__project_slug}}/utils/debugger.py +++ b/template/backend/{{copier__project_slug}}/utils/debugger.py @@ -19,4 +19,23 @@ def pycharm_debugger(): def vscode_debugger(): - raise NotImplementedError("VSCode debugger not implemented") + logger.info("VSCode debugpy starting...") + import debugpy + + # debugpy.listen() may only be called once per process; breakpoint() can fire + # multiple times, and runserver's reloader runs request code in the RUN_MAIN + # worker. Guard against re-listen; wait for the IDE to attach before pausing. + if not debugpy.is_client_connected(): + try: + # Bind all interfaces so the port-forward can reach the pod; this + # debug server is dev-only (active only when PYTHONBREAKPOINT is set). + debugpy.listen(("0.0.0.0", 5678)) # nosec B104 + logger.info( + "debugpy listening on 0.0.0.0:5678 - waiting for VSCode to attach..." + ) + except RuntimeError: + logger.info("debugpy already listening; waiting for client.") + debugpy.wait_for_client() + logger.info("VSCode debugger attached.") + + debugpy.breakpoint() diff --git a/template/docs/debug.md b/template/docs/debug.md index 96ea6e7..5fce8a0 100644 --- a/template/docs/debug.md +++ b/template/docs/debug.md @@ -1,9 +1,10 @@ # :bug: How to debug the application -The steps below describe how to set up interactive debugging with PyCharm. +The steps below describe how to set up interactive debugging with PyCharm or VSCode. ## PyCharm Debugging Setup -Update `k8s/base/app.configmap.yaml` with `data` field `PYTHONBREAKPOINT: "utils.pycharm_debugger"` +Set `PYTHONBREAKPOINT: "utils.pycharm_debugger"` in the `app-config` patch in +`k8s/local/kustomization.yaml` (debugger config is local-only so it can't reach sandbox/prod). In PyCharm: @@ -19,11 +20,48 @@ In PyCharm: ![debug__debug_configuration.png](images/debug__debug_configuration.png) +## VSCode Debugging Setup + +VSCode uses [debugpy](https://github.com/microsoft/debugpy) (already in `requirements/local.in`). +Unlike PyCharm, the pod runs the debug *server* and VSCode *attaches* to it, so Tilt +forwards the debug port (`5678`) into the pod for you. + +1. Set `PYTHONBREAKPOINT: "{{ copier__project_slug }}.utils.vscode_debugger"` in the + `app-config` patch in `k8s/local/kustomization.yaml` (debugger config is local-only). +2. Run `tilt up`. Confirm the `backend` resource forwards both `8000` and `5678`. +3. A `.vscode/launch.json` with an "Attach to Django (debugpy in k8s)" configuration is + already included at the project root. + ## Debugging in development -Before the code you want to debug, add the following: + +`PYTHONBREAKPOINT` (set above) routes Python's built-in `breakpoint()` to the debugger for +your IDE. Before the code you want to inspect, add: ```python breakpoint() ``` -You must then set break points in your IDE and call the code as usual to hit them. +Then call the code as usual (e.g. hit the endpoint). What happens when `breakpoint()` is +reached differs by IDE: + +### PyCharm + +Hitting `breakpoint()` only *connects* the debugger — it does **not** suspend on that line +(`suspend=False`). You must set break points in the PyCharm gutter at the lines you want to +stop on; execution pauses when it reaches one of those. + +### VSCode + +`breakpoint()` **is** the stop point — you do not need to set gutter breakpoints: + +1. Add `breakpoint()` and trigger the code. The request **blocks** at that line waiting for + a debugger to attach (debugpy starts listening on the first `breakpoint()` that is hit). +2. In VSCode, open the Run and Debug panel and start "Attach to Django (debugpy in k8s)". + Execution resumes and pauses right at your `breakpoint()`. + +Once attached you *may* optionally set additional gutter breakpoints in VSCode and they'll be +hit on subsequent runs, but a plain `breakpoint()` is sufficient on its own. + +> :warning: Because the request blocks until you attach, a `breakpoint()` you never attach +> to will hang that request indefinitely. This is dev-only (local readiness/liveness probes +> are disabled), but remember to remove `breakpoint()` and reset `PYTHONBREAKPOINT: ""` when done. diff --git a/template/k8s/base/app.configmap.yaml b/template/k8s/base/app.configmap.yaml index 685be10..5ca6416 100644 --- a/template/k8s/base/app.configmap.yaml +++ b/template/k8s/base/app.configmap.yaml @@ -15,14 +15,6 @@ data: POSTGRES_DB: "{{ copier__project_slug }}" POSTGRES_USER: "{{ copier__project_slug }}" REDIS_URL: "redis://redis:6379/1" - # todo: see if PYDEVD_USE_* can be removed later, I was getting this error - # with pydevd-pycharm==243.26053.29: - # cannot import name 'trace_dispatch' from '_pydevd_bundle.pydevd_trace_dispatch' - # (/app/lib/python3.12/site-packages/_pydevd_bundle/pydevd_trace_dispatch.py) - # Also probably need to add cython to local.in in that case. - PYDEVD_USE_CYTHON: "NO" - PYDEVD_USE_FRAME_EVAL: "NO" - PYTHONBREAKPOINT: "" # "utils.pycharm_debugger" for pycharm {%- if copier__mail_service == 'Mailgun' %} MAILGUN_DOMAIN: "{{ copier__domain_name }}" MAILGUN_API_URL: "https://api.mailgun.net/v3"{%- endif %} diff --git a/template/k8s/local/kustomization.yaml b/template/k8s/local/kustomization.yaml index f589994..e70b625 100644 --- a/template/k8s/local/kustomization.yaml +++ b/template/k8s/local/kustomization.yaml @@ -29,9 +29,29 @@ patches: value: - name: DOCKER_GATEWAY_IP # used for debugger to reach out to host machine value: "172.17.0.1" + - op: add + path: /spec/template/spec/containers/0/ports/- + value: + name: debugpy + containerPort: 5678 target: kind: Deployment name: backend +# Debugger config is local-only so it can never reach sandbox/prod. +# To debug, set PYTHONBREAKPOINT below; leave it "" to disable. +- patch: |- + - op: add + path: /data/PYTHONBREAKPOINT + value: "" # "utils.pycharm_debugger" for pycharm, "{{ copier__project_slug }}.utils.vscode_debugger" for vscode + - op: add + path: /data/PYDEVD_USE_CYTHON + value: "NO" + - op: add + path: /data/PYDEVD_USE_FRAME_EVAL + value: "NO" + target: + kind: ConfigMap + name: app-config {%- if copier__use_celery %} - patch: |- - op: add