Skip to content

Commit 015d392

Browse files
Debug support for pydevd, new --protocols debug flag (#5759)
Use the debug python launcher to add support for PyCharm. Also improves debugging of applications using python modules (e.g., gunicorn)
1 parent 7a2ba77 commit 015d392

14 files changed

+176
-98
lines changed

cmd/skaffold/app/cmd/debug.go

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func NewCmdDebug() *cobra.Command {
3737
WithLongDescription("Similar to `dev`, but configures the pipeline for debugging. "+
3838
"Auto-build and sync is disabled by default to prevent accidentally tearing down debug sessions.").
3939
WithCommonFlags().
40+
WithFlags([]*Flag{
41+
{Value: &debugging.Protocols, Name: "protocols", DefValue: []string{}, Usage: "Priority sorted order of debugger protocols to support."},
42+
}).
4043
WithExample("Launch with port-forwarding", "debug --port-forward").
4144
WithHouseKeepingMessages().
4245
NoArgs(func(ctx context.Context, out io.Writer) error {

docs/content/en/docs/references/cli/_index.md

+2
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ Options:
435435
-p, --profile=[]: Activate profiles by name (prefixed with `-` to disable a profile)
436436
--profile-auto-activation=true: Set to false to disable profile auto activation
437437
--propagate-profiles=true: Setting '--propagate-profiles=false' disables propagating profiles set by the '--profile' flag across config dependencies. This mean that only profiles defined directly in the target 'skaffold.yaml' file are activated.
438+
--protocols=[]: Priority sorted order of debugger protocols to support.
438439
--remote-cache-dir='': Specify the location of the git repositories cache (default $HOME/.skaffold/repos)
439440
--rpc-http-port=50052: tcp port to expose event REST API over HTTP
440441
--rpc-port=50051: tcp port to expose event API
@@ -488,6 +489,7 @@ Env vars:
488489
* `SKAFFOLD_PROFILE` (same as `--profile`)
489490
* `SKAFFOLD_PROFILE_AUTO_ACTIVATION` (same as `--profile-auto-activation`)
490491
* `SKAFFOLD_PROPAGATE_PROFILES` (same as `--propagate-profiles`)
492+
* `SKAFFOLD_PROTOCOLS` (same as `--protocols`)
491493
* `SKAFFOLD_REMOTE_CACHE_DIR` (same as `--remote-cache-dir`)
492494
* `SKAFFOLD_RPC_HTTP_PORT` (same as `--rpc-http-port`)
493495
* `SKAFFOLD_RPC_PORT` (same as `--rpc-port`)

pkg/skaffold/debug/debug_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (t testTransformer) IsApplicable(config imageConfiguration) bool {
112112
return true
113113
}
114114

115-
func (t testTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
115+
func (t testTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
116116
port := portAlloc(9999)
117117
container.Ports = append(container.Ports, v1.ContainerPort{Name: "test", ContainerPort: port})
118118

pkg/skaffold/debug/transform.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ type containerTransformer interface {
109109
// and required initContainer (an empty string if not required), or return a non-nil error if
110110
// the container could not be transformed. The initContainer image is intended to install any
111111
// required debug support tools.
112-
Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error)
112+
Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error)
113113
}
114114

115115
const (
@@ -132,6 +132,8 @@ var containerTransforms []containerTransformer
132132
// as a command-line. These entrypoints are ignored.
133133
var entrypointLaunchers []string
134134

135+
var Protocols = []string{}
136+
135137
// isEntrypointLauncher checks if the given entrypoint is a known entrypoint launcher,
136138
// meaning an entrypoint that treats the image's CMD as a command-line.
137139
func isEntrypointLauncher(entrypoint []string) bool {
@@ -454,7 +456,7 @@ func performContainerTransform(container *v1.Container, config imageConfiguratio
454456
logrus.Tracef("Examining container %q with config %v", container.Name, config)
455457
for _, transform := range containerTransforms {
456458
if transform.IsApplicable(config) {
457-
return transform.Apply(container, config, portAlloc)
459+
return transform.Apply(container, config, portAlloc, Protocols)
458460
}
459461
}
460462
return ContainerDebugConfiguration{}, "", fmt.Errorf("unable to determine runtime for %q", container.Name)

pkg/skaffold/debug/transform_go.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (t dlvTransformer) IsApplicable(config imageConfiguration) bool {
9292

9393
// Apply configures a container definition for Go with Delve.
9494
// Returns the debug configuration details, with the "go" support image
95-
func (t dlvTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
95+
func (t dlvTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
9696
logrus.Infof("Configuring %q for Go/Delve debugging", container.Name)
9797

9898
// try to find existing `dlv` command

pkg/skaffold/debug/transform_go_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func TestDlvTransformerApply(t *testing.T) {
208208
}
209209
for _, test := range tests {
210210
testutil.Run(t, test.description, func(t *testutil.T) {
211-
config, image, err := dlvTransformer{}.Apply(&test.containerSpec, test.configuration, identity)
211+
config, image, err := dlvTransformer{}.Apply(&test.containerSpec, test.configuration, identity, nil)
212212

213213
t.CheckError(test.shouldErr, err)
214214
t.CheckDeepEqual(test.result, test.containerSpec)

pkg/skaffold/debug/transform_jvm.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type jdwpSpec struct {
6363

6464
// Apply configures a container definition for JVM debugging.
6565
// Returns a simple map describing the debug configuration details.
66-
func (t jdwpTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
66+
func (t jdwpTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
6767
logrus.Infof("Configuring %q for JVM debugging", container.Name)
6868
// try to find existing JAVA_TOOL_OPTIONS or jdwp command argument
6969
spec := retrieveJdwpSpec(config)

pkg/skaffold/debug/transform_jvm_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func TestJdwpTransformerApply(t *testing.T) {
156156
}
157157
for _, test := range tests {
158158
testutil.Run(t, test.description, func(t *testutil.T) {
159-
config, image, err := jdwpTransformer{}.Apply(&test.containerSpec, test.configuration, identity)
159+
config, image, err := jdwpTransformer{}.Apply(&test.containerSpec, test.configuration, identity, nil)
160160

161161
// Apply never fails since there's always the option to set JAVA_TOOL_OPTIONS
162162
t.CheckNil(err)

pkg/skaffold/debug/transform_netcore.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (t netcoreTransformer) IsApplicable(config imageConfiguration) bool {
6767

6868
// Apply configures a container definition for vsdbg.
6969
// Returns a simple map describing the debug configuration details.
70-
func (t netcoreTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
70+
func (t netcoreTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
7171
logrus.Infof("Configuring %q for netcore debugging", container.Name)
7272

7373
return ContainerDebugConfiguration{

pkg/skaffold/debug/transform_netcore_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestNetcoreTransformerApply(t *testing.T) {
120120
}
121121
for _, test := range tests {
122122
testutil.Run(t, test.description, func(t *testutil.T) {
123-
config, image, err := netcoreTransformer{}.Apply(&test.containerSpec, test.configuration, identity)
123+
config, image, err := netcoreTransformer{}.Apply(&test.containerSpec, test.configuration, identity, nil)
124124

125125
t.CheckError(test.shouldErr, err)
126126
t.CheckDeepEqual(test.result, test.containerSpec)

pkg/skaffold/debug/transform_nodejs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (t nodeTransformer) IsApplicable(config imageConfiguration) bool {
7575

7676
// Apply configures a container definition for NodeJS Chrome V8 Inspector.
7777
// Returns a simple map describing the debug configuration details.
78-
func (t nodeTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
78+
func (t nodeTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
7979
logrus.Infof("Configuring %q for node.js debugging", container.Name)
8080

8181
// try to find existing `--inspect` command

pkg/skaffold/debug/transform_nodejs_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func TestNodeTransformer_Apply(t *testing.T) {
316316
}
317317
for _, test := range tests {
318318
testutil.Run(t, test.description, func(t *testutil.T) {
319-
config, image, err := nodeTransformer{}.Apply(&test.containerSpec, test.configuration, identity)
319+
config, image, err := nodeTransformer{}.Apply(&test.containerSpec, test.configuration, identity, nil)
320320

321321
// Apply never fails since there's always the option to set NODE_OPTIONS
322322
t.CheckNil(err)

pkg/skaffold/debug/transform_python.go

+96-47
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,20 @@ const (
3737
// most examples use 5678
3838
defaultPtvsdPort = 5678
3939
defaultDebugpyPort = 5678
40+
defaultPydevdPort = 5678
4041
)
4142

4243
type pythonDebugType int
4344

4445
const (
4546
ptvsd pythonDebugType = iota
4647
debugpy
48+
pydevd
49+
)
50+
51+
const (
52+
pydevdProtocol = "pydevd"
53+
dapProtocol = "dap"
4754
)
4855

4956
// pythonSpec captures the useful python-ptvsd devtools options
@@ -56,58 +63,84 @@ type pythonSpec struct {
5663

5764
// isLaunchingPython determines if the arguments seems to be invoking python
5865
func isLaunchingPython(args []string) bool {
59-
return len(args) > 0 &&
60-
(args[0] == "python" || strings.HasSuffix(args[0], "/python") ||
61-
args[0] == "python2" || strings.HasSuffix(args[0], "/python2") ||
62-
args[0] == "python3" || strings.HasSuffix(args[0], "/python3"))
66+
return len(args) > 0 && (args[0] == "python" || strings.HasSuffix(args[0], "/python") ||
67+
args[0] == "python2" || strings.HasSuffix(args[0], "/python2") ||
68+
args[0] == "python3" || strings.HasSuffix(args[0], "/python3"))
69+
}
70+
71+
func hasCommonPythonEnvVars(env map[string]string) bool {
72+
for _, key := range []string{
73+
"PYTHON_VERSION",
74+
"PYTHONVERBOSE",
75+
"PYTHONINSPECT",
76+
"PYTHONOPTIMIZE",
77+
"PYTHONUSERSITE",
78+
"PYTHONUNBUFFERED",
79+
"PYTHONPATH",
80+
"PYTHONUSERBASE",
81+
"PYTHONWARNINGS",
82+
"PYTHONHOME",
83+
"PYTHONCASEOK",
84+
"PYTHONIOENCODING",
85+
"PYTHONHASHSEED",
86+
"PYTHONDONTWRITEBYTECODE",
87+
} {
88+
if _, found := env[key]; found {
89+
return true
90+
}
91+
}
92+
93+
return false
6394
}
6495

6596
func (t pythonTransformer) IsApplicable(config imageConfiguration) bool {
66-
// We can only put Python in debug mode by modifying the python command line,
67-
// so looking for Python-related environment variables is insufficient.
97+
if hasCommonPythonEnvVars(config.env) {
98+
return true
99+
}
100+
68101
if len(config.entrypoint) > 0 && !isEntrypointLauncher(config.entrypoint) {
69102
return isLaunchingPython(config.entrypoint)
70103
}
71104
return isLaunchingPython(config.arguments)
72105
}
73106

74-
// Apply configures a container definition for Python with ptvsd/debugpy.
107+
// Apply configures a container definition for Python with ptvsd/debugpy/pydevd.
75108
// Returns a simple map describing the debug configuration details.
76-
func (t pythonTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator) (ContainerDebugConfiguration, string, error) {
109+
func (t pythonTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (ContainerDebugConfiguration, string, error) {
77110
logrus.Infof("Configuring %q for python debugging", container.Name)
78111

79112
// try to find existing `-mptvsd` or `-mdebugpy` command
80113
if spec := retrievePythonDebugSpec(config); spec != nil {
81-
container.Ports = exposePort(container.Ports, "dap", spec.port)
114+
protocol := spec.protocol()
115+
container.Ports = exposePort(container.Ports, protocol, spec.port)
82116
return ContainerDebugConfiguration{
83117
Runtime: "python",
84-
Ports: map[string]uint32{"dap": uint32(spec.port)},
118+
Ports: map[string]uint32{protocol: uint32(spec.port)},
85119
}, "", nil
86120
}
87121

88-
spec := &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort)}
122+
spec := createPythonDebugSpec(overrideProtocols, portAlloc)
123+
89124
switch {
90125
case isLaunchingPython(config.entrypoint):
91126
container.Command = rewritePythonCommandLine(config.entrypoint, *spec)
92127

93128
case (len(config.entrypoint) == 0 || isEntrypointLauncher(config.entrypoint)) && isLaunchingPython(config.arguments):
94129
container.Args = rewritePythonCommandLine(config.arguments, *spec)
95130

131+
case hasCommonPythonEnvVars(config.env):
132+
container.Command = rewritePythonCommandLine(config.entrypoint, *spec)
133+
96134
default:
97135
return ContainerDebugConfiguration{}, "", fmt.Errorf("%q does not appear to invoke python", container.Name)
98136
}
99137

100-
pyUserBase := "/dbg/python"
101-
if existing, found := config.env["PYTHONUSERBASE"]; found {
102-
// todo: handle windows containers?
103-
pyUserBase = pyUserBase + ":" + existing
104-
}
105-
container.Env = setEnvVar(container.Env, "PYTHONUSERBASE", pyUserBase)
106-
container.Ports = exposePort(container.Ports, "dap", spec.port)
138+
protocol := spec.protocol()
139+
container.Ports = exposePort(container.Ports, protocol, spec.port)
107140

108141
return ContainerDebugConfiguration{
109142
Runtime: "python",
110-
Ports: map[string]uint32{"dap": uint32(spec.port)},
143+
Ports: map[string]uint32{protocol: uint32(spec.port)},
111144
}, "python", nil
112145
}
113146

@@ -131,6 +164,19 @@ func extractPythonDebugSpec(args []string) *pythonSpec {
131164
return nil
132165
}
133166

167+
func createPythonDebugSpec(overrideProtocols []string, portAlloc portAllocator) *pythonSpec {
168+
for _, p := range overrideProtocols {
169+
switch p {
170+
case pydevdProtocol:
171+
return &pythonSpec{debugger: pydevd, port: portAlloc(defaultPydevdPort)}
172+
case dapProtocol:
173+
return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort)}
174+
}
175+
}
176+
177+
return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort)}
178+
}
179+
134180
func extractPtvsdSpec(args []string) *pythonSpec {
135181
if !hasPyModule("ptvsd", args) {
136182
return nil
@@ -212,42 +258,45 @@ func hasPyModule(module string, args []string) bool {
212258
return false
213259
}
214260

215-
// rewritePythonCommandLine rewrites a python command-line to insert a `-mptvsd` etc
261+
// rewritePythonCommandLine rewrites a python command-line to use the debug-support's launcher.
216262
func rewritePythonCommandLine(commandLine []string, spec pythonSpec) []string {
217263
// Assumes that commandLine[0] is "python" or "python3" etc
218-
return util.StrSliceInsert(commandLine, 1, spec.asArguments())
264+
return util.StrSliceInsert(commandLine, 0, spec.asArguments())
219265
}
220266

221267
func (spec pythonSpec) asArguments() []string {
268+
args := []string{"/dbg/python/launcher", "--mode", spec.launcherMode()}
269+
if spec.port >= 0 {
270+
args = append(args, "--port", strconv.FormatInt(int64(spec.port), 10))
271+
}
272+
if spec.wait {
273+
args = append(args, "--wait")
274+
}
275+
args = append(args, "--")
276+
return args
277+
}
278+
279+
func (spec pythonSpec) launcherMode() string {
222280
switch spec.debugger {
281+
case pydevd:
282+
return "pydevd"
223283
case ptvsd:
224-
args := []string{"-mptvsd"}
225-
// --host is a mandatory argument
226-
if spec.host == "" {
227-
args = append(args, "--host", "0.0.0.0")
228-
} else {
229-
args = append(args, "--host", spec.host)
230-
}
231-
if spec.port >= 0 {
232-
args = append(args, "--port", strconv.FormatInt(int64(spec.port), 10))
233-
}
234-
if spec.wait {
235-
args = append(args, "--wait")
236-
}
237-
return args
238-
284+
return "ptvsd"
239285
case debugpy:
240-
args := []string{"-mdebugpy"}
286+
return "debugpy"
287+
}
288+
logrus.Fatalf("invalid debugger type: %q", spec.debugger)
289+
return ""
290+
}
241291

242-
if spec.host == "" {
243-
args = append(args, "--listen", strconv.FormatInt(int64(spec.port), 10))
244-
} else {
245-
args = append(args, "--listen", fmt.Sprintf("%s:%d", spec.host, spec.port))
246-
}
247-
if spec.wait {
248-
args = append(args, "--wait-for-client")
249-
}
250-
return args
292+
func (spec pythonSpec) protocol() string {
293+
switch spec.debugger {
294+
case pydevd:
295+
return pydevdProtocol
296+
case debugpy, ptvsd:
297+
return dapProtocol
298+
default:
299+
logrus.Fatalf("invalid debugger type: %q", spec.debugger)
300+
return dapProtocol
251301
}
252-
return nil
253302
}

0 commit comments

Comments
 (0)