Skip to content

fix: improve cached image startup and cache features #353

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 5 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 83 additions & 29 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ func Run(ctx context.Context, opts options.Options) error {
}
}

var (
username string
skippedRebuild bool
)
if _, err := os.Stat(magicDir.Image()); errors.Is(err, fs.ErrNotExist) {
if buildParams == nil {
// If there isn't a devcontainer.json file in the repository,
// we fallback to whatever the `DefaultImage` is.
Expand Down Expand Up @@ -343,17 +348,19 @@ func Run(ctx context.Context, opts options.Options) error {
if err := util.AddAllowedPathToDefaultIgnoreList(magicDir.Image()); err != nil {
return fmt.Errorf("add magic image file to ignore list: %w", err)
}
if err := util.AddAllowedPathToDefaultIgnoreList(magicDir.Features()); err != nil {
return fmt.Errorf("add features to ignore list: %w", err)
}
magicTempDir := magicdir.At(buildParams.BuildContext, magicdir.TempDir)
if err := opts.Filesystem.MkdirAll(magicTempDir.Path(), 0o755); err != nil {
return fmt.Errorf("create magic temp dir in build context: %w", err)
}
// Add the magic directives that embed the binary into the built image.
buildParams.DockerfileContent += magicdir.Directives
// Copy the envbuilder binary into the build context.
// External callers will need to specify the path to the desired envbuilder binary.

envbuilderBinDest := filepath.Join(magicTempDir.Path(), "envbuilder")
// Also touch the magic file that signifies the image has been built!
magicImageDest := magicTempDir.Image()

// Clean up after build!
var cleanupOnce sync.Once
cleanupBuildContext = func() {
Expand All @@ -367,14 +374,19 @@ func Run(ctx context.Context, opts options.Options) error {
}
defer cleanupBuildContext()

// Copy the envbuilder binary into the build context. External callers
// will need to specify the path to the desired envbuilder binary.
opts.Logger(log.LevelDebug, "copying envbuilder binary at %q to build context %q", opts.BinaryPath, envbuilderBinDest)
if err := copyFile(opts.Filesystem, opts.BinaryPath, envbuilderBinDest, 0o755); err != nil {
return fmt.Errorf("copy envbuilder binary to build context: %w", err)
}

opts.Logger(log.LevelDebug, "touching magic image file at %q in build context %q", magicImageDest, magicTempDir)
if err := touchFile(opts.Filesystem, magicImageDest, 0o755); err != nil {
return fmt.Errorf("touch magic image file in build context: %w", err)
// Also write the magic file that signifies the image has been built.
// Since the user in the image is set to root, we also store the user
// in the magic file to be used by envbuilder when the image is run.
opts.Logger(log.LevelDebug, "writing magic image file at %q in build context %q", magicImageDest, magicTempDir)
if err := writeFile(opts.Filesystem, magicImageDest, 0o755, fmt.Sprintf("USER=%s\n", buildParams.User)); err != nil {
return fmt.Errorf("write magic image file in build context: %w", err)
}
}

Expand All @@ -393,7 +405,6 @@ func Run(ctx context.Context, opts options.Options) error {
return fmt.Errorf("temp remount: %w", err)
}

skippedRebuild := false
stdoutWriter, closeStdout := log.Writer(opts.Logger)
defer closeStdout()
stderrWriter, closeStderr := log.Writer(opts.Logger)
Expand Down Expand Up @@ -669,14 +680,21 @@ func Run(ctx context.Context, opts options.Options) error {
exportEnvFile.Close()
}

username := configFile.Config.User
username = configFile.Config.User
if buildParams.User != "" {
username = buildParams.User
}
} else {
skippedRebuild = true
magicEnv, err := parseMagicImageFile(opts.Filesystem, magicDir.Image())
if err != nil {
return fmt.Errorf("parse magic env: %w", err)
}
username = magicEnv["USER"]
}
if username == "" {
opts.Logger(log.LevelWarn, "#%d: no user specified, using root", stageNumber)
}

userInfo, err := getUser(username)
if err != nil {
return fmt.Errorf("update user: %w", err)
Expand Down Expand Up @@ -957,7 +975,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
}

defaultBuildParams := func() (*devcontainer.Compiled, error) {
dockerfile := filepath.Join(buildTimeWorkspaceFolder, "Dockerfile")
dockerfile := magicDir.Join("Dockerfile")
file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
Expand All @@ -979,7 +997,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
return &devcontainer.Compiled{
DockerfilePath: dockerfile,
DockerfileContent: content,
BuildContext: buildTimeWorkspaceFolder,
BuildContext: magicDir.Path(),
}, nil
}

Expand Down Expand Up @@ -1019,7 +1037,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
opts.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...")
fallbackDockerfile = defaultParams.DockerfilePath
}
buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, buildTimeWorkspaceFolder, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv)
buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, magicDir.Path(), fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv)
if err != nil {
return nil, fmt.Errorf("compile devcontainer.json: %w", err)
}
Expand Down Expand Up @@ -1110,33 +1128,38 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
// add the magic directives to the Dockerfile content.
// MAGICDIR
buildParams.DockerfileContent += magicdir.Directives

