From c06d39da7294a323eb2bf763727070116f6fe0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 13 Sep 2023 11:37:27 +0200 Subject: [PATCH 01/11] Add `debug` option to include Delve debugger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christoph Stäbler --- pkg/build/gobuild.go | 114 +++++++++++++++++++++++++++++++++- pkg/build/gobuild_test.go | 56 +++++++++++++++++ pkg/build/options.go | 7 +++ pkg/commands/options/build.go | 3 + pkg/commands/resolver.go | 4 ++ 5 files changed, 183 insertions(+), 1 deletion(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 0bc44cbfc..4b0600069 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -23,6 +23,7 @@ import ( "fmt" gb "go/build" "io" + "io/fs" "log" "os" "os/exec" @@ -101,6 +102,7 @@ type gobuild struct { platformMatcher *platformMatcher dir string labels map[string]string + debug bool semaphore *semaphore.Weighted cache *layerCache @@ -127,6 +129,7 @@ type gobuildOpener struct { labels map[string]string dir string jobs int + debug bool } func (gbo *gobuildOpener) Open() (Interface, error) { @@ -156,6 +159,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { defaultLdflags: gbo.defaultLdflags, labels: gbo.labels, dir: gbo.dir, + debug: gbo.debug, platformMatcher: matcher, cache: &layerCache{ buildToDiff: map[string]buildIDToDiffID{}, @@ -274,6 +278,63 @@ func getGoBinary() string { return defaultGoBin } +func getDelve(ctx context.Context, platform v1.Platform) (string, error) { + env, err := buildEnv(platform, os.Environ(), nil, nil) + if err != nil { + return "", fmt.Errorf("could not create env for Delve build: %w", err) + } + + tmpInstallDir, err := os.MkdirTemp("", "delve") + if err != nil { + return "", fmt.Errorf("could not create tmp dir for Delve installation: %w", err) + } + + // install delve to tmp directory + env = append(env, fmt.Sprintf("GOPATH=%s", tmpInstallDir)) + + args := []string{ + "install", + "github.com/go-delve/delve/cmd/dlv@latest", + } + + gobin := getGoBinary() + cmd := exec.CommandContext(ctx, gobin, args...) + cmd.Env = env + + var output bytes.Buffer + cmd.Stderr = &output + cmd.Stdout = &output + + log.Printf("Building Delve for %s", platform) + if err := cmd.Run(); err != nil { + os.RemoveAll(tmpInstallDir) + return "", fmt.Errorf("go build Delve: %w: %s", err, output.String()) + } + + // find the delve binary in tmpInstallDir/bin/ + delveBinary := "" + err = filepath.WalkDir(filepath.Join(tmpInstallDir, "bin"), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() && strings.Contains(d.Name(), "dlv") { + delveBinary = path + } + + return nil + }) + if err != nil { + return "", fmt.Errorf("could not search for Delve binary: %w", err) + } + + if delveBinary == "" { + return "", fmt.Errorf("could not find Delve binary in %q", tmpInstallDir) + } + + return delveBinary, nil +} + func build(ctx context.Context, buildCtx buildContext) (string, error) { // Create the set of build arguments from the config flags/ldflags with any // template parameters applied. @@ -1030,6 +1091,45 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl }, }) + delvePath := "" // path for delve in image + if g.debug { + // get delve locally + delveBinary, err := getDelve(ctx, *platform) + if err != nil { + return nil, fmt.Errorf("building Delve: %w", err) + } + defer os.RemoveAll(filepath.Dir(delveBinary)) + + delvePath = "/usr/bin/" + filepath.Base(delveBinary) + + // add layer with delve binary + delveLayer, err := g.cache.get(ctx, delveBinary, func() (v1.Layer, error) { + return buildLayer(delvePath, delveBinary, platform, layerMediaType, &lo) + }) + if err != nil { + return nil, fmt.Errorf("cache.get(%q): %w", delveBinary, err) + } + + layers = append(layers, mutate.Addendum{ + Layer: delveLayer, + MediaType: layerMediaType, + History: v1.History{ + Author: "ko", + Created: g.creationTime, + CreatedBy: "ko build " + ref.String(), + Comment: "Delve debugger, at " + delvePath, + }, + }) + } + delveArgs := []string{ + "exec", + "--listen=:40000", + "--headless", + "--log", + "--accept-multiclient", + "--api-version=2", + } + // Augment the base image with our application layer. withApp, err := mutate.Append(base, layers...) if err != nil { @@ -1047,10 +1147,22 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl cfg.Config.Entrypoint = []string{appPath} cfg.Config.Cmd = nil if platform.OS == "windows" { - cfg.Config.Entrypoint = []string{`C:\ko-app\` + appFileName} + appPath := `C:\ko-app\` + appFileName + if g.debug { + cfg.Config.Entrypoint = append([]string{"C:\\" + delvePath}, delveArgs...) + cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) + } else { + cfg.Config.Entrypoint = []string{appPath} + } + updatePath(cfg, `C:\ko-app`) cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`) } else { + if g.debug { + cfg.Config.Entrypoint = append([]string{delvePath}, delveArgs...) + cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) + } + updatePath(cfg, appDir) cfg.Config.Env = append(cfg.Config.Env, "KO_DATA_PATH="+kodataRoot) } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 015971509..d5cf6316f 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -1453,3 +1453,59 @@ func TestGoBuildConsistentMediaTypes(t *testing.T) { }) } } + +func TestDebugger(t *testing.T) { + base, err := random.Image(1024, 3) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + importpath := "github.com/google/ko" + + ng, err := NewGo( + context.Background(), + "", + WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), + WithPlatforms("all"), + WithDebugger(), + ) + if err != nil { + t.Fatalf("NewGo() = %v", err) + } + + result, err := ng.Build(context.Background(), StrictScheme+importpath) + if err != nil { + t.Fatalf("Build() = %v", err) + } + + img, ok := result.(v1.Image) + if !ok { + t.Fatalf("Build() not an Image: %T", result) + } + + // 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{ + "/usr/bin/dlv", + "exec", + "--listen=:40000", + "--headless", + "--log", + "--accept-multiclient", + "--api-version=2", + "/ko-app/ko", + } + + 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 c09aa16d4..70ccab954 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -202,3 +202,10 @@ func WithSBOMDir(dir string) Option { return nil } } + +func WithDebugger() Option { + return func(gbo *gobuildOpener) error { + gbo.debug = true + return nil + } +} diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index c02eda6f2..5524091f6 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -65,6 +65,7 @@ type BuildOptions struct { SBOMDir string Platforms []string Labels []string + Debug bool // UserAgent enables overriding the default value of the `User-Agent` HTTP // request header used when retrieving the base image. UserAgent string @@ -94,6 +95,8 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { "Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]*") cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{}, "Which labels (key=value) to add to the image.") + cmd.Flags().BoolVar(&bo.Debug, "debug", bo.Debug, + "Include Delve debugger into image and wrap arround ko-app. This debugger will listen to port 40000.") bo.Trimpath = true } diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 7d890a9bc..bfd0dba08 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -101,6 +101,10 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { if bo.DisableOptimizations { opts = append(opts, build.WithDisabledOptimizations()) } + if bo.Debug { + opts = append(opts, build.WithDebugger()) + opts = append(opts, build.WithDisabledOptimizations()) // also needed for Delve + } switch bo.SBOM { case "none": opts = append(opts, build.WithDisabledSBOM()) From e082b51973bc6627100be1e6509c45497b85ffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 13 Sep 2023 11:39:02 +0200 Subject: [PATCH 02/11] Run ./hack/update-codegen.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christoph Stäbler --- 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 + 5 files changed, 5 insertions(+) diff --git a/docs/reference/ko_apply.md b/docs/reference/ko_apply.md index b49212098..dd7e51270 100644 --- a/docs/reference/ko_apply.md +++ b/docs/reference/ko_apply.md @@ -47,6 +47,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 arround ko-app. This debugger will listen to port 40000. --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 8c4d97d6e..7dd22905e 100644 --- a/docs/reference/ko_build.md +++ b/docs/reference/ko_build.md @@ -44,6 +44,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 arround ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for build --image-label strings Which labels (key=value) to add to the image. diff --git a/docs/reference/ko_create.md b/docs/reference/ko_create.md index 700f94340..8cd4567ee 100644 --- a/docs/reference/ko_create.md +++ b/docs/reference/ko_create.md @@ -47,6 +47,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 arround ko-app. This debugger will listen to port 40000. --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 9466bbb30..cb932013f 100644 --- a/docs/reference/ko_resolve.md +++ b/docs/reference/ko_resolve.md @@ -40,6 +40,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 arround ko-app. This debugger will listen to port 40000. --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 5e2c88b04..c32d08373 100644 --- a/docs/reference/ko_run.md +++ b/docs/reference/ko_run.md @@ -32,6 +32,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 arround ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for run --image-label strings Which labels (key=value) to add to the image. From 374672228cff4a5e64391c401f79358c88680234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Fri, 29 Sep 2023 10:36:38 +0200 Subject: [PATCH 03/11] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christoph Stäbler --- docs/reference/ko_apply.md | 2 +- docs/reference/ko_build.md | 2 +- docs/reference/ko_create.md | 2 +- docs/reference/ko_resolve.md | 2 +- docs/reference/ko_run.md | 2 +- pkg/commands/options/build.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/reference/ko_apply.md b/docs/reference/ko_apply.md index dd7e51270..4b5c3d32a 100644 --- a/docs/reference/ko_apply.md +++ b/docs/reference/ko_apply.md @@ -47,7 +47,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 arround ko-app. This debugger will listen to port 40000. + --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --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 7dd22905e..a2c6bb200 100644 --- a/docs/reference/ko_build.md +++ b/docs/reference/ko_build.md @@ -44,7 +44,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 arround ko-app. This debugger will listen to port 40000. + --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for build --image-label strings Which labels (key=value) to add to the image. diff --git a/docs/reference/ko_create.md b/docs/reference/ko_create.md index 8cd4567ee..0e812e569 100644 --- a/docs/reference/ko_create.md +++ b/docs/reference/ko_create.md @@ -47,7 +47,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 arround ko-app. This debugger will listen to port 40000. + --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --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 cb932013f..2fb87ab7f 100644 --- a/docs/reference/ko_resolve.md +++ b/docs/reference/ko_resolve.md @@ -40,7 +40,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 arround ko-app. This debugger will listen to port 40000. + --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --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 c32d08373..b9d858ada 100644 --- a/docs/reference/ko_run.md +++ b/docs/reference/ko_run.md @@ -32,7 +32,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 arround ko-app. This debugger will listen to port 40000. + --debug Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000. --disable-optimizations Disable optimizations when building Go code. Useful when you want to interactively debug the created container. -h, --help help for run --image-label strings Which labels (key=value) to add to the image. diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index 5524091f6..236490f99 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -96,7 +96,7 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{}, "Which labels (key=value) to add to the image.") cmd.Flags().BoolVar(&bo.Debug, "debug", bo.Debug, - "Include Delve debugger into image and wrap arround ko-app. This debugger will listen to port 40000.") + "Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000.") bo.Trimpath = true } From 464b8bb4ffcffc42cd111542870e20bac0feaf54 Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Mon, 20 May 2024 21:59:42 -0400 Subject: [PATCH 04/11] review feedback Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 7 +++---- pkg/build/gobuild_test.go | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 4b0600069..03ccb0f95 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -313,7 +313,7 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { // find the delve binary in tmpInstallDir/bin/ delveBinary := "" - err = filepath.WalkDir(filepath.Join(tmpInstallDir, "bin"), func(path string, d fs.DirEntry, err error) error { + if err := filepath.WalkDir(filepath.Join(tmpInstallDir, "bin"), func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -323,8 +323,7 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { } return nil - }) - if err != nil { + }); err != nil { return "", fmt.Errorf("could not search for Delve binary: %w", err) } @@ -1100,7 +1099,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } defer os.RemoveAll(filepath.Dir(delveBinary)) - delvePath = "/usr/bin/" + filepath.Base(delveBinary) + delvePath = path.Join("/ko-app", filepath.Base(delveBinary)) // add layer with delve binary delveLayer, err := g.cache.get(ctx, delveBinary, func() (v1.Layer, error) { diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index d5cf6316f..de5cc403b 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -1489,7 +1489,7 @@ func TestDebugger(t *testing.T) { } gotEntrypoint := cfg.Config.Entrypoint wantEntrypoint := []string{ - "/usr/bin/dlv", + "/ko-app/dlv", "exec", "--listen=:40000", "--headless", From c23455109422465169be375a13b5584b9fd03b29 Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Wed, 22 May 2024 18:27:06 -0400 Subject: [PATCH 05/11] predict dlv install path using os + arch Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 03ccb0f95..008768c91 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -23,7 +23,6 @@ import ( "fmt" gb "go/build" "io" - "io/fs" "log" "os" "os/exec" @@ -279,7 +278,7 @@ func getGoBinary() string { } func getDelve(ctx context.Context, platform v1.Platform) (string, error) { - env, err := buildEnv(platform, os.Environ(), nil, nil) + env, err := buildEnv(platform, os.Environ(), nil) if err != nil { return "", fmt.Errorf("could not create env for Delve build: %w", err) } @@ -312,23 +311,13 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { } // find the delve binary in tmpInstallDir/bin/ - delveBinary := "" - if err := filepath.WalkDir(filepath.Join(tmpInstallDir, "bin"), func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if !d.IsDir() && strings.Contains(d.Name(), "dlv") { - delveBinary = path - } - - return nil - }); err != nil { - return "", fmt.Errorf("could not search for Delve binary: %w", err) + osArchDir := "" + if platform.OS != "" && platform.Architecture != "" { + osArchDir = fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) } - - if delveBinary == "" { - return "", fmt.Errorf("could not find Delve binary in %q", tmpInstallDir) + delveBinary := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") + if _, err := os.Stat(delveBinary); err != nil { + return "", fmt.Errorf("could not find Delve binary at %q: %w", delveBinary, err) } return delveBinary, nil From 4a05ed5c81e2ea274df4edacbdcfaabc38d768ed Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Thu, 23 May 2024 17:21:10 -0400 Subject: [PATCH 06/11] add e2e test for dlv Signed-off-by: Dan Luhring --- .github/workflows/e2e.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index d0e92387c..571ab2ee9 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -83,3 +83,6 @@ jobs: - "-X main.version=${{ github.sha }}" EOF docker run $(go run ./ build ./test/ --platform=${PLATFORM}) --wait=false 2>&1 | grep "${{ github.sha }}" + + # Check that --debug adds dlv to the image, and that dlv is runnable. + docker run --entrypoint="dlv" $(go run ./ build ./test/ --platform=${PLATFORM} --debug) version | grep "Delve Debugger" From 49efbefa825c1e28619efe8343802dd65b107519 Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Thu, 23 May 2024 20:15:39 -0400 Subject: [PATCH 07/11] docs: add draft for debugging Signed-off-by: Dan Luhring --- docs/features/debugging.md | 29 +++++++++++++++++++++++++++++ mkdocs.yml | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/features/debugging.md diff --git a/docs/features/debugging.md b/docs/features/debugging.md new file mode 100644 index 000000000..93dd2cdec --- /dev/null +++ b/docs/features/debugging.md @@ -0,0 +1,29 @@ +# Debugging + +Sometimes it's challenging to track down the cause of unexpected behavior in an app. Because `ko` makes it simple to make tweaks to your app and immediately rebuild your image, it's possible to iteratively explore various aspects of your app, such as by adding log lines that print variable values. + +But to help you solve the problem _as fast as possible_, `ko` supports debugging your Go app with [delve](https://github.com/go-delve/delve). + +To use this feature, just add the `--debug` flag to your `ko build` command. This adjusts how the image is built: + +- It installs `delve` in the image (in addition to your own app). +- It sets the image's `ENTRYPOINT` to a `delve exec ...` command that runs the Go app in debug-mode, listening on port `40000` for a debugger client. +- It ensures your compiled Go app includes debug symbols needed to enable debugging. + +**Note:** This feature is geared toward development workflows. It **should not** be used in production. + +### How it works + +Build the image using the debug feature. + +```plaintext +ko build . --debug +``` + +Run the container, ensuring that the debug port (`40000`) is exposed to allow clients to connect to it. + +```plaintext +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! diff --git a/mkdocs.yml b/mkdocs.yml index 077c600f1..626da448f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ nav: - features/k8s.md - features/static-assets.md - features/build-cache.md + - features/debugging.md - Advanced: - advanced/go-packages.md - advanced/limitations.md @@ -65,4 +66,4 @@ markdown_extensions: pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - - pymdownx.superfences \ No newline at end of file + - pymdownx.superfences From 847893ca06fcd51dca2f156f2d44705e647b49f1 Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Fri, 24 May 2024 09:40:08 -0400 Subject: [PATCH 08/11] limit platforms supported by debugging Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 008768c91..5ceddbfd3 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -277,6 +277,16 @@ func getGoBinary() string { return defaultGoBin } +func doesPlatformSupportDebugging(platform v1.Platform) bool { + // Here's the list of supported platforms by Delve: + // + // https://github.com/go-delve/delve/blob/master/Documentation/faq.md#unsupportedplatforms + // + // For the time being, we'll support only linux/amd64 and linux/arm64. + + return platform.OS == "linux" && (platform.Architecture == "amd64" || platform.Architecture == "arm64") +} + func getDelve(ctx context.Context, platform v1.Platform) (string, error) { env, err := buildEnv(platform, os.Environ(), nil) if err != nil { @@ -969,6 +979,9 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl OSVersion: cf.OSVersion, } } + if g.debug && !doesPlatformSupportDebugging(*platform) { + return nil, fmt.Errorf("debugging is not supported for %s", platform) + } if !g.platformMatcher.matches(platform) { return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms) From 969a12140a8eda61a76693a67ceba41fd8ad023c Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Sun, 9 Jun 2024 14:14:19 -0400 Subject: [PATCH 09/11] dlv: os_arch dir only if different from runtime Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 5ceddbfd3..adc9a504a 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -288,6 +288,12 @@ func doesPlatformSupportDebugging(platform v1.Platform) bool { } func getDelve(ctx context.Context, platform v1.Platform) (string, error) { + if platform.OS == "" || platform.Architecture == "" { + return "", fmt.Errorf("platform os (%q) or arch (%q) is empty", + platform.OS, + platform.Architecture, + ) + } env, err := buildEnv(platform, os.Environ(), nil) if err != nil { return "", fmt.Errorf("could not create env for Delve build: %w", err) @@ -322,7 +328,7 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { // find the delve binary in tmpInstallDir/bin/ osArchDir := "" - if platform.OS != "" && platform.Architecture != "" { + if platform.OS != runtime.GOOS || platform.Architecture != runtime.GOARCH { osArchDir = fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) } delveBinary := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") From 73a8a50acf1456a926d939e08e78fbf53087778d Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Mon, 10 Jun 2024 15:22:07 -0400 Subject: [PATCH 10/11] clone and build dlv instead of go install Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 43 +++++++++++++++++++++++++++------------ pkg/build/gobuild_test.go | 2 +- pkg/internal/git/clone.go | 25 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 pkg/internal/git/clone.go diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index adc9a504a..b5b397c31 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -288,12 +288,15 @@ func doesPlatformSupportDebugging(platform v1.Platform) bool { } func getDelve(ctx context.Context, platform v1.Platform) (string, error) { + const delveCloneURL = "https://github.com/go-delve/delve.git" + if platform.OS == "" || platform.Architecture == "" { return "", fmt.Errorf("platform os (%q) or arch (%q) is empty", platform.OS, platform.Architecture, ) } + env, err := buildEnv(platform, os.Environ(), nil) if err != nil { return "", fmt.Errorf("could not create env for Delve build: %w", err) @@ -303,13 +306,26 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { if err != nil { return "", fmt.Errorf("could not create tmp dir for Delve installation: %w", err) } + cloneDir := filepath.Join(tmpInstallDir, "delve") + err = os.MkdirAll(cloneDir, 0755) + if err != nil { + return "", fmt.Errorf("making dir for delve clone: %v", err) + } + err = git.Clone(ctx, cloneDir, delveCloneURL) + if err != nil { + return "", fmt.Errorf("cloning delve repo: %v", err) + } + osArchDir := fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) + delveBinaryPath := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") // install delve to tmp directory - env = append(env, fmt.Sprintf("GOPATH=%s", tmpInstallDir)) - args := []string{ - "install", - "github.com/go-delve/delve/cmd/dlv@latest", + "build", + "-C", + cloneDir, + "./cmd/dlv", + "-o", + delveBinaryPath, } gobin := getGoBinary() @@ -326,17 +342,11 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { return "", fmt.Errorf("go build Delve: %w: %s", err, output.String()) } - // find the delve binary in tmpInstallDir/bin/ - osArchDir := "" - if platform.OS != runtime.GOOS || platform.Architecture != runtime.GOARCH { - osArchDir = fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) - } - delveBinary := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") - if _, err := os.Stat(delveBinary); err != nil { - return "", fmt.Errorf("could not find Delve binary at %q: %w", delveBinary, err) + if _, err := os.Stat(delveBinaryPath); err != nil { + return "", fmt.Errorf("could not find Delve binary at %q: %w", delveBinaryPath, err) } - return delveBinary, nil + return delveBinaryPath, nil } func build(ctx context.Context, buildCtx buildContext) (string, error) { @@ -979,6 +989,13 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl return nil, err } if platform == nil { + if cf.OS == "" { + cf.OS = "linux" + } + if cf.Architecture == "" { + cf.Architecture = "amd64" + } + platform = &v1.Platform{ OS: cf.OS, Architecture: cf.Architecture, diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index de5cc403b..2a0b89423 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -1465,7 +1465,7 @@ func TestDebugger(t *testing.T) { context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), - WithPlatforms("all"), + WithPlatforms("linux/amd64"), WithDebugger(), ) if err != nil { diff --git a/pkg/internal/git/clone.go b/pkg/internal/git/clone.go new file mode 100644 index 000000000..05a3c51ec --- /dev/null +++ b/pkg/internal/git/clone.go @@ -0,0 +1,25 @@ +package git + +import ( + "context" + "fmt" + "os/exec" +) + +// Clone the git repository from the repoURL to the specified dir. +func Clone(ctx context.Context, dir string, repoURL string) error { + rc := runConfig{ + dir: dir, + args: []string{"clone", "--depth", "1", repoURL}, + } + + cmd := exec.CommandContext(ctx, "git", "clone", repoURL, dir) + cmd.Dir = dir + + _, err := run(ctx, rc) + if err != nil { + return fmt.Errorf("running git clone: %v", err) + } + + return nil +} From 9a2e1a7affd04cca8fd4c5eeba8843b060f1d9ef Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Mon, 10 Jun 2024 15:25:51 -0400 Subject: [PATCH 11/11] debug: build unsupported platforms with a warning Signed-off-by: Dan Luhring --- pkg/build/gobuild.go | 19 +++++++++++-------- pkg/internal/git/clone.go | 40 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index b5b397c31..15bf34a1f 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -309,11 +309,11 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { cloneDir := filepath.Join(tmpInstallDir, "delve") err = os.MkdirAll(cloneDir, 0755) if err != nil { - return "", fmt.Errorf("making dir for delve clone: %v", err) + return "", fmt.Errorf("making dir for delve clone: %w", err) } err = git.Clone(ctx, cloneDir, delveCloneURL) if err != nil { - return "", fmt.Errorf("cloning delve repo: %v", err) + return "", fmt.Errorf("cloning delve repo: %w", err) } osArchDir := fmt.Sprintf("%s_%s", platform.OS, platform.Architecture) delveBinaryPath := filepath.Join(tmpInstallDir, "bin", osArchDir, "dlv") @@ -321,16 +321,15 @@ func getDelve(ctx context.Context, platform v1.Platform) (string, error) { // install delve to tmp directory args := []string{ "build", - "-C", - cloneDir, - "./cmd/dlv", "-o", delveBinaryPath, + "./cmd/dlv", } gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, args...) cmd.Env = env + cmd.Dir = cloneDir var output bytes.Buffer cmd.Stderr = &output @@ -962,6 +961,10 @@ func (g *gobuild) configForImportPath(ip string) Config { return config } +func (g gobuild) useDebugging(platform v1.Platform) bool { + return g.debug && doesPlatformSupportDebugging(platform) +} + func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, platform *v1.Platform) (oci.SignedImage, error) { if err := g.semaphore.Acquire(ctx, 1); err != nil { return nil, err @@ -1003,7 +1006,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } } if g.debug && !doesPlatformSupportDebugging(*platform) { - return nil, fmt.Errorf("debugging is not supported for %s", platform) + log.Printf("image for platform %q will be built without debugging enabled because debugging is not supported for that platform", *platform) } if !g.platformMatcher.matches(platform) { @@ -1116,7 +1119,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl }) delvePath := "" // path for delve in image - if g.debug { + if g.useDebugging(*platform) { // get delve locally delveBinary, err := getDelve(ctx, *platform) if err != nil { @@ -1182,7 +1185,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl updatePath(cfg, `C:\ko-app`) cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`) } else { - if g.debug { + if g.useDebugging(*platform) { cfg.Config.Entrypoint = append([]string{delvePath}, delveArgs...) cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) } diff --git a/pkg/internal/git/clone.go b/pkg/internal/git/clone.go index 05a3c51ec..628302a0b 100644 --- a/pkg/internal/git/clone.go +++ b/pkg/internal/git/clone.go @@ -1,3 +1,39 @@ +// Copyright 2024 ko Build Authors All Rights Reserved. +// +// 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. + +// MIT License +// +// Copyright (c) 2016-2022 Carlos Alexandro Becker +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package git import ( @@ -10,7 +46,7 @@ import ( func Clone(ctx context.Context, dir string, repoURL string) error { rc := runConfig{ dir: dir, - args: []string{"clone", "--depth", "1", repoURL}, + args: []string{"clone", "--depth", "1", repoURL, "."}, } cmd := exec.CommandContext(ctx, "git", "clone", repoURL, dir) @@ -18,7 +54,7 @@ func Clone(ctx context.Context, dir string, repoURL string) error { _, err := run(ctx, rc) if err != nil { - return fmt.Errorf("running git clone: %v", err) + return fmt.Errorf("running git clone: %w", err) } return nil