Skip to content

Support providing feature directories in build contexts #117

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 1 commit into from
Mar 30, 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
30 changes: 17 additions & 13 deletions devcontainer/devcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type Compiled struct {
DockerfilePath string
DockerfileContent string
BuildContext string
FeatureContexts map[string]string
BuildArgs []string

User string
Expand Down Expand Up @@ -130,7 +131,7 @@ func (s Spec) HasDockerfile() bool {
// devcontainerDir is the path to the directory where the devcontainer.json file
// is located. scratchDir is the path to the directory where the Dockerfile will
// be written to if one doesn't exist.
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir, fallbackDockerfile, workspaceFolder string) (*Compiled, error) {
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string, fallbackDockerfile, workspaceFolder string, useBuildContexts bool) (*Compiled, error) {
params := &Compiled{
User: s.ContainerUser,
ContainerEnv: s.ContainerEnv,
Expand Down Expand Up @@ -213,25 +214,26 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir, fallbac
if remoteUser == "" {
remoteUser = params.User
}
params.DockerfileContent, err = s.compileFeatures(fs, devcontainerDir, scratchDir, params.User, remoteUser, params.DockerfileContent)
params.DockerfileContent, params.FeatureContexts, err = s.compileFeatures(fs, devcontainerDir, scratchDir, params.User, remoteUser, params.DockerfileContent, useBuildContexts)
if err != nil {
return nil, err
}
return params, nil
}

func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir, containerUser, remoteUser, dockerfileContent string) (string, error) {
func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir string, containerUser, remoteUser, dockerfileContent string, useBuildContexts bool) (string, map[string]string, error) {
// If there are no features, we don't need to do anything!
if len(s.Features) == 0 {
return dockerfileContent, nil
return dockerfileContent, nil, nil
}

featuresDir := filepath.Join(scratchDir, "features")
err := fs.MkdirAll(featuresDir, 0644)
if err != nil {
return "", fmt.Errorf("create features directory: %w", err)
return "", nil, fmt.Errorf("create features directory: %w", err)
}
featureDirectives := []string{}
featureContexts := make(map[string]string)

// TODO: Respect the installation order outlined by the spec:
// https://containers.dev/implementors/features/#installation-order
Expand All @@ -251,7 +253,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
if _, featureRef, ok = strings.Cut(featureRefRaw, "./"); !ok {
featureRefParsed, err := name.NewTag(featureRefRaw)
if err != nil {
return "", fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err)
return "", nil, fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err)
}
featureRef = featureRefParsed.Repository.Name()
}
Expand All @@ -275,19 +277,21 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
featureSha := md5.Sum([]byte(featureRefRaw))
featureName := filepath.Base(featureRef)
featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4]))
err = fs.MkdirAll(featureDir, 0644)
if err != nil {
return "", err
if err := fs.MkdirAll(featureDir, 0644); err != nil {
return "", nil, err
}
spec, err := features.Extract(fs, devcontainerDir, featureDir, featureRefRaw)
if err != nil {
return "", fmt.Errorf("extract feature %s: %w", featureRefRaw, err)
return "", nil, fmt.Errorf("extract feature %s: %w", featureRefRaw, err)
}
directive, err := spec.Compile(containerUser, remoteUser, featureOpts)
directive, err := spec.Compile(featureName, containerUser, remoteUser, useBuildContexts, featureOpts)
if err != nil {
return "", fmt.Errorf("compile feature %s: %w", featureRefRaw, err)
return "", nil, fmt.Errorf("compile feature %s: %w", featureRefRaw, err)
}
featureDirectives = append(featureDirectives, directive)
if useBuildContexts {
featureContexts[featureName] = featureDir
}
}

lines := []string{"\nUSER root"}
Expand All @@ -297,7 +301,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
// we're going to run as root.
lines = append(lines, fmt.Sprintf("USER %s", remoteUser))
}
return strings.Join(append([]string{dockerfileContent}, lines...), "\n"), err
return strings.Join(append([]string{dockerfileContent}, lines...), "\n"), featureContexts, err
}

