From 38ac39f7afbf08491e37b9d0a3dc63d4a9c0f703 Mon Sep 17 00:00:00 2001 From: Njal Karevoll Date: Thu, 11 Sep 2025 10:32:54 +0200 Subject: [PATCH] feat(debug): add --debug-continue flag to allow application startup without debugger attachment --- docs/features/debugging.md | 6 ++ docs/reference/ko_apply.md | 1 + docs/reference/ko_build.md | 1 + docs/reference/ko_create.md | 1 + docs/reference/ko_resolve.md | 1 + docs/reference/ko_run.md | 1 + pkg/build/gobuild.go | 4 ++ pkg/build/gobuild_test.go | 118 ++++++++++++++++++++++------------ pkg/build/options.go | 7 ++ pkg/commands/options/build.go | 3 + pkg/commands/resolver.go | 4 ++ 11 files changed, 106 insertions(+), 41 deletions(-) diff --git a/docs/features/debugging.md b/docs/features/debugging.md index 93dd2cdec5..536dd4618f 100644 --- a/docs/features/debugging.md +++ b/docs/features/debugging.md @@ -27,3 +27,9 @@ docker run -p 40000:40000 ``` This sets up your app to be waiting to run the command you've specified. All that's needed now is to connect your debugger client to the running container! + +By default, the application will not start until a debugger has connected and issued the `continue` command. This is required in order to be able to set breakpoints for code that is executed during start-up. If you want the application to start running without waiting for a debugger, use the `--debug-continue` parameter: + +```plaintext +ko build . --debug --debug-continue +``` diff --git a/docs/reference/ko_apply.md b/docs/reference/ko_apply.md index 1dbe2a2423..2a488fab66 100644 --- a/docs/reference/ko_apply.md +++ b/docs/reference/ko_apply.md @@ -48,6 +48,7 @@ ko apply -f FILENAME [flags] --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. + --debug-continue Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for apply diff --git a/docs/reference/ko_build.md b/docs/reference/ko_build.md index c4b62f2539..31bd1c5d03 100644 --- a/docs/reference/ko_build.md +++ b/docs/reference/ko_build.md @@ -45,6 +45,7 @@ ko build IMPORTPATH... [flags] --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. + --debug-continue Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for build --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. diff --git a/docs/reference/ko_create.md b/docs/reference/ko_create.md index 51e47cb7d2..3c39438efc 100644 --- a/docs/reference/ko_create.md +++ b/docs/reference/ko_create.md @@ -48,6 +48,7 @@ ko create -f FILENAME [flags] --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. + --debug-continue Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for create diff --git a/docs/reference/ko_resolve.md b/docs/reference/ko_resolve.md index cd51f4408a..92ce11d71d 100644 --- a/docs/reference/ko_resolve.md +++ b/docs/reference/ko_resolve.md @@ -41,6 +41,7 @@ ko resolve -f FILENAME [flags] --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. + --debug-continue Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -f, --filename strings Filename, directory, or URL to files to use to create the resource -h, --help help for resolve diff --git a/docs/reference/ko_run.md b/docs/reference/ko_run.md index e9ee80ffc0..59219c616d 100644 --- a/docs/reference/ko_run.md +++ b/docs/reference/ko_run.md @@ -33,6 +33,7 @@ ko run IMPORTPATH [flags] --bare Whether to just use KO_DOCKER_REPO without additional context (may not work properly with --tags). -B, --base-import-paths Whether to use the base path without MD5 hash after KO_DOCKER_REPO (may not work properly with --tags). --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. + --debug-continue Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for run --image-annotation strings Which annotations (key=value[,key=value]) to add to the OCI manifest. diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index c26cdedc81..f1e2af124a 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -105,6 +105,7 @@ type gobuild struct { annotations map[string]string user string debug bool + debugContinue bool semaphore *semaphore.Weighted cache *layerCache @@ -134,6 +135,7 @@ type gobuildOpener struct { dir string jobs int debug bool + debugContinue bool } func (gbo *gobuildOpener) Open() (Interface, error) { @@ -169,6 +171,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { annotations: gbo.annotations, dir: gbo.dir, debug: gbo.debug, + debugContinue: gbo.debugContinue, platformMatcher: matcher, cache: &layerCache{ buildToDiff: map[string]buildIDToDiffID{}, @@ -1129,6 +1132,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl "--log", "--accept-multiclient", "--api-version=2", + fmt.Sprintf("--continue=%t", g.debugContinue), "--", } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index cc5f8d5ab0..29de64fabb 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -1498,52 +1498,88 @@ func TestDebugger(t *testing.T) { } importpath := "github.com/google/ko" - ng, err := NewGo( - context.Background(), - "", - WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), - WithPlatforms("linux/amd64"), - WithDebugger(), - ) - if err != nil { - t.Fatalf("NewGo() = %v", err) + tests := []struct { + desc string + opts []Option + wantEntrypoint []string + }{ + { + desc: "debugger enabled", + opts: []Option{ + WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), + WithPlatforms("linux/amd64"), + WithDebugger(), + }, + wantEntrypoint: []string{ + "/ko-app/dlv", + "exec", + "--listen=:40000", + "--headless", + "--log", + "--accept-multiclient", + "--api-version=2", + "--continue=false", + "--", + "/ko-app/ko", + }, + }, + { + desc: "debugger enabled with continue", + opts: []Option{ + WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), + WithPlatforms("linux/amd64"), + WithDebugger(), + WithDebuggerContinue(), + }, + wantEntrypoint: []string{ + "/ko-app/dlv", + "exec", + "--listen=:40000", + "--headless", + "--log", + "--accept-multiclient", + "--api-version=2", + "--continue=true", + "--", + "/ko-app/ko", + }, + }, } - result, err := ng.Build(context.Background(), StrictScheme+importpath) - if err != nil { - t.Fatalf("Build() = %v", err) - } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + ng, err := NewGo(context.Background(), "", tc.opts...) + if err != nil { + t.Fatalf("NewGo() = %v", err) + } - img, ok := result.(v1.Image) - if !ok { - t.Fatalf("Build() not an Image: %T", result) - } + result, err := ng.Build(context.Background(), StrictScheme+importpath) + if err != nil { + t.Fatalf("Build() = %v", err) + } - // Check that the entrypoint of the image is not overwritten - cfg, err := img.ConfigFile() - if err != nil { - t.Errorf("ConfigFile() = %v", err) - } - gotEntrypoint := cfg.Config.Entrypoint - wantEntrypoint := []string{ - "/ko-app/dlv", - "exec", - "--listen=:40000", - "--headless", - "--log", - "--accept-multiclient", - "--api-version=2", - "--", - "/ko-app/ko", - } + img, ok := result.(v1.Image) + if !ok { + t.Fatalf("Build() not an Image: %T", result) + } - if got, want := len(gotEntrypoint), len(wantEntrypoint); got != want { - t.Fatalf("len(entrypoint) = %v, want %v", got, want) - } + // Check that the entrypoint of the image is not overwritten + cfg, err := img.ConfigFile() + if err != nil { + t.Errorf("ConfigFile() = %v", err) + } + gotEntrypoint := cfg.Config.Entrypoint + wantEntrypoint := tc.wantEntrypoint - for i := range wantEntrypoint { - if got, want := gotEntrypoint[i], wantEntrypoint[i]; got != want { - t.Errorf("entrypoint[%d] = %v, want %v", i, got, want) - } + if got, want := len(gotEntrypoint), len(wantEntrypoint); got != want { + t.Fatalf("len(entrypoint) = %v, want %v", got, want) + } + + for i := range wantEntrypoint { + if got, want := gotEntrypoint[i], wantEntrypoint[i]; got != want { + t.Errorf("entrypoint[%d] = %v, want %v", i, got, want) + } + } + }) } } diff --git a/pkg/build/options.go b/pkg/build/options.go index 75443a9b60..1915ec523a 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -210,3 +210,10 @@ func WithDebugger() Option { return nil } } + +func WithDebuggerContinue() Option { + return func(gbo *gobuildOpener) error { + gbo.debugContinue = true + return nil + } +} diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index dd9c6eb712..b8e0679ca1 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -68,6 +68,7 @@ type BuildOptions struct { Annotations []string User string Debug bool + DebugContinue bool // UserAgent enables overriding the default value of the `User-Agent` HTTP // request header used when retrieving the base image. UserAgent string @@ -103,6 +104,8 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { "The default user the image should be run as.") cmd.Flags().BoolVar(&bo.Debug, "debug", bo.Debug, "Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000.") + cmd.Flags().BoolVar(&bo.DebugContinue, "debug-continue", bo.DebugContinue, + "Continue the debugged process on start. Useful when you don't want to require a debugger to attach for the application to start.") bo.Trimpath = true } diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 829799ca21..6d0b547e42 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -104,6 +104,10 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { if bo.Debug { opts = append(opts, build.WithDebugger()) opts = append(opts, build.WithDisabledOptimizations()) // also needed for Delve + + if bo.DebugContinue { + opts = append(opts, build.WithDebuggerContinue()) + } } switch bo.SBOM { case "none":