diff --git a/cmd/agent/container/setup.go b/cmd/agent/container/setup.go index 3060b00ad..74e9f0523 100644 --- a/cmd/agent/container/setup.go +++ b/cmd/agent/container/setup.go @@ -302,6 +302,10 @@ func dockerlessBuild( args = append(args, parseIgnorePaths(dockerlessOptions.IgnorePaths)...) args = append(args, "--build-arg", "TARGETOS="+runtime.GOOS) args = append(args, "--build-arg", "TARGETARCH="+runtime.GOARCH) + if dockerlessOptions.RegistryCache != "" { + log.Debug("Appending registry cache to dockerless build arguments ", dockerlessOptions.RegistryCache) + args = append(args, "--registry-cache", dockerlessOptions.RegistryCache) + } // ignore mounts args = append(args, "--ignore-path", setupInfo.SubstitutionContext.ContainerWorkspaceFolder) diff --git a/cmd/agent/workspace/build.go b/cmd/agent/workspace/build.go index e64982a10..78eaeb605 100644 --- a/cmd/agent/workspace/build.go +++ b/cmd/agent/workspace/build.go @@ -81,9 +81,10 @@ func (cmd *BuildCmd) Run(ctx context.Context) error { for _, platform := range platforms { // build the image imageName, err := runner.Build(ctx, provider2.BuildOptions{ - CLIOptions: workspaceInfo.CLIOptions, - - Platform: platform, + CLIOptions: workspaceInfo.CLIOptions, + RegistryCache: workspaceInfo.RegistryCache, + Platform: platform, + ExportCache: true, }) if err != nil { logger.Errorf("Error building image: %v", err) diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index c81504d45..e1044ff67 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -126,7 +126,8 @@ func (cmd *UpCmd) devPodUp(ctx context.Context, workspaceInfo *provider2.AgentWo // start the devcontainer result, err := runner.Up(ctx, devcontainer.UpOptions{ - CLIOptions: workspaceInfo.CLIOptions, + CLIOptions: workspaceInfo.CLIOptions, + RegistryCache: workspaceInfo.RegistryCache, }, workspaceInfo.InjectTimeout) if err != nil { return nil, err @@ -206,7 +207,7 @@ func initWorkspace(ctx context.Context, cancel context.CancelFunc, workspaceInfo } // install docker in background - errChan := make(chan error, 1) + errChan := make(chan error, 2) go func() { if !workspaceInfo.Agent.IsDockerDriver() || workspaceInfo.Agent.Docker.Install == "false" { errChan <- nil @@ -229,12 +230,28 @@ func initWorkspace(ctx context.Context, cancel context.CancelFunc, workspaceInfo } } - // wait until docker is installed + // wait until docker is installed before configuring docker daemon err = <-errChan if err != nil { return nil, nil, "", errors.Wrap(err, "install docker") } + // If we are provisioning the machine, ensure the daemon has required options + local, err := workspaceInfo.Agent.Local.Bool() + if workspaceInfo.Agent.IsDockerDriver() && err != nil && !local { + errChan <- configureDockerDaemon(ctx, logger) + } else { + logger.Debug("Skipping configuring daemon") + errChan <- nil + } + + // wait until docker daemon is configured + err = <-errChan + if err != nil { + logger.Warn("Could not find docker daemon config file, if using the registry cache, please ensure the daemon is configured with containerd-snapshotter=true") + logger.Warn("More info at https://docs.docker.com/engine/storage/containerd/") + } + return tunnelClient, logger, dockerCredentialsDir, nil } @@ -410,7 +427,7 @@ func prepareImage(workspaceDir, image string) error { return nil } -func installDocker(log log.Logger) error { +func installDocker(log log.Logger) (err error) { if !command.Exists("docker") { writer := log.Writer(logrus.InfoLevel, false) defer writer.Close() @@ -420,11 +437,33 @@ func installDocker(log log.Logger) error { shellCommand := exec.Command("sh", "-c", scripts.InstallDocker) shellCommand.Stdout = writer shellCommand.Stderr = writer - err := shellCommand.Run() - if err != nil { + err = shellCommand.Run() + } + return err +} + +func configureDockerDaemon(ctx context.Context, log log.Logger) (err error) { + log.Info("Configuring docker daemon ...") + // Enable image snapshotter in the dameon + var daemonConfig = []byte(`{ + "features": { + "containerd-snapshotter": true + } + }`) + // Check rootless docker + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + if _, err = os.Stat(fmt.Sprintf("%s/.config/docker", homeDir)); !errors.Is(err, os.ErrNotExist) { + err = os.WriteFile(fmt.Sprintf("%s/.config/docker/daemon.json", homeDir), daemonConfig, 0644) + } + // otherwise assume default + if err != nil { + if err = os.WriteFile("/etc/docker/daemon.json", daemonConfig, 0644); err != nil { return err } } - - return nil + // reload docker daemon + return exec.CommandContext(ctx, "pkill", "-HUP", "dockerd").Run() } diff --git a/e2e/tests/build/build.go b/e2e/tests/build/build.go index c12e4e0c1..5d54dbfc7 100644 --- a/e2e/tests/build/build.go +++ b/e2e/tests/build/build.go @@ -57,13 +57,18 @@ var _ = DevPodDescribe("devpod build test suite", func() { err = f.DevPodBuild(ctx, tempDir, "--force-build", "--platform", "linux/amd64,linux/arm64", "--repository", prebuildRepo, "--skip-push") framework.ExpectNoError(err) + // parse the dockerfile + file, err := dockerfile.Parse(modifiedDockerfileContents) + framework.ExpectNoError(err) + info := &config.ImageBuildInfo{Dockerfile: file} + // make sure images are there - prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, log.Default) + prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, info, log.Default) framework.ExpectNoError(err) _, err = dockerHelper.InspectImage(ctx, prebuildRepo+":"+prebuildHash, false) framework.ExpectNoError(err) - prebuildHash, err = config.CalculatePrebuildHash(cfg, "linux/arm64", "arm64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, log.Default) + prebuildHash, err = config.CalculatePrebuildHash(cfg, "linux/arm64", "arm64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, info, log.Default) framework.ExpectNoError(err) _, err = dockerHelper.InspectImage(ctx, prebuildRepo+":"+prebuildHash, false) framework.ExpectNoError(err) @@ -98,8 +103,13 @@ var _ = DevPodDescribe("devpod build test suite", func() { err = f.DevPodBuild(ctx, tempDir, "--skip-push") framework.ExpectNoError(err) + // parse the dockerfile + file, err := dockerfile.Parse(modifiedDockerfileContents) + framework.ExpectNoError(err) + info := &config.ImageBuildInfo{Dockerfile: file} + // make sure images are there - prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, log.Default) + prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, info, log.Default) framework.ExpectNoError(err) _, err = dockerHelper.InspectImage(ctx, dockerdriver.GetImageName(tempDir, prebuildHash), false) framework.ExpectNoError(err) @@ -154,8 +164,13 @@ var _ = DevPodDescribe("devpod build test suite", func() { err = f.DevPodBuild(ctx, tempDir, "--force-build", "--force-internal-buildkit", "--repository", prebuildRepo, "--skip-push") framework.ExpectNoError(err) + // parse the dockerfile + file, err := dockerfile.Parse(modifiedDockerfileContents) + framework.ExpectNoError(err) + info := &config.ImageBuildInfo{Dockerfile: file} + // make sure images are there - prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, log.Default) + prebuildHash, err := config.CalculatePrebuildHash(cfg, "linux/amd64", "amd64", filepath.Dir(cfg.Origin), dockerfilePath, modifiedDockerfileContents, info, log.Default) framework.ExpectNoError(err) _, err = dockerHelper.InspectImage(ctx, prebuildRepo+":"+prebuildHash, false) diff --git a/examples/build/.devcontainer.json b/examples/build/.devcontainer.json new file mode 100644 index 000000000..33d87350a --- /dev/null +++ b/examples/build/.devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "My dev env", + "build": { + "context": ".", + "dockerfile": "./Dockerfile" + }, + "features":{ + "ghcr.io/devcontainers/features/github-cli:1": {} + } +} diff --git a/examples/build/Dockerfile b/examples/build/Dockerfile new file mode 100644 index 000000000..bcfff340e --- /dev/null +++ b/examples/build/Dockerfile @@ -0,0 +1,30 @@ +FROM mcr.microsoft.com/devcontainers/go:1.22-bullseye + +ARG TARGETOS +ARG TARGETARCH + +# Install Node.js +RUN \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set environment variables for Rust +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=1.69.0 + +# Install Protobuf compiler +RUN \ + apt-get update \ + && apt-get install -y --no-install-recommends protobuf-compiler \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY app /app +COPY files /files + +RUN echo hello \ No newline at end of file diff --git a/examples/build/README.md b/examples/build/README.md new file mode 100644 index 000000000..f615ac901 --- /dev/null +++ b/examples/build/README.md @@ -0,0 +1,6 @@ +## Build Example + +This folder holds a super simple devcontainer configuration that builds a local Dockerfile with a devcontainer feature. You can start this project via: +``` +devpod up ./examples/build +``` diff --git a/pkg/client/clientimplementation/workspace_client.go b/pkg/client/clientimplementation/workspace_client.go index 38bd7fcd5..e81deaf76 100644 --- a/pkg/client/clientimplementation/workspace_client.go +++ b/pkg/client/clientimplementation/workspace_client.go @@ -203,6 +203,9 @@ func (s *workspaceClient) agentInfo(cliOptions provider.CLIOptions) (string, *pr // Get the timeout from the context options agentInfo.InjectTimeout = config.ParseTimeOption(s.devPodConfig, config.ContextOptionAgentInjectTimeout) + // Set registry cache from context option + agentInfo.RegistryCache = s.devPodConfig.ContextOption(config.ContextOptionRegistryCache) + // marshal config out, err := json.Marshal(agentInfo) if err != nil { diff --git a/pkg/config/context.go b/pkg/config/context.go index a65e091d5..b4d676131 100644 --- a/pkg/config/context.go +++ b/pkg/config/context.go @@ -14,6 +14,7 @@ const ( ContextOptionSSHAgentForwarding = "SSH_AGENT_FORWARDING" ContextOptionSSHConfigPath = "SSH_CONFIG_PATH" ContextOptionAgentInjectTimeout = "AGENT_INJECT_TIMEOUT" + ContextOptionRegistryCache = "REGISTRY_CACHE" ) var ContextOptions = []ContextOption{ @@ -86,4 +87,9 @@ var ContextOptions = []ContextOption{ Description: "Specifies the timeout to inject the agent", Default: "20", }, + { + Name: ContextOptionRegistryCache, + Description: "Specifies the registry to use as a build cache, e.g. gcr.io/my-project/my-dev-env", + Default: "", + }, } diff --git a/pkg/devcontainer/build.go b/pkg/devcontainer/build.go index 4c01bb484..fa5ae3101 100644 --- a/pkg/devcontainer/build.go +++ b/pkg/devcontainer/build.go @@ -103,6 +103,7 @@ func (r *runner) build( ImageMetadata: imageMetadata, ImageName: overrideBuildImageName, PrebuildHash: imageTag, + RegistryCache: options.RegistryCache, }, nil } @@ -133,6 +134,7 @@ func (r *runner) extendImage( ImageDetails: imageBuildInfo.ImageDetails, ImageMetadata: extendedBuildInfo.MetadataConfig, ImageName: imageBase, + RegistryCache: options.RegistryCache, }, nil } @@ -287,7 +289,7 @@ func (r *runner) buildImage( return nil, err } - prebuildHash, err := config.CalculatePrebuildHash(parsedConfig.Config, options.Platform, targetArch, config.GetContextPath(parsedConfig.Config), dockerfilePath, dockerfileContent, r.Log) + prebuildHash, err := config.CalculatePrebuildHash(parsedConfig.Config, options.Platform, targetArch, config.GetContextPath(parsedConfig.Config), dockerfilePath, dockerfileContent, buildInfo, r.Log) if err != nil { return nil, err } @@ -319,6 +321,7 @@ func (r *runner) buildImage( ImageMetadata: extendedBuildInfo.MetadataConfig, ImageName: prebuildImage, PrebuildHash: prebuildHash, + RegistryCache: options.RegistryCache, }, nil } else if err != nil { r.Log.Debugf("Error trying to find prebuild image %s: %v", prebuildImage, err) @@ -333,7 +336,7 @@ func (r *runner) buildImage( return nil, fmt.Errorf("cannot build devcontainer because driver is non-docker and dockerless fallback is disabled") } - return dockerlessFallback(r.LocalWorkspaceFolder, substitutionContext.ContainerWorkspaceFolder, parsedConfig, buildInfo, extendedBuildInfo, dockerfileContent) + return dockerlessFallback(r.LocalWorkspaceFolder, substitutionContext.ContainerWorkspaceFolder, parsedConfig, buildInfo, extendedBuildInfo, dockerfileContent, options) } return dockerDriver.BuildDevContainer(ctx, prebuildHash, parsedConfig, extendedBuildInfo, dockerfilePath, dockerfileContent, r.LocalWorkspaceFolder, options) @@ -346,6 +349,7 @@ func dockerlessFallback( buildInfo *config.ImageBuildInfo, extendedBuildInfo *feature.ExtendedBuildInfo, dockerfileContent string, + options provider.BuildOptions, ) (*config.BuildInfo, error) { contextPath := config.GetContextPath(parsedConfig.Config) devPodInternalFolder := filepath.Join(contextPath, config.DevPodContextFeatureFolder) @@ -380,6 +384,7 @@ func dockerlessFallback( User: buildInfo.User, }, + RegistryCache: options.RegistryCache, }, nil } diff --git a/pkg/devcontainer/build/options.go b/pkg/devcontainer/build/options.go index 4a48ece66..b313ac437 100644 --- a/pkg/devcontainer/build/options.go +++ b/pkg/devcontainer/build/options.go @@ -8,6 +8,7 @@ type BuildOptions struct { Images []string CacheFrom []string + CacheTo []string Dockerfile string Context string diff --git a/pkg/devcontainer/buildkit/buildkit.go b/pkg/devcontainer/buildkit/buildkit.go index 958a019c1..850a76c8e 100644 --- a/pkg/devcontainer/buildkit/buildkit.go +++ b/pkg/devcontainer/buildkit/buildkit.go @@ -28,6 +28,10 @@ func Build(ctx context.Context, client *buildkit.Client, writer io.Writer, platf if err != nil { return err } + cacheTo, err := ParseCacheEntry(options.CacheTo) + if err != nil { + return err + } // is context stream? attachable := []session.Attachable{} @@ -42,6 +46,7 @@ func Build(ctx context.Context, client *buildkit.Client, writer io.Writer, platf }, Session: attachable, CacheImports: cacheFrom, + CacheExports: cacheTo, } // set options target diff --git a/pkg/devcontainer/config/build.go b/pkg/devcontainer/config/build.go index 69e59fdbf..89a7521ca 100644 --- a/pkg/devcontainer/config/build.go +++ b/pkg/devcontainer/config/build.go @@ -21,6 +21,7 @@ type BuildInfo struct { ImageMetadata *ImageMetadataConfig ImageName string PrebuildHash string + RegistryCache string Dockerless *BuildInfoDockerless } diff --git a/pkg/devcontainer/config/prebuild.go b/pkg/devcontainer/config/prebuild.go index ce351a4e6..bc5f53651 100644 --- a/pkg/devcontainer/config/prebuild.go +++ b/pkg/devcontainer/config/prebuild.go @@ -15,7 +15,11 @@ import ( "github.com/loft-sh/log/hash" ) -func CalculatePrebuildHash(originalConfig *DevContainerConfig, platform, architecture, contextPath, dockerfilePath, dockerfileContent string, log log.Logger) (string, error) { +func CalculatePrebuildHash( + originalConfig *DevContainerConfig, + platform, architecture, contextPath, dockerfilePath, dockerfileContent string, + buildInfo *ImageBuildInfo, + log log.Logger) (string, error) { parsedConfig := CloneDevContainerConfig(originalConfig) if platform != "" { @@ -57,8 +61,17 @@ func CalculatePrebuildHash(originalConfig *DevContainerConfig, platform, archite } excludes = append(excludes, DevPodContextFeatureFolder+"/") + // find exact files to hash + // todo pass down target or search all + // todo update DirectoryHash function + var includes []string + if buildInfo.Dockerfile != nil { + includes = buildInfo.Dockerfile.BuildContextFiles() + } + log.Debug("Build context files to use for hash are ", includes) + // get hash of the context directory - contextHash, err := util.DirectoryHash(contextPath, excludes) + contextHash, err := util.DirectoryHash(contextPath, excludes, includes) if err != nil { return "", err } diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index 3d9f02ab9..7671c1236 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -85,8 +85,9 @@ type runner struct { type UpOptions struct { provider2.CLIOptions - NoBuild bool - ForceBuild bool + NoBuild bool + ForceBuild bool + RegistryCache string } func (r *runner) Up(ctx context.Context, options UpOptions, timeout time.Duration) (*config.Result, error) { diff --git a/pkg/devcontainer/single.go b/pkg/devcontainer/single.go index 9355c4866..8f9710187 100644 --- a/pkg/devcontainer/single.go +++ b/pkg/devcontainer/single.go @@ -14,7 +14,7 @@ import ( "github.com/pkg/errors" ) -var dockerlessImage = "ghcr.io/loft-sh/dockerless:0.1.4" +var dockerlessImage = "ghcr.io/loft-sh/dockerless:0.2.0" const ( DevPodExtraEnvVar = "DEVPOD" @@ -86,7 +86,9 @@ func (r *runner) runSingleContainer( PrebuildRepositories: options.PrebuildRepositories, ForceDockerless: options.ForceDockerless, }, - NoBuild: options.NoBuild, + NoBuild: options.NoBuild, + RegistryCache: options.RegistryCache, + ExportCache: false, }) if err != nil { return nil, errors.Wrap(err, "build image") @@ -186,6 +188,7 @@ func (r *runner) getDockerlessRunOptions( "DOCKERLESS": "true", "DOCKERLESS_CONTEXT": buildInfo.Dockerless.Context, "DOCKERLESS_DOCKERFILE": buildInfo.Dockerless.Dockerfile, + "GODEBUG": "http2client=0", // https://github.com/GoogleContainerTools/kaniko/issues/875 } for k, v := range mergedConfig.ContainerEnv { env[k] = v diff --git a/pkg/dockerfile/parse.go b/pkg/dockerfile/parse.go index 76fff3a52..740faa4eb 100644 --- a/pkg/dockerfile/parse.go +++ b/pkg/dockerfile/parse.go @@ -73,6 +73,24 @@ func (d *Dockerfile) FindBaseImage(buildArgs map[string]string, target string) s return "" } +// BuildContextFiles traverses a build stage and returns a list of any file path that would affect the build context +func (d *Dockerfile) BuildContextFiles() (files []string) { + // Iterate over all build stages + for _, stage := range d.Stages { + // Add the values of any ADD or COPY instructions + for _, in := range stage.Instructions { + if strings.HasPrefix(in.Value, "ADD") || strings.HasPrefix(in.Value, "COPY") { + // Take all parts except the first (ADD/COPY) and the last (destination on remote), e.g. "COPY src files /app", we want src and files + parts := strings.Split(in.Original, " ") + if len(parts) > 2 { + files = append(files, parts[1:len(parts)-1]...) + } + } + } + } + return files +} + func (d *Dockerfile) replaceVariables(val string, buildArgs map[string]string, baseImageEnv map[string]string, stage *BaseStage, untilLine int) string { newVal := argumentExpression.ReplaceAllFunc([]byte(val), func(match []byte) []byte { subMatches := argumentExpression.FindStringSubmatch(string(match)) diff --git a/pkg/dockerfile/parse_test.go b/pkg/dockerfile/parse_test.go new file mode 100644 index 000000000..99301ef7a --- /dev/null +++ b/pkg/dockerfile/parse_test.go @@ -0,0 +1,24 @@ +package dockerfile + +import ( + _ "embed" + "fmt" + "testing" + + "gotest.tools/assert" +) + +//go:embed test_Dockerfile +var testDockerFileContents string + +func TestBuildContextFiles(t *testing.T) { + dockerFile, err := Parse(testDockerFileContents) + assert.NilError(t, err) + + fmt.Print(dockerFile.Stages) + + files := dockerFile.BuildContextFiles() + assert.Equal(t, len(files), 2) + assert.Equal(t, files[0], "app") + assert.Equal(t, files[1], "files") +} diff --git a/pkg/dockerfile/test_Dockerfile b/pkg/dockerfile/test_Dockerfile new file mode 100644 index 000000000..96f591f5b --- /dev/null +++ b/pkg/dockerfile/test_Dockerfile @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/devcontainers/go:1.22-bullseye + +ARG TARGETOS +ARG TARGETARCH + +# Install Node.js +RUN \ + --mount=type=cache,target=/var/cache/apt \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set environment variables for Rust +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=1.69.0 + +# Install Protobuf compiler +RUN \ + --mount=type=cache,target=/var/cache/apt \ + apt-get update \ + && apt-get install -y --no-install-recommends protobuf-compiler \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY app /app +COPY files /files + +RUN echo hello \ No newline at end of file diff --git a/pkg/driver/docker/build.go b/pkg/driver/docker/build.go index ad69c01ac..c17388aef 100644 --- a/pkg/driver/docker/build.go +++ b/pkg/driver/docker/build.go @@ -45,6 +45,7 @@ func (d *dockerDriver) BuildDevContainer( ImageMetadata: extendedBuildInfo.MetadataConfig, ImageName: imageName, PrebuildHash: prebuildHash, + RegistryCache: options.RegistryCache, }, nil } else if err != nil { d.Log.Debugf("Error trying to find local image %s: %v", imageName, err) @@ -57,10 +58,11 @@ func (d *dockerDriver) BuildDevContainer( } // get build options - buildOptions, err := CreateBuildOptions(dockerfilePath, dockerfileContent, parsedConfig, extendedBuildInfo, imageName, options.Repository, options.PrebuildRepositories, prebuildHash) + buildOptions, err := CreateBuildOptions(dockerfilePath, dockerfileContent, parsedConfig, extendedBuildInfo, imageName, options, prebuildHash) if err != nil { return nil, err } + d.Log.Debug("Using registry cache", options.RegistryCache) // build image writer := d.Log.Writer(logrus.InfoLevel, false) @@ -111,6 +113,7 @@ func (d *dockerDriver) BuildDevContainer( ImageMetadata: extendedBuildInfo.MetadataConfig, ImageName: imageName, PrebuildHash: prebuildHash, + RegistryCache: options.RegistryCache, }, nil } @@ -119,8 +122,7 @@ func CreateBuildOptions( parsedConfig *config.SubstitutedConfig, extendedBuildInfo *feature.ExtendedBuildInfo, imageName string, - pushRepository string, - prebuildRepositories []string, + options provider.BuildOptions, prebuildHash string, ) (*build.BuildOptions, error) { var err error @@ -155,10 +157,10 @@ func CreateBuildOptions( if imageName != "" { buildOptions.Images = append(buildOptions.Images, imageName) } - if pushRepository != "" { - buildOptions.Images = append(buildOptions.Images, pushRepository+":"+prebuildHash) + if options.Repository != "" { + buildOptions.Images = append(buildOptions.Images, options.Repository+":"+prebuildHash) } - for _, prebuildRepository := range prebuildRepositories { + for _, prebuildRepository := range options.PrebuildRepositories { buildOptions.Images = append(buildOptions.Images, prebuildRepository+":"+prebuildHash) } buildOptions.Context = config.GetContextPath(parsedConfig.Config) @@ -167,7 +169,18 @@ func CreateBuildOptions( if buildOptions.BuildArgs == nil { buildOptions.BuildArgs = map[string]string{} } - buildOptions.BuildArgs["BUILDKIT_INLINE_CACHE"] = "1" + + // define cache args + if options.RegistryCache != "" { + buildOptions.CacheFrom = []string{fmt.Sprintf("type=registry,ref=%s", options.RegistryCache)} + // only export cache on build not up, otherwise we slow down the workspace start time + if options.ExportCache { + buildOptions.CacheTo = []string{fmt.Sprintf("type=registry,ref=%s,mode=max,image-manifest=true", options.RegistryCache)} + } + } else { + buildOptions.BuildArgs["BUILDKIT_INLINE_CACHE"] = "1" + } + return buildOptions, nil } @@ -303,6 +316,9 @@ func (d *dockerDriver) buildxBuild(ctx context.Context, writer io.Writer, platfo for _, cacheFrom := range options.CacheFrom { args = append(args, "--cache-from", cacheFrom) } + for _, cacheTo := range options.CacheTo { + args = append(args, "--cache-to", cacheTo) + } // add additional build cli options args = append(args, options.CliOpts...) diff --git a/pkg/options/resolve.go b/pkg/options/resolve.go index 15e778a44..a399cdc8c 100644 --- a/pkg/options/resolve.go +++ b/pkg/options/resolve.go @@ -218,6 +218,7 @@ func ResolveAgentConfig(devConfig *config.Config, provider *provider2.ProviderCo agentConfig.Dockerless.Image = resolver.ResolveDefaultValue(agentConfig.Dockerless.Image, options) agentConfig.Dockerless.Disabled = types.StrBool(resolver.ResolveDefaultValue(string(agentConfig.Dockerless.Disabled), options)) agentConfig.Dockerless.IgnorePaths = resolver.ResolveDefaultValue(agentConfig.Dockerless.IgnorePaths, options) + agentConfig.Dockerless.RegistryCache = devConfig.ContextOption(config.ContextOptionRegistryCache) agentConfig.Driver = resolver.ResolveDefaultValue(agentConfig.Driver, options) agentConfig.Local = types.StrBool(resolver.ResolveDefaultValue(string(agentConfig.Local), options)) agentConfig.Docker.Path = resolver.ResolveDefaultValue(agentConfig.Docker.Path, options) diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index bc2b9ff1d..dd669f8cb 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -133,6 +133,9 @@ type ProviderDockerlessOptions struct { // IgnorePaths are additional ignore paths that should be ignored during deletion IgnorePaths string `json:"ignorePaths,omitempty"` + // Registry to use as remote cache + RegistryCache string `json:"registryCache,omitempty"` + // DisableDockerCredentials prevents docker credentials from getting injected DisableDockerCredentials types.StrBool `json:"disableDockerCredentials,omitempty"` } diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index 4dbc13652..730eaa515 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -176,6 +176,9 @@ type AgentWorkspaceInfo struct { // InjectTimeout specifies how long to wait for the agent to be injected into the dev container InjectTimeout time.Duration `json:"injectTimeout,omitempty"` + + // RegistryCache defines the registry to use for caching builds + RegistryCache string `json:"registryCache,omitempty"` } type CLIOptions struct { @@ -215,8 +218,10 @@ type CLIOptions struct { type BuildOptions struct { CLIOptions - Platform string - NoBuild bool + Platform string + RegistryCache string + ExportCache bool + NoBuild bool } func (w WorkspaceSource) String() string { diff --git a/pkg/util/hash/hash.go b/pkg/util/hash/hash.go index b0c1df8a0..c17f94af6 100644 --- a/pkg/util/hash/hash.go +++ b/pkg/util/hash/hash.go @@ -22,7 +22,7 @@ var ( errFileReadOverLimit = errors.New("read files over limit") ) -func DirectoryHash(srcPath string, excludePatterns []string) (string, error) { +func DirectoryHash(srcPath string, excludePatterns, includeFiles []string) (string, error) { srcPath, err := filepath.Abs(srcPath) if err != nil { return "", err @@ -86,6 +86,17 @@ func DirectoryHash(srcPath string, excludePatterns []string) (string, error) { } relFilePath = filepath.ToSlash(relFilePath) + // Ensure file affects build context + include := false + for _, f := range includeFiles { + if strings.HasPrefix(relFilePath, f) { + include = true + } + } + if !include { + return nil + } + skip := false // If "include" is an exact match for the current file