Skip to content

Commit

Permalink
feat: introduce DebuggerMetaData to control if process should wait fo…
Browse files Browse the repository at this point in the history
…r debugger to be attached or not
  • Loading branch information
pkoutsovasilis committed Apr 24, 2024
1 parent a0687ef commit 82e162d
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 39 deletions.
16 changes: 8 additions & 8 deletions pkg/skaffold/debug/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ type containerTransformer interface {
// and required initContainer (an empty string if not required), or return a non-nil error if
// the container could not be transformed. The initContainer image is intended to install any
// required debug support tools.
Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error)
Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error)
}

// TransformContainer rewrites the container definition to enable debugging.
// Returns a debugging configuration description with associated language runtime support
// container image, or an error if the rewrite was unsuccessful.
func TransformContainer(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator) (types.ContainerDebugConfiguration, string, error) {
configuration, requiredImage, err := transformContainer(adapter, config, portAlloc)
func TransformContainer(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
configuration, requiredImage, err := transformContainer(adapter, config, portAlloc, dmd)
if err == nil {
configuration.Artifact = config.Artifact
if configuration.WorkingDir == "" {
Expand All @@ -70,7 +70,7 @@ func TransformContainer(adapter types.ContainerAdapter, config ImageConfiguratio
return configuration, requiredImage, err
}

func transformContainer(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator) (types.ContainerDebugConfiguration, string, error) {
func transformContainer(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
// Update the image configuration's environment with those set in the k8s manifest.
// (Environment variables in the k8s container's `env` add to the image configuration's `env` settings rather than replace.)
container := adapter.GetContainer()
Expand All @@ -91,7 +91,7 @@ func transformContainer(adapter types.ContainerAdapter, config ImageConfiguratio

// Apply command-line unwrapping for buildpack images and images using `sh -c`-style command-lines
next := func(adapter types.ContainerAdapter, config ImageConfiguration) (types.ContainerDebugConfiguration, string, error) {
return performContainerTransform(adapter, config, portAlloc)
return performContainerTransform(adapter, config, portAlloc, dmd)
}
if isCNBImage(config) {
return updateForCNBImage(adapter, config, next)
Expand Down Expand Up @@ -151,20 +151,20 @@ func isShDashC(cmd, arg string) bool {
return (cmd == "/bin/sh" || cmd == "/bin/bash") && arg == "-c"
}

func performContainerTransform(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator) (types.ContainerDebugConfiguration, string, error) {
func performContainerTransform(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
log.Entry(context.TODO()).Tracef("Examining container %q with config %v", adapter.GetContainer().Name, config)
// This approach prioritizes the user-defined runtime specified in the configuration. If the user explicitly defines the runtime, we assume they have a
// specific intention and want to use that specific transform. If no explicit runtime is specified, the code tries to infer the appropriate transform
// based on other indicators in the configuration.
for transform := range containerTransforms {
if transform.MatchRuntime(config) {
return transform.Apply(adapter, config, portAlloc, Protocols)
return transform.Apply(adapter, config, portAlloc, Protocols, dmd)
}
}

for transform := range containerTransforms {
if transform.IsApplicable(config) {
return transform.Apply(adapter, config, portAlloc, Protocols)
return transform.Apply(adapter, config, portAlloc, Protocols, dmd)
}
}
return types.ContainerDebugConfiguration{}, "", fmt.Errorf("unable to determine runtime for %q", adapter.GetContainer().Name)
Expand Down
22 changes: 22 additions & 0 deletions pkg/skaffold/debug/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ type PortAllocator func(int32) int32
// configurationRetriever retrieves an container image configuration
type ConfigurationRetriever func(string) (ImageConfiguration, error)

// DebuggerMetaData captures info for debug tranformers to construct the cli command appropriately
type DebuggerMetaData struct {
// ShouldWait hints that the process should start and wait for the debugger to be attached.
// For kubernetes this derives from an annotation at Pod-Level skaffold.dev/debug-suspend and
// for docker from an image label skaffold.dev/debug-suspend.
ShouldWait bool
}

// ImageConfiguration captures information from a docker/oci image configuration.
// It also includes a "artifact", usually containing the corresponding artifact's' image name from `skaffold.yaml`.
type ImageConfiguration struct {
Expand All @@ -92,6 +100,20 @@ var entrypointLaunchers []string

var Protocols = []string{}

// ExtractDebuggerMetaData extracts and returns the appropriate DebuggerMetaData based on the passed map[string]string
// which contains the context-appropriate metadata, e.g. for Kubernetes deployment the annotations and for Docker the image
// Labels.
func ExtractDebuggerMetaData(md map[string]string) *DebuggerMetaData {
dmd := &DebuggerMetaData{}

if md == nil {
return dmd
}

_, dmd.ShouldWait = md["skaffold.dev/debug-suspend"]
return dmd
}

// isEntrypointLauncher checks if the given entrypoint is a known entrypoint launcher,
// meaning an entrypoint that treats the image's CMD as a command-line.
func isEntrypointLauncher(entrypoint []string) bool {
Expand Down
23 changes: 19 additions & 4 deletions pkg/skaffold/debug/transform_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ type dlvSpec struct {
port uint16
headless bool
log bool
wait bool
apiVersion int
}

func newDlvSpec(port uint16) dlvSpec {
return dlvSpec{mode: "exec", port: port, apiVersion: defaultAPIVersion, headless: true}
return dlvSpec{mode: "exec", port: port, apiVersion: defaultAPIVersion, headless: true, wait: true}
}

// isLaunchingDlv determines if the arguments seems to be invoking Delve
Expand Down Expand Up @@ -110,7 +111,7 @@ func (t dlvTransformer) IsApplicable(config ImageConfiguration) bool {

// Apply configures a container definition for Go with Delve.
// Returns the debug configuration details, with the "go" support image
func (t dlvTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t dlvTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
container := adapter.GetContainer()
log.Entry(context.TODO()).Infof("Configuring %q for Go/Delve debugging", container.Name)

Expand All @@ -120,6 +121,13 @@ func (t dlvTransformer) Apply(adapter types.ContainerAdapter, config ImageConfig
if spec == nil {
newSpec := newDlvSpec(uint16(portAlloc(defaultDlvPort)))
spec = &newSpec

if dmd != nil {
spec.wait = dmd.ShouldWait
} else {
spec.wait = false
}

switch {
case len(config.Entrypoint) > 0 && !isEntrypointLauncher(config.Entrypoint):
container.Command = rewriteDlvCommandLine(config.Entrypoint, *spec, container.Args)
Expand Down Expand Up @@ -155,7 +163,7 @@ func extractDlvSpec(args []string) *dlvSpec {
return nil
}
// delve's defaults
spec := dlvSpec{apiVersion: 2, log: false, headless: false}
spec := dlvSpec{apiVersion: 2, log: false, headless: false, wait: true}
arguments:
for _, arg := range args {
switch {
Expand All @@ -167,6 +175,8 @@ arguments:
spec.headless = true
case arg == "--log":
spec.log = true
case arg == "--continue":
spec.wait = false
case strings.HasPrefix(arg, "--listen="):
address := strings.SplitN(arg, "=", 2)[1]
split := strings.SplitN(address, ":", 2)
Expand Down Expand Up @@ -207,7 +217,12 @@ func (spec dlvSpec) asArguments() []string {
if spec.headless {
args = append(args, "--headless")
}
args = append(args, "--continue", "--accept-multiclient")

if !spec.wait {
args = append(args, "--continue")
}

args = append(args, "--accept-multiclient")
if spec.port > 0 {
args = append(args, fmt.Sprintf("--listen=%s:%d", spec.host, spec.port))
} else {
Expand Down
19 changes: 10 additions & 9 deletions pkg/skaffold/debug/transform_go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

func TestNewDlvSpecDefaults(t *testing.T) {
spec := newDlvSpec(20)
expected := dlvSpec{mode: "exec", port: 20, apiVersion: 2, headless: true, log: false}
expected := dlvSpec{mode: "exec", port: 20, apiVersion: 2, headless: true, log: false, wait: true}
testutil.CheckDeepEqual(t, expected, spec, cmp.AllowUnexported(spec))
}

Expand All @@ -40,14 +40,15 @@ func TestExtractDlvArg(t *testing.T) {
{nil, nil},
{[]string{"foo"}, nil},
{[]string{"foo", "--foo"}, nil},
{[]string{"dlv", "debug", "--headless"}, &dlvSpec{mode: "debug", headless: true, apiVersion: 2, log: false}},
{[]string{"dlv", "--headless", "exec"}, &dlvSpec{mode: "exec", headless: true, apiVersion: 2, log: false}},
{[]string{"dlv", "--headless", "exec", "--", "--listen=host:4345"}, &dlvSpec{mode: "exec", headless: true, apiVersion: 2, log: false}},
{[]string{"dlv", "test", "--headless", "--listen=host:4345"}, &dlvSpec{mode: "test", host: "host", port: 4345, headless: true, apiVersion: 2, log: false}},
{[]string{"dlv", "debug", "--headless", "--api-version=1"}, &dlvSpec{mode: "debug", headless: true, apiVersion: 1, log: false}},
{[]string{"dlv", "debug", "--listen=host:4345", "--headless", "--api-version=2", "--log"}, &dlvSpec{mode: "debug", host: "host", port: 4345, headless: true, apiVersion: 2, log: true}},
{[]string{"dlv", "debug", "--listen=:4345"}, &dlvSpec{mode: "debug", port: 4345, apiVersion: 2}},
{[]string{"dlv", "debug", "--listen=host:"}, &dlvSpec{mode: "debug", host: "host", apiVersion: 2}},
{[]string{"dlv", "debug", "--headless"}, &dlvSpec{mode: "debug", headless: true, apiVersion: 2, log: false, wait: true}},
{[]string{"dlv", "--headless", "exec"}, &dlvSpec{mode: "exec", headless: true, apiVersion: 2, log: false, wait: true}},
{[]string{"dlv", "--headless", "exec", "--", "--listen=host:4345"}, &dlvSpec{mode: "exec", headless: true, apiVersion: 2, log: false, wait: true}},
{[]string{"dlv", "test", "--headless", "--listen=host:4345"}, &dlvSpec{mode: "test", host: "host", port: 4345, headless: true, apiVersion: 2, log: false, wait: true}},
{[]string{"dlv", "debug", "--headless", "--api-version=1"}, &dlvSpec{mode: "debug", headless: true, apiVersion: 1, log: false, wait: true}},
{[]string{"dlv", "debug", "--listen=host:4345", "--headless", "--api-version=2", "--log"}, &dlvSpec{mode: "debug", host: "host", port: 4345, headless: true, apiVersion: 2, log: true, wait: true}},
{[]string{"dlv", "debug", "--listen=:4345"}, &dlvSpec{mode: "debug", port: 4345, apiVersion: 2, wait: true}},
{[]string{"dlv", "debug", "--listen=host:"}, &dlvSpec{mode: "debug", host: "host", apiVersion: 2, wait: true}},
{[]string{"dlv", "debug", "--listen=host:", "--continue"}, &dlvSpec{mode: "debug", host: "host", apiVersion: 2, wait: false}},
}
for _, test := range tests {
testutil.Run(t, strings.Join(test.in, " "), func(t *testutil.T) {
Expand Down
12 changes: 10 additions & 2 deletions pkg/skaffold/debug/transform_jvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type jdwpSpec struct {

// Apply configures a container definition for JVM debugging.
// Returns a simple map describing the debug configuration details.
func (t jdwpTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t jdwpTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
container := adapter.GetContainer()
log.Entry(context.TODO()).Infof("Configuring %q for JVM debugging", container.Name)
// try to find existing JAVA_TOOL_OPTIONS or jdwp command argument
Expand All @@ -88,7 +88,15 @@ func (t jdwpTransformer) Apply(adapter types.ContainerAdapter, config ImageConfi
port = int32(spec.port)
} else {
port = portAlloc(defaultJdwpPort)
jto := fmt.Sprintf("-agentlib:jdwp=transport=dt_socket,server=y,address=%d,suspend=n,quiet=y", port)

var suspendParam string
if dmd != nil && dmd.ShouldWait {
suspendParam = "y"
} else {
suspendParam = "n"
}

jto := fmt.Sprintf("-agentlib:jdwp=transport=dt_socket,server=y,address=%d,suspend=%s,quiet=y", port, suspendParam)
if existing, found := config.Env["JAVA_TOOL_OPTIONS"]; found {
jto = existing + " " + jto
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/skaffold/debug/transform_netcore.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (t netcoreTransformer) IsApplicable(config ImageConfiguration) bool {

// Apply configures a container definition for vsdbg.
// Returns a simple map describing the debug configuration details.
func (t netcoreTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t netcoreTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
container := adapter.GetContainer()
log.Entry(context.TODO()).Infof("Configuring %q for netcore debugging", container.Name)

Expand Down
8 changes: 7 additions & 1 deletion pkg/skaffold/debug/transform_nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,20 @@ func (t nodeTransformer) IsApplicable(config ImageConfiguration) bool {

// Apply configures a container definition for NodeJS Chrome V8 Inspector.
// Returns a simple map describing the debug configuration details.
func (t nodeTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t nodeTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
container := adapter.GetContainer()
log.Entry(context.TODO()).Infof("Configuring %q for node.js debugging", container.Name)

// try to find existing `--inspect` command
spec := retrieveNodeInspectSpec(config)
if spec == nil {
spec = &inspectSpec{host: "0.0.0.0", port: portAlloc(defaultDevtoolsPort)}
if dmd != nil {
spec.brk = dmd.ShouldWait
} else {
spec.brk = false
}

switch {
case isLaunchingNode(config.Entrypoint):
container.Command = rewriteNodeCommandLine(config.Entrypoint, *spec)
Expand Down
17 changes: 11 additions & 6 deletions pkg/skaffold/debug/transform_python.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (t pythonTransformer) IsApplicable(config ImageConfiguration) bool {

// Apply configures a container definition for Python with ptvsd/debugpy/pydevd.
// Returns a simple map describing the debug configuration details.
func (t pythonTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t pythonTransformer) Apply(adapter types.ContainerAdapter, config ImageConfiguration, portAlloc PortAllocator, overrideProtocols []string, dmd *DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
container := adapter.GetContainer()
log.Entry(context.TODO()).Infof("Configuring %q for python debugging", container.Name)

Expand All @@ -132,7 +132,12 @@ func (t pythonTransformer) Apply(adapter types.ContainerAdapter, config ImageCon
}, "", nil
}

spec := createPythonDebugSpec(overrideProtocols, portAlloc)
shouldWait := false
if dmd != nil {
shouldWait = dmd.ShouldWait
}

spec := createPythonDebugSpec(overrideProtocols, portAlloc, shouldWait)

switch {
case isLaunchingPython(config.Entrypoint):
Expand Down Expand Up @@ -177,17 +182,17 @@ func extractPythonDebugSpec(args []string) *pythonSpec {
return nil
}

func createPythonDebugSpec(overrideProtocols []string, portAlloc PortAllocator) *pythonSpec {
func createPythonDebugSpec(overrideProtocols []string, portAlloc PortAllocator, shouldWait bool) *pythonSpec {
for _, p := range overrideProtocols {
switch p {
case pydevdProtocol:
return &pythonSpec{debugger: pydevd, port: portAlloc(defaultPydevdPort)}
return &pythonSpec{debugger: pydevd, port: portAlloc(defaultPydevdPort), wait: shouldWait}
case dapProtocol:
return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort)}
return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort), wait: shouldWait}
}
}

return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort)}
return &pythonSpec{debugger: debugpy, port: portAlloc(defaultDebugpyPort), wait: shouldWait}
}

func extractPtvsdSpec(args []string) *pythonSpec {
Expand Down
4 changes: 3 additions & 1 deletion pkg/skaffold/docker/debugger/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func transformImage(ctx context.Context, artifact graph.Artifact, cfg *container
// the set of image IDs required to provide debugging support files
requiredSupportImages := make(map[string]bool)

if configuration, requiredImage, err := debug.TransformContainer(adapter, imageConfig, portAlloc); err == nil {
dmd := debug.ExtractDebuggerMetaData(imageConfig.Labels)

if configuration, requiredImage, err := debug.TransformContainer(adapter, imageConfig, portAlloc, dmd); err == nil {
configurations[adapter.GetContainer().Name] = configuration
if len(requiredImage) > 0 {
log.Entry(ctx).Infof("%q requires debugging support image %q", cfg.Image, requiredImage)
Expand Down
2 changes: 1 addition & 1 deletion pkg/skaffold/kubernetes/debugging/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (t testTransformer) MatchRuntime(config debug.ImageConfiguration) bool {
return true
}

func (t testTransformer) Apply(adapter types.ContainerAdapter, config debug.ImageConfiguration, portAlloc debug.PortAllocator, overrideProtocols []string) (types.ContainerDebugConfiguration, string, error) {
func (t testTransformer) Apply(adapter types.ContainerAdapter, config debug.ImageConfiguration, portAlloc debug.PortAllocator, overrideProtocols []string, dmd *debug.DebuggerMetaData) (types.ContainerDebugConfiguration, string, error) {
port := portAlloc(9999)
container := adapter.GetContainer()
container.Ports = append(container.Ports, types.ContainerPort{Name: "test", ContainerPort: port})
Expand Down
5 changes: 4 additions & 1 deletion pkg/skaffold/kubernetes/debugging/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,10 @@ func rewriteContainers(metadata *metav1.ObjectMeta, podSpec *v1.PodSpec, retriev
a := adapter.NewAdapter(&container)
// requiredImage, if not empty, is the image ID providing the debugging support files
// `err != nil` means that the container did not or could not be transformed
if configuration, requiredImage, err := debug.TransformContainer(a, imageConfig, portAlloc); err == nil {

dmd := debug.ExtractDebuggerMetaData(metadata.Annotations)

if configuration, requiredImage, err := debug.TransformContainer(a, imageConfig, portAlloc, dmd); err == nil {
configurations[container.Name] = configuration
podSpec.Containers[i] = container // apply any configuration changes
if len(requiredImage) > 0 {
Expand Down
Loading

0 comments on commit 82e162d

Please sign in to comment.