// UserFromDockerfile inspects the contents of a provided Dockerfile
Expand Down
6 changes: 3 additions & 3 deletions devcontainer/devcontainer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func TestCompileWithFeatures(t *testing.T) {
dc, err := devcontainer.Parse([]byte(raw))
require.NoError(t, err)
fs := memfs.New()
params, err := dc.Compile(fs, "", magicDir, "", "")
params, err := dc.Compile(fs, "", magicDir, "", "", false)
require.NoError(t, err)

// We have to SHA because we get a different MD5 every time!
Expand Down Expand Up @@ -118,7 +118,7 @@ func TestCompileDevContainer(t *testing.T) {
dc := &devcontainer.Spec{
Image: "codercom/code-server:latest",
}
params, err := dc.Compile(fs, "", magicDir, "", "")
params, err := dc.Compile(fs, "", magicDir, "", "", false)
require.NoError(t, err)
require.Equal(t, filepath.Join(magicDir, "Dockerfile"), params.DockerfilePath)
require.Equal(t, magicDir, params.BuildContext)
Expand All @@ -144,7 +144,7 @@ func TestCompileDevContainer(t *testing.T) {
_, err = io.WriteString(file, "FROM ubuntu")
require.NoError(t, err)
_ = file.Close()
params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace")
params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false)
require.NoError(t, err)
require.Equal(t, "ARG1=value1", params.BuildArgs[0])
require.Equal(t, "ARG2=workspace", params.BuildArgs[1])
Expand Down
14 changes: 11 additions & 3 deletions devcontainer/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ type Spec struct {

// Extract unpacks the feature from the image and returns a set of lines
// that should be appended to a Dockerfile to install the feature.
func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any) (string, error) {
func (s *Spec) Compile(featureName, containerUser, remoteUser string, useBuildContexts bool, options map[string]any) (string, error) {
// TODO not sure how we figure out _(REMOTE|CONTAINER)_USER_HOME
// as per the feature spec.
// See https://containers.dev/implementors/features/#user-env-var
Expand All @@ -219,7 +219,11 @@ func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any)
// regardless of map iteration order.
sort.Strings(runDirective)
// See https://containers.dev/implementors/features/#invoking-installsh
runDirective = append([]string{"RUN"}, runDirective...)
if useBuildContexts {
runDirective = append([]string{"RUN", "--mount=type=bind,from=" + featureName + ",target=/envbuilder-features/" + featureName + ",rw"}, runDirective...)
} else {
runDirective = append([]string{"RUN"}, runDirective...)
}
runDirective = append(runDirective, "./install.sh")

comment := ""
Expand All @@ -236,7 +240,11 @@ func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any)
if comment != "" {
lines = append(lines, comment)
}
lines = append(lines, "WORKDIR "+s.Directory)
if useBuildContexts {
lines = append(lines, "WORKDIR /envbuilder-features/"+featureName)
} else {
lines = append(lines, "WORKDIR "+s.Directory)
}
envKeys := make([]string, 0, len(s.ContainerEnv))
for key := range s.ContainerEnv {
envKeys = append(envKeys, key)
Expand Down
17 changes: 13 additions & 4 deletions devcontainer/features/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestCompile(t *testing.T) {
t.Run("UnknownOption", func(t *testing.T) {
t.Parallel()
spec := &features.Spec{}
_, err := spec.Compile("containerUser", "remoteUser", map[string]any{
_, err := spec.Compile("test", "containerUser", "remoteUser", false, map[string]any{
"unknown": "value",
})
require.ErrorContains(t, err, "unknown option")
Expand All @@ -83,7 +83,7 @@ func TestCompile(t *testing.T) {
spec := &features.Spec{
Directory: "/",
}
directive, err := spec.Compile("containerUser", "remoteUser", nil)
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
require.NoError(t, err)
require.Equal(t, "WORKDIR /\nRUN _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
})
Expand All @@ -95,7 +95,7 @@ func TestCompile(t *testing.T) {
"FOO": "bar",
},
}
directive, err := spec.Compile("containerUser", "remoteUser", nil)
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
require.NoError(t, err)
require.Equal(t, "WORKDIR /\nENV FOO=bar\nRUN _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
})
Expand All @@ -109,8 +109,17 @@ func TestCompile(t *testing.T) {
},
},
}
directive, err := spec.Compile("containerUser", "remoteUser", nil)
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
require.NoError(t, err)
require.Equal(t, "WORKDIR /\nRUN FOO=\"bar\" _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
})
t.Run("BuildContext", func(t *testing.T) {
t.Parallel()
spec := &features.Spec{
Directory: "/",
}
directive, err := spec.Compile("test", "containerUser", "remoteUser", true, nil)
require.NoError(t, err)
require.Equal(t, "WORKDIR /envbuilder-features/test\nRUN --mount=type=bind,from=test,target=/envbuilder-features/test,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
})
}
2 changes: 1 addition & 1 deletion envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ func Run(ctx context.Context, options Options) error {
logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...")
fallbackDockerfile = defaultParams.DockerfilePath
}
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder)
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false)
if err != nil {
return fmt.Errorf("compile devcontainer.json: %w", err)
}
Expand Down
Loading