Skip to content
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

Pod 823/cache #1245

Merged
merged 26 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8635fad
POD-823: Use private dockerless WIP
bkneis Sep 2, 2024
e2335f4
POD-823: WIP implment cache for docker
bkneis Sep 3, 2024
ad06732
POD-823: Run example and collect logs
bkneis Sep 3, 2024
aa350ee
POD-823: Remove devpod internal
bkneis Sep 3, 2024
02968d6
POD-823: Cleanup
bkneis Sep 3, 2024
bacb6da
POD-823: Configure docker daemon
bkneis Sep 4, 2024
5b16fbf
POD-823: Append registry cache to build info#
bkneis Sep 4, 2024
590fde7
POD-823: Test kaniko build times
bkneis Sep 4, 2024
c89d96b
POD-823: Implement toggle to export cache manifest
bkneis Sep 4, 2024
6b478a7
POD-823: Improve hashing of image tag using parsed dockerfile
bkneis Sep 5, 2024
1d91603
POD-823: Add todo comment
bkneis Sep 5, 2024
2a14b7c
POD-823: Fix rebase error
bkneis Sep 5, 2024
8af4580
POD-823: Simplify examples build
bkneis Sep 5, 2024
a26cb5a
POD-823: Remove default registry£
bkneis Sep 5, 2024
368c752
POD-823: Fix e2e tests
bkneis Sep 5, 2024
86c89a1
POD-823: Implement unit test for parser
bkneis Sep 5, 2024
d1177c8
POD-823: PR feedback
bkneis Sep 5, 2024
b5169bd
POD-823: PR feedback
bkneis Sep 5, 2024
b2530b2
POD-823: Use official dockerless image
bkneis Sep 6, 2024
f24e013
POD-823: Mount cluster wide cache for dockerless
bkneis Sep 10, 2024
7a2f783
POD-823: Fix unit tests
bkneis Sep 10, 2024
6b568d8
POD-823: Configure docker daemon after install
bkneis Sep 10, 2024
712436d
POD-823: Print warning instead of error for configuring daemon
bkneis Sep 10, 2024
2e15836
POD-823: Comment on use of http1
bkneis Sep 10, 2024
555cbe3
Remove local mount caching
bkneis Sep 10, 2024
aa1e749
POD-823: Remove inner cache
bkneis Sep 11, 2024
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
4 changes: 4 additions & 0 deletions cmd/agent/container/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Info("Appending registry cache to dockerless build arguments ", dockerlessOptions.RegistryCache)
bkneis marked this conversation as resolved.
Show resolved Hide resolved
args = append(args, "--registry-cache", dockerlessOptions.RegistryCache)
}

// ignore mounts
args = append(args, "--ignore-path", setupInfo.SubstitutionContext.ContainerWorkspaceFolder)
Expand Down
7 changes: 4 additions & 3 deletions cmd/agent/workspace/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
145 changes: 138 additions & 7 deletions cmd/agent/workspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workspace

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
Expand All @@ -23,6 +24,8 @@ import (
"github.com/loft-sh/devpod/pkg/devcontainer/crane"
"github.com/loft-sh/devpod/pkg/dockercredentials"
"github.com/loft-sh/devpod/pkg/extract"
"github.com/loft-sh/devpod/pkg/git"
"github.com/loft-sh/devpod/pkg/gitcredentials"
provider2 "github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/scripts"
"github.com/loft-sh/log"
Expand Down Expand Up @@ -126,7 +129,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
Expand Down Expand Up @@ -206,13 +210,19 @@ 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
} else {
errChan <- installDocker(logger)
}
// If we are provisioning the machine, ensure the daemon has required options
if workspaceInfo.Machine != nil {
bkneis marked this conversation as resolved.
Show resolved Hide resolved
errChan <- configureDockerDaemon(ctx, logger)
} else {
errChan <- nil
}
}()

// prepare workspace
Expand All @@ -234,6 +244,11 @@ func initWorkspace(ctx context.Context, cancel context.CancelFunc, workspaceInfo
if err != nil {
return nil, nil, "", errors.Wrap(err, "install docker")
}
// wait until daemon is configured
err = <-errChan
if err != nil {
return nil, nil, "", errors.Wrap(err, "configure docker")
}

return tunnelClient, logger, dockerCredentialsDir, nil
}
Expand Down Expand Up @@ -410,7 +425,101 @@ func prepareImage(workspaceDir, image string) error {
return nil
}