magicTempDir := filepath.Join(buildParams.BuildContext, magicdir.TempDir)
if err := opts.Filesystem.MkdirAll(magicTempDir, 0o755); err != nil {
return nil, fmt.Errorf("create magic temp dir in build context: %w", err)
}
envbuilderBinDest := filepath.Join(magicTempDir, "envbuilder")

// Copy the envbuilder binary into the build context.
opts.Logger(log.LevelDebug, "copying envbuilder binary at %q to build context %q", opts.BinaryPath, envbuilderBinDest)
if err := copyFile(opts.Filesystem, opts.BinaryPath, envbuilderBinDest, 0o755); err != nil {
return nil, xerrors.Errorf("copy envbuilder binary to build context: %w", err)
}

// Also touch the magic file that signifies the image has been built!A
magicImageDest := filepath.Join(magicTempDir, "image")
opts.Logger(log.LevelDebug, "touching magic image file at %q in build context %q", magicImageDest, magicTempDir)
if err := touchFile(opts.Filesystem, magicImageDest, 0o755); err != nil {
return nil, fmt.Errorf("touch magic image file at %q: %w", magicImageDest, err)
}

// Clean up after probe!
defer func() {
// Clean up after we're done!
for _, path := range []string{magicImageDest, envbuilderBinDest, magicTempDir} {
if err := opts.Filesystem.Remove(path); err != nil {
opts.Logger(log.LevelWarn, "failed to clean up magic temp dir from build context: %w", err)
}
}
}()

// Copy the envbuilder binary into the build context. External callers
// will need to specify the path to the desired envbuilder binary.
opts.Logger(log.LevelDebug, "copying envbuilder binary at %q to build context %q", opts.BinaryPath, envbuilderBinDest)
if err := copyFile(opts.Filesystem, opts.BinaryPath, envbuilderBinDest, 0o755); err != nil {
return nil, xerrors.Errorf("copy envbuilder binary to build context: %w", err)
}

// Also write the magic file that signifies the image has been built.
// Since the user in the image is set to root, we also store the user
// in the magic file to be used by envbuilder when the image is run.
opts.Logger(log.LevelDebug, "writing magic image file at %q in build context %q", magicImageDest, magicTempDir)
if err := writeFile(opts.Filesystem, magicImageDest, 0o755, fmt.Sprintf("USER=%s\n", buildParams.User)); err != nil {
return nil, fmt.Errorf("write magic image file in build context: %w", err)
}

stdoutWriter, closeStdout := log.Writer(opts.Logger)
defer closeStdout()
stderrWriter, closeStderr := log.Writer(opts.Logger)
Expand Down Expand Up @@ -1465,12 +1488,43 @@ func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
return nil
}

func touchFile(fs billy.Filesystem, dst string, mode fs.FileMode) error {
f, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
func writeFile(fs billy.Filesystem, dst string, mode fs.FileMode, content string) error {
f, err := fs.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
_, err = f.Write([]byte(content))
if err != nil {
return fmt.Errorf("write file: %w", err)
}
return nil
}

func parseMagicImageFile(fs billy.Filesystem, path string) (map[string]string, error) {
file, err := fs.Open(path)
if err != nil {
return xerrors.Errorf("failed to touch file: %w", err)
return nil, fmt.Errorf("open magic image file: %w", err)
}
defer file.Close()

env := make(map[string]string)
s := bufio.NewScanner(file)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid magic image file format: %q", line)
}
env[parts[0]] = parts[1]
}
if err := s.Err(); err != nil {
return nil, fmt.Errorf("scan magic image file: %w", err)
}
return f.Close()
return env, nil
}

func initDockerConfigJSON(logf log.Func, magicDir magicdir.MagicDir, dockerConfigBase64 string) (func() error, error) {
Expand Down
Loading