Skip to content

feat: embed binary in image when pushing image #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 24, 2024
38 changes: 38 additions & 0 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,29 @@ func Run(ctx context.Context, options Options) error {
util.AddToDefaultIgnoreList(util.IgnoreListEntry{
Path: ignorePath,
PrefixMatchOnly: false,
AllowedPaths: map[string]struct{}{},
})
}

// In order to allow 'resuming' envbuilder, embed the binary into the image
// if it is being pushed
if options.PushImage {
exePath, err := os.Executable()
if err != nil {
return xerrors.Errorf("get exe path: %w", err)
}
// Add an exception for the current running binary in kaniko ignore list
if err := util.AddAllowedPathToDefaultIgnoreList(exePath); err != nil {
return xerrors.Errorf("add exe path to ignore list: %w", err)
}
// Copy the envbuilder binary into the build context.
buildParams.DockerfileContent += fmt.Sprintf("\nCOPY %s %s", exePath, exePath)
dst := filepath.Join(buildParams.BuildContext, exePath)
if err := copyFile(exePath, dst); err != nil {
return xerrors.Errorf("copy running binary to build context: %w", err)
}
}

// temp move of all ro mounts
tempRemountDest := filepath.Join("/", MagicDir, "mnt")
ignorePrefixes := []string{tempRemountDest, "/proc", "/sys"}
Expand Down Expand Up @@ -1182,3 +1202,21 @@ func maybeDeleteFilesystem(log LoggerFunc, force bool) error {

return util.DeleteFilesystem()
}

func copyFile(src, dst string) error {
content, err := os.ReadFile(src)
if err != nil {
return xerrors.Errorf("read file failed: %w", err)
}

err = os.MkdirAll(filepath.Dir(dst), 0o755)
if err != nil {
return xerrors.Errorf("mkdir all failed: %w", err)
}

err = os.WriteFile(dst, content, 0o644)
if err != nil {
return xerrors.Errorf("write file failed: %w", err)
}
return nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.22.3

// There are a few options we need added to Kaniko!
// See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main
replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20240612094751-9d2f7eaa733c
replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20240623122900-c36d9e29c8b5

require (
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/coder/kaniko v0.0.0-20240612094751-9d2f7eaa733c h1:m/cK7QW+IIydq+7zmuGesY1k6CEZlKooSF+KtIcXke8=
github.com/coder/kaniko v0.0.0-20240612094751-9d2f7eaa733c/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM=
github.com/coder/kaniko v0.0.0-20240623122900-c36d9e29c8b5 h1:Le7TE50D8g3A/fAuXmFsDPw3ne0fh9+ZjYLZFxD6cOA=
github.com/coder/kaniko v0.0.0-20240623122900-c36d9e29c8b5/go.mod h1:YMK7BlxerzLlMwihGxNWUaFoN9LXCij4P+w/8/fNlcM=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
Expand Down
69 changes: 69 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
Expand Down Expand Up @@ -1396,6 +1397,74 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
})
}

func TestEmbedBinaryImage(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would annotate this test file with given/when/then markers.

t.Parallel()

srv := createGitServer(t, gitServerOptions{
files: map[string]string{
".devcontainer/Dockerfile": fmt.Sprintf("FROM %s\nRUN date --utc > /root/date.txt", testImageAlpine),
".devcontainer/devcontainer.json": `{
"name": "Test",
"build": {
"dockerfile": "Dockerfile"
},
}`,
},
})

testReg := setupInMemoryRegistry(t, setupInMemoryRegistryOpts{})
testRepo := testReg + "/test-embed-binary-image"
ref, err := name.ParseReference(testRepo + ":latest")
require.NoError(t, err)

_, err = runEnvbuilder(t, options{env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("CACHE_REPO", testRepo),
envbuilderEnv("PUSH_IMAGE", "1"),
}})
require.NoError(t, err)

_, err = remote.Image(ref)
require.NoError(t, err, "expected image to be present after build + push")

ctx := context.Background()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not a problem at all, but a cleaner approach would be:

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
require.NoError(t, err)
t.Cleanup(func() {
cli.Close()
})

// Pull the image we just built
rc, err := cli.ImagePull(ctx, ref.String(), image.PullOptions{})
require.NoError(t, err)
t.Cleanup(func() { _ = rc.Close() })
_, err = io.ReadAll(rc)
require.NoError(t, err)

// Run it
ctr, err := cli.ContainerCreate(ctx, &container.Config{
Image: ref.String(),
Cmd: []string{"sleep", "infinity"},
Labels: map[string]string{
testContainerLabel: "true",
},
}, nil, nil, nil, "")
require.NoError(t, err)
t.Cleanup(func() {
_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
RemoveVolumes: true,
Force: true,
})
})
err = cli.ContainerStart(ctx, ctr.ID, container.StartOptions{})
require.NoError(t, err)

out := execContainer(t, ctr.ID, "[[ -f \"/.envbuilder/bin/envbuilder\" ]] && echo \"exists\"")
require.Equal(t, "exists", strings.TrimSpace(out))
out = execContainer(t, ctr.ID, "cat /root/date.txt")
require.NotEmpty(t, strings.TrimSpace(out))
}

func TestChownHomedir(t *testing.T) {
t.Parallel()

Expand Down