func installDocker(log log.Logger) error {
func cloneRepository(ctx context.Context, workspaceInfo *provider2.AgentWorkspaceInfo, helper string, log log.Logger) error {
s := workspaceInfo.Workspace.Source
log.Info("Clone repository")
log.Infof("URL: %s\n", s.GitRepository)
if s.GitBranch != "" {
log.Infof("Branch: %s\n", s.GitBranch)
}
if s.GitCommit != "" {
log.Infof("Commit: %s\n", s.GitCommit)
}
if s.GitSubPath != "" {
log.Infof("Subpath: %s\n", s.GitSubPath)
}
if s.GitPRReference != "" {
log.Infof("PR: %s\n", s.GitPRReference)
}

workspaceDir := workspaceInfo.ContentFolder
// remove the credential helper or otherwise we will receive strange errors within the container
defer func() {
if helper != "" {
if err := gitcredentials.RemoveHelperFromPath(gitcredentials.GetLocalGitConfigPath(workspaceDir)); err != nil {
log.Errorf("Remove git credential helper: %v", err)
}
}
}()

// check if command exists
if !command.Exists("git") {
local, _ := workspaceInfo.Agent.Local.Bool()
if local {
return fmt.Errorf("seems like git isn't installed on your system. Please make sure to install git and make it available in the PATH")
}
if err := git.InstallBinary(log); err != nil {
return err
}
}

// setup private ssh key if passed in
extraEnv := []string{}
if workspaceInfo.CLIOptions.SSHKey != "" {
sshExtraEnv, err := setupSSHKey(workspaceInfo.CLIOptions.SSHKey, workspaceInfo.Agent.Path)
if err != nil {
return err
}
extraEnv = append(extraEnv, sshExtraEnv...)
}

// run git command
cloner := git.NewCloner(workspaceInfo.CLIOptions.GitCloneStrategy)
gitInfo := git.NewGitInfo(s.GitRepository, s.GitBranch, s.GitCommit, s.GitPRReference, s.GitSubPath)
err := git.CloneRepositoryWithEnv(ctx, gitInfo, extraEnv, workspaceDir, helper, cloner, log)
if err != nil {
return fmt.Errorf("clone repository: %w", err)
}

log.Done("Successfully cloned repository")

return nil
}

func setupSSHKey(key string, agentPath string) ([]string, error) {
keyFile, err := os.CreateTemp("", "")
if err != nil {
return nil, err
}
defer os.Remove(keyFile.Name())
defer keyFile.Close()

if err := writeSSHKey(keyFile, key); err != nil {
return nil, err
}

if err := os.Chmod(keyFile.Name(), 0o400); err != nil {
return nil, err
}

env := []string{"GIT_TERMINAL_PROMPT=0"}
gitSSHCmd := []string{agentPath, "helper", "ssh-git-clone", "--key-file=" + keyFile.Name()}
env = append(env, "GIT_SSH_COMMAND="+command.Quote(gitSSHCmd))

return env, nil
}

func writeSSHKey(key *os.File, sshKey string) error {
data, err := base64.StdEncoding.DecodeString(sshKey)
if err != nil {
return err
}

_, err = key.WriteString(string(data))
return err
}

func installDocker(log log.Logger) (err error) {
if !command.Exists("docker") {
writer := log.Writer(logrus.InfoLevel, false)
defer writer.Close()
Expand All @@ -420,11 +529,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()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

is this the best way to reload the docker daemon? I was surprised docker did not have a command for this and I didn't think I could assume systemd was being used to manage docker

Copy link
Contributor

Choose a reason for hiding this comment

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

this looks scary 😬

Copy link
Contributor

Choose a reason for hiding this comment

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

can't think of a better way to do it though, unless we'd start this build container with already mounted config? is that even possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, maybe I could try to detect how dockerd is managed (systemd,systemctl etc) using uname and if not available use this as a last resort? Yes that is possible, I thought of the mounted config but this would be provider specific and need to be implemented by each machine provider, gcloud, aws, digital ocean etc. :/ At least with HUP it's a reload and not restart but it's a bit "under the hood"

}
7 changes: 7 additions & 0 deletions examples/build/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "My dev env",
"build": {
"context": ".",
"dockerfile": "./Dockerfile"
}
}
35 changes: 35 additions & 0 deletions examples/build/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 test
RUN echo layer

RUN echo hello
6 changes: 6 additions & 0 deletions examples/build/README.md
Original file line number Diff line number Diff line change
@@ -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:
bkneis marked this conversation as resolved.
Show resolved Hide resolved
```
devpod up ./examples/build
```
1 change: 1 addition & 0 deletions examples/build/app/code10
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
Empty file added examples/build/app/test
Empty file.
1 change: 1 addition & 0 deletions examples/build/files/test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
1 change: 1 addition & 0 deletions examples/build/files/test2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
3 changes: 3 additions & 0 deletions pkg/client/clientimplementation/workspace_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
ContextOptionSSHAgentForwarding = "SSH_AGENT_FORWARDING"
ContextOptionSSHConfigPath = "SSH_CONFIG_PATH"
ContextOptionAgentInjectTimeout = "AGENT_INJECT_TIMEOUT"
ContextOptionRegistryCache = "REGISTRY_CACHE"
)

var ContextOptions = []ContextOption{
Expand Down Expand Up @@ -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",
bkneis marked this conversation as resolved.
Show resolved Hide resolved
Default: "gcr.io/pascal-project-387807/my-dev-env", // todo remove
},
}
9 changes: 7 additions & 2 deletions pkg/devcontainer/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (r *runner) build(
ImageMetadata: imageMetadata,
ImageName: overrideBuildImageName,
PrebuildHash: imageTag,
RegistryCache: options.RegistryCache,
}, nil
}

Expand Down Expand Up @@ -133,6 +134,7 @@ func (r *runner) extendImage(
ImageDetails: imageBuildInfo.ImageDetails,
ImageMetadata: extendedBuildInfo.MetadataConfig,
ImageName: imageBase,
RegistryCache: options.RegistryCache,
}, nil
}

Expand Down Expand Up @@ -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, r.Log, buildInfo)
bkneis marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -380,6 +384,7 @@ func dockerlessFallback(

User: buildInfo.User,
},
RegistryCache: options.RegistryCache,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/devcontainer/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type BuildOptions struct {

Images []string
CacheFrom []string
CacheTo []string

Dockerfile string
Context string
Expand Down
5 changes: 5 additions & 0 deletions pkg/devcontainer/buildkit/buildkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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
Expand Down
Loading
Loading