From bf307ebbe0f7eade79a3aa9bb3d1611e62705675 Mon Sep 17 00:00:00 2001 From: Riedmann Date: Thu, 29 Aug 2024 13:18:37 +0200 Subject: [PATCH 1/3] refactor: Extract hardcoded default app dir into a constant The currently hardcoded 'ko-app' app directory was used as a hardcoded string in several places in gobuild. To simplify making it overwritable, all occurances are extraced into a constant for the default value. --- pkg/build/gobuild.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 243c8266fd..f31e92fdd3 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -57,6 +57,7 @@ import ( const ( defaultAppFilename = "ko-app" + defaultAppDir = "/ko-app" defaultGoBin = "go" // defaults to first go binary found in PATH goBinPathEnv = "KO_GO_PATH" // env lookup for optional relative or full go binary path @@ -601,12 +602,12 @@ func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) ( // For Windows, the layer must contain a Hives/ directory, and the root // of the actual filesystem goes in a Files/ directory. // For Linux, the binary goes into /ko-app/ - dirs := []string{"ko-app"} + dirs := []string{defaultAppDir} if platform.OS == "windows" { dirs = []string{ "Hives", "Files", - "Files/ko-app", + path.Join("Files", defaultAppDir), } name = "Files" + name } @@ -1057,7 +1058,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl }, }) - appDir := "/ko-app" + appDir := defaultAppDir appFileName := appFilename(ref.Path()) appPath := path.Join(appDir, appFileName) @@ -1103,7 +1104,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } defer os.RemoveAll(filepath.Dir(delveBinary)) - delvePath = path.Join("/ko-app", filepath.Base(delveBinary)) + delvePath = path.Join(defaultAppDir, filepath.Base(delveBinary)) // add layer with delve binary delveLayer, err := g.cache.get(ctx, delveBinary, func() (v1.Layer, error) { @@ -1151,7 +1152,7 @@ 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" { - appPath := `C:\ko-app\` + appFileName + appPath := strings.Replace(path.Join("C:", defaultAppDir, appFileName), "/", `\`, -1) if g.debug { cfg.Config.Entrypoint = append([]string{"C:\\" + delvePath}, delveArgs...) cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) @@ -1159,7 +1160,8 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl cfg.Config.Entrypoint = []string{appPath} } - updatePath(cfg, `C:\ko-app`) + addDirPath := strings.Replace(path.Join("C:", defaultAppDir), "/", `\`, -1) + updatePath(cfg, addDirPath) cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`) } else { if g.useDebugging(*platform) { From a1a788d077de0aca4c933b016aa7a89b4d886798 Mon Sep 17 00:00:00 2001 From: Riedmann Date: Thu, 29 Aug 2024 15:56:30 +0200 Subject: [PATCH 2/3] feat: Allow optionally defining in which directory binaries are placed An option is added to overwrite the default /ko-app folder to place app binaries in. This can be set via cmd line flag or as a default in the config file. --- pkg/build/gobuild.go | 32 ++++--- pkg/build/gobuild_test.go | 161 ++++++++++++++++++++++++++++++++-- pkg/build/options.go | 7 ++ pkg/commands/options/build.go | 12 +++ pkg/commands/resolver.go | 4 + 5 files changed, 197 insertions(+), 19 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index f31e92fdd3..3f282417c8 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -102,6 +102,7 @@ type gobuild struct { defaultLdflags []string platformMatcher *platformMatcher dir string + appDir string labels map[string]string annotations map[string]string user string @@ -133,6 +134,7 @@ type gobuildOpener struct { annotations map[string]string user string dir string + appDir string jobs int debug bool } @@ -169,6 +171,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { labels: gbo.labels, annotations: gbo.annotations, dir: gbo.dir, + appDir: gbo.appDir, debug: gbo.debug, platformMatcher: matcher, cache: &layerCache{ @@ -593,7 +596,7 @@ func appFilename(importpath string) string { // owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" -func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) (*bytes.Buffer, error) { +func tarBinary(name, binary, binaryDir string, platform *v1.Platform, opts *layerOptions) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) tw := tar.NewWriter(buf) defer tw.Close() @@ -602,12 +605,12 @@ func tarBinary(name, binary string, platform *v1.Platform, opts *layerOptions) ( // For Windows, the layer must contain a Hives/ directory, and the root // of the actual filesystem goes in a Files/ directory. // For Linux, the binary goes into /ko-app/ - dirs := []string{defaultAppDir} + dirs := []string{binaryDir} if platform.OS == "windows" { dirs = []string{ "Hives", "Files", - path.Join("Files", defaultAppDir), + path.Join("Files", binaryDir), } name = "Files" + name } @@ -942,6 +945,13 @@ func (g gobuild) useDebugging(platform v1.Platform) bool { return g.debug && doesPlatformSupportDebugging(platform) } +func (g gobuild) getAppDir() string { + if g.appDir != "" { + return g.appDir + } + return defaultAppDir +} + 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 @@ -1058,7 +1068,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl }, }) - appDir := defaultAppDir + appDir := g.getAppDir() appFileName := appFilename(ref.Path()) appPath := path.Join(appDir, appFileName) @@ -1069,7 +1079,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } miss := func() (v1.Layer, error) { - return buildLayer(appPath, file, platform, layerMediaType, &lo) + return buildLayer(g.getAppDir(), appPath, file, platform, layerMediaType, &lo) } var binaryLayer v1.Layer @@ -1104,11 +1114,11 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } defer os.RemoveAll(filepath.Dir(delveBinary)) - delvePath = path.Join(defaultAppDir, filepath.Base(delveBinary)) + delvePath = path.Join(g.getAppDir(), 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) + return buildLayer(appDir, delvePath, delveBinary, platform, layerMediaType, &lo) }) if err != nil { return nil, fmt.Errorf("cache.get(%q): %w", delveBinary, err) @@ -1152,7 +1162,7 @@ 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" { - appPath := strings.Replace(path.Join("C:", defaultAppDir, appFileName), "/", `\`, -1) + appPath := strings.Replace(path.Join("C:", g.getAppDir(), appFileName), "/", `\`, -1) if g.debug { cfg.Config.Entrypoint = append([]string{"C:\\" + delvePath}, delveArgs...) cfg.Config.Entrypoint = append(cfg.Config.Entrypoint, appPath) @@ -1160,7 +1170,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl cfg.Config.Entrypoint = []string{appPath} } - addDirPath := strings.Replace(path.Join("C:", defaultAppDir), "/", `\`, -1) + addDirPath := strings.Replace(path.Join("C:", g.getAppDir()), "/", `\`, -1) updatePath(cfg, addDirPath) cfg.Config.Env = append(cfg.Config.Env, `KO_DATA_PATH=C:\var\run\ko`) } else { @@ -1221,9 +1231,9 @@ type layerOptions struct { linuxCapabilities *caps.FileCaps } -func buildLayer(appPath, file string, platform *v1.Platform, layerMediaType types.MediaType, opts *layerOptions) (v1.Layer, error) { +func buildLayer(appDir, appPath, file string, platform *v1.Platform, layerMediaType types.MediaType, opts *layerOptions) (v1.Layer, error) { // Construct a tarball with the binary and produce a layer. - binaryLayerBuf, err := tarBinary(appPath, file, platform, opts) + binaryLayerBuf, err := tarBinary(appPath, file, appDir, platform, opts) if err != nil { return nil, fmt.Errorf("tarring binary: %w", err) } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index cc5f8d5ab0..940b609d76 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -640,7 +640,13 @@ func TestGoBuildNoKoData(t *testing.T) { }) } -func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) { +type validationOptions struct { + checkAnnotations bool + expectSBOM bool + entryPointBasePath string +} + +func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, opts validationOptions) { t.Helper() ls, err := img.Layers() @@ -722,7 +728,11 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation t.Errorf("len(entrypoint) = %v, want %v", got, want) } - if got, want := entrypoint[0], "/ko-app/test"; got != want { + want := "/ko-app/test" + if opts.entryPointBasePath != "" { + want = path.Join(opts.entryPointBasePath, "test") + } + if got := entrypoint[0]; got != want { t.Errorf("entrypoint = %v, want %v", got, want) } }) @@ -756,7 +766,9 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation pathValue := strings.TrimPrefix(envVar, "PATH=") pathEntries := strings.Split(pathValue, ":") for _, pathEntry := range pathEntries { - if pathEntry == "/ko-app" { + if opts.entryPointBasePath != "" && pathEntry == opts.entryPointBasePath { + found = true + } else if pathEntry == "/ko-app" { found = true } } @@ -780,7 +792,7 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation }) t.Run("check annotations", func(t *testing.T) { - if !checkAnnotations { + if !opts.checkAnnotations { t.Skip("skipping annotations check") } mf, err := img.Manifest() @@ -797,7 +809,7 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation } }) - if expectSBOM { + if opts.expectSBOM { t.Run("checking for SBOM", func(t *testing.T) { f, err := img.Attachment("sbom") if err != nil { @@ -863,7 +875,7 @@ func TestGoBuild(t *testing.T) { t.Fatalf("Build() not a SignedImage: %T", result) } - validateImage(t, img, baseLayers, creationTime, true, true) + validateImage(t, img, baseLayers, creationTime, validationOptions{checkAnnotations: true, expectSBOM: true}) // Check that rebuilding the image again results in the same image digest. t.Run("check determinism", func(t *testing.T) { @@ -1105,7 +1117,82 @@ func TestGoBuildWithoutSBOM(t *testing.T) { t.Fatalf("Build() not a SignedImage: %T", result) } - validateImage(t, img, baseLayers, creationTime, true, false) + validateImage(t, img, baseLayers, creationTime, validationOptions{checkAnnotations: true, expectSBOM: false}) +} + +func TestGoBuild_WithAppDir(t *testing.T) { + baseLayers := int64(3) + base, err := random.Image(1024, baseLayers) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + importpath := "github.com/google/ko" + + creationTime := v1.Time{Time: time.Unix(5000, 0)} + ng, err := NewGo( + context.Background(), + "", + WithCreationTime(creationTime), + WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }), + withBuilder(writeTempFile), + withSBOMber(fauxSBOM), + WithLabel("foo", "bar"), + WithLabel("hello", "world"), + WithPlatforms("all"), + WithAppDir("/usr/local/bin"), + ) + if err != nil { + t.Fatalf("NewGo() = %v", err) + } + + result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) + if err != nil { + t.Fatalf("Build() = %v", err) + } + + img, ok := result.(oci.SignedImage) + if !ok { + t.Fatalf("Build() not a SignedImage: %T", result) + } + + validateImage(t, img, baseLayers, creationTime, validationOptions{checkAnnotations: true, expectSBOM: true, entryPointBasePath: "/usr/local/bin"}) + + // Check that rebuilding the image again results in the same image digest. + t.Run("check determinism", func(t *testing.T) { + result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test")) + if err != nil { + t.Fatalf("Build() = %v", err) + } + + d1, err := result.Digest() + if err != nil { + t.Fatalf("Digest() = %v", err) + } + d2, err := result2.Digest() + if err != nil { + t.Fatalf("Digest() = %v", err) + } + + if d1 != d2 { + t.Errorf("Digest mismatch: %s != %s", d1, d2) + } + }) + + t.Run("check labels", func(t *testing.T) { + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("ConfigFile() = %v", err) + } + + want := map[string]string{ + "foo": "bar", + "hello": "world", + } + got := cfg.Config.Labels + if d := cmp.Diff(got, want); d != "" { + t.Fatalf("Labels diff (-got,+want): %s", d) + } + }) } func TestGoBuildIndex(t *testing.T) { @@ -1151,7 +1238,7 @@ func TestGoBuildIndex(t *testing.T) { if err != nil { t.Fatalf("idx.Image(%s) = %v", desc.Digest, err) } - validateImage(t, img, baseLayers, creationTime, false, true) + validateImage(t, img, baseLayers, creationTime, validationOptions{checkAnnotations: false, expectSBOM: true}) } if want, got := images, int64(len(im.Manifests)); want != got { @@ -1547,3 +1634,61 @@ func TestDebugger(t *testing.T) { } } } + +func TestDebugger_WithAppDirOverwrite(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("linux/amd64"), + WithAppDir("/usr/local/bin"), + 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/local/bin/dlv", + "exec", + "--listen=:40000", + "--headless", + "--log", + "--accept-multiclient", + "--api-version=2", + "--", + "/usr/local/bin/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 75443a9b60..69236f304d 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -210,3 +210,10 @@ func WithDebugger() Option { return nil } } + +func WithAppDir(dir string) Option { + return func(gbo *gobuildOpener) error { + gbo.appDir = dir + return nil + } +} diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index dd9c6eb712..e3981f309c 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -59,6 +59,10 @@ type BuildOptions struct { // Empty string means the current working directory. WorkingDirectory string + // AppDirectory allows for setting where the Go application binaries will be placed in the resulting container. + // Empty string means the default directly /ko-app. + AppDirectory string + ConcurrentBuilds int DisableOptimizations bool SBOM string @@ -103,6 +107,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().StringVar(&bo.AppDirectory, "app-dir", "", + "Directory the application binaries should be placed inside the container instead of default /ko-app.") bo.Trimpath = true } @@ -174,6 +180,12 @@ func (bo *BuildOptions) LoadConfig() error { bo.BaseImage = ref } + if bo.AppDirectory == "" { + if appDir := v.GetString("defaultAppDirectory"); appDir != "" { + bo.AppDirectory = appDir + } + } + if len(bo.BaseImageOverrides) == 0 { baseImageOverrides := map[string]string{} overrides := v.GetStringMapString("baseImageOverrides") diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 829799ca21..93d2d9034c 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -139,6 +139,10 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { opts = append(opts, build.WithSBOMDir(bo.SBOMDir)) } + if bo.AppDirectory != "" { + opts = append(opts, build.WithAppDir(bo.AppDirectory)) + } + return opts, nil } From 0795ff9b315700a447f365afeeb95a011112f0e3 Mon Sep 17 00:00:00 2001 From: Riedmann Date: Fri, 30 Aug 2024 12:37:34 +0200 Subject: [PATCH 3/3] docs: Describe overwriting directory executables are placed into --- docs/configuration.md | 12 ++++++++++++ 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 + 6 files changed, 17 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 5dcff22e1a..bacefe2a65 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -183,6 +183,18 @@ influence the build process. | `KO_CONFIG_PATH` | `./.ko.yaml` | Path to `ko` configuration file (optional) | | `KOCACHE` | (not set) | This tells `ko` to store a local mapping between the `go build` inputs to the image layer that they produce, so `go build` can be skipped entirely if the layer is already present in the image registry (optional). | +## Overwriting which directory binaries are placed in + +By default `ko` places executable binaries of your application into `/ko-app`. + +As it is sometimes desirable to place an executable into a specific folder inside +the container, or into a default executable path like `/bin` or `/usr/local/bin`, +this directory can be overwritten. + +This custom path can be defined either +- via the `--app-dir` command line flag +- or a `defaultAppDirectory` entry in the `.ko.yaml` file. + ## Naming Images `ko` provides a few different strategies for naming the image it pushes, to diff --git a/docs/reference/ko_apply.md b/docs/reference/ko_apply.md index 1dbe2a2423..9cf3f4c717 100644 --- a/docs/reference/ko_apply.md +++ b/docs/reference/ko_apply.md @@ -45,6 +45,7 @@ ko apply -f FILENAME [flags] ### Options ``` + --app-dir string Directory the application binaries should be placed inside the container instead of default /ko-app. --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. diff --git a/docs/reference/ko_build.md b/docs/reference/ko_build.md index c4b62f2539..cdb8ae7161 100644 --- a/docs/reference/ko_build.md +++ b/docs/reference/ko_build.md @@ -42,6 +42,7 @@ ko build IMPORTPATH... [flags] ### Options ``` + --app-dir string Directory the application binaries should be placed inside the container instead of default /ko-app. --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. diff --git a/docs/reference/ko_create.md b/docs/reference/ko_create.md index 51e47cb7d2..4cb48fab97 100644 --- a/docs/reference/ko_create.md +++ b/docs/reference/ko_create.md @@ -45,6 +45,7 @@ ko create -f FILENAME [flags] ### Options ``` + --app-dir string Directory the application binaries should be placed inside the container instead of default /ko-app. --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. diff --git a/docs/reference/ko_resolve.md b/docs/reference/ko_resolve.md index cd51f4408a..4cb5c1c305 100644 --- a/docs/reference/ko_resolve.md +++ b/docs/reference/ko_resolve.md @@ -38,6 +38,7 @@ ko resolve -f FILENAME [flags] ### Options ``` + --app-dir string Directory the application binaries should be placed inside the container instead of default /ko-app. --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. diff --git a/docs/reference/ko_run.md b/docs/reference/ko_run.md index e9ee80ffc0..b3eb25aa4e 100644 --- a/docs/reference/ko_run.md +++ b/docs/reference/ko_run.md @@ -30,6 +30,7 @@ ko run IMPORTPATH [flags] ### Options ``` + --app-dir string Directory the application binaries should be placed inside the container instead of default /ko-app. --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.