Skip to content

Commit febe5b3

Browse files
fix: Use fallback image if devcontainer.json doesn't specify an image or Dockerfile (#30)
Co-authored-by: Kyle Carberry <[email protected]>
1 parent 2bb2298 commit febe5b3

File tree

4 files changed

+56
-17
lines changed

4 files changed

+56
-17
lines changed

devcontainer/devcontainer.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,22 @@ type Compiled struct {
5656
Env []string
5757
}
5858

59+
// HasImage returns true if the devcontainer.json specifies an image.
60+
func (s Spec) HasImage() bool {
61+
return s.Image != ""
62+
}
63+
64+
// HasDockerfile returns true if the devcontainer.json specifies the path to a
65+
// Dockerfile.
66+
func (s Spec) HasDockerfile() bool {
67+
return s.Dockerfile != "" || s.Build.Dockerfile != ""
68+
}
69+
5970
// Compile returns the build parameters for the workspace.
6071
// devcontainerDir is the path to the directory where the devcontainer.json file
6172
// is located. scratchDir is the path to the directory where the Dockerfile will
6273
// be written to if one doesn't exist.
63-
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string) (*Compiled, error) {
74+
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir, fallbackDockerfile string) (*Compiled, error) {
6475
env := make([]string, 0)
6576
for key, value := range s.RemoteEnv {
6677
env = append(env, key+"="+value)
@@ -93,7 +104,11 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string)
93104
s.Build.Context = s.Context
94105
}
95106

96-
params.DockerfilePath = filepath.Join(devcontainerDir, s.Build.Dockerfile)
107+
if s.Build.Dockerfile != "" {
108+
params.DockerfilePath = filepath.Join(devcontainerDir, s.Build.Dockerfile)
109+
} else {
110+
params.DockerfilePath = fallbackDockerfile
111+
}
97112
params.BuildContext = filepath.Join(devcontainerDir, s.Build.Context)
98113
}
99114

devcontainer/devcontainer_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestCompileDevContainer(t *testing.T) {
4444
dc := &devcontainer.Spec{
4545
Image: "codercom/code-server:latest",
4646
}
47-
params, err := dc.Compile(fs, "", envbuilder.MagicDir)
47+
params, err := dc.Compile(fs, "", envbuilder.MagicDir, "")
4848
require.NoError(t, err)
4949
require.Equal(t, filepath.Join(envbuilder.MagicDir, "Dockerfile"), params.DockerfilePath)
5050
require.Equal(t, envbuilder.MagicDir, params.BuildContext)
@@ -69,7 +69,7 @@ func TestCompileDevContainer(t *testing.T) {
6969
_, err = io.WriteString(file, "FROM ubuntu")
7070
require.NoError(t, err)
7171
_ = file.Close()
72-
params, err := dc.Compile(fs, dcDir, envbuilder.MagicDir)
72+
params, err := dc.Compile(fs, dcDir, envbuilder.MagicDir, "")
7373
require.NoError(t, err)
7474
require.Equal(t, "ARG1=value1", params.BuildArgs[0])
7575
require.Equal(t, filepath.Join(dcDir, "Dockerfile"), params.DockerfilePath)

envbuilder.go

+21-13
Original file line numberDiff line numberDiff line change
@@ -314,36 +314,34 @@ func Run(ctx context.Context, options Options) error {
314314
}
315315
}
316316

317-
var buildParams *devcontainer.Compiled
318-
319-
defaultBuildParams := func() error {
317+
defaultBuildParams := func() (*devcontainer.Compiled, error) {
320318
dockerfile := filepath.Join(MagicDir, "Dockerfile")
321319
file, err := options.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0644)
322320
if err != nil {
323-
return err
321+
return nil, err
324322
}
325323
defer file.Close()
326324
if options.FallbackImage == "" {
327325
if fallbackErr != nil {
328-
return xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage)
326+
return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage)
329327
}
330328
// We can't use errors.Join here because our tests
331329
// don't support parsing a multiline error.
332-
return ErrNoFallbackImage
330+
return nil, ErrNoFallbackImage
333331
}
334332
content := "FROM " + options.FallbackImage
335333
_, err = file.Write([]byte(content))
336334
if err != nil {
337-
return err
335+
return nil, err
338336
}
339-
buildParams = &devcontainer.Compiled{
337+
return &devcontainer.Compiled{
340338
DockerfilePath: dockerfile,
341339
DockerfileContent: content,
342340
BuildContext: MagicDir,
343-
}
344-
return nil
341+
}, nil
345342
}
346343

344+
var buildParams *devcontainer.Compiled
347345
if options.DockerfilePath == "" {
348346
// Only look for a devcontainer if a Dockerfile wasn't specified.
349347
// devcontainer is a standard, so it's reasonable to be the default.
@@ -364,7 +362,16 @@ func Run(ctx context.Context, options Options) error {
364362
}
365363
devContainer, err := devcontainer.Parse(content)
366364
if err == nil {
367-
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir)
365+
var fallbackDockerfile string
366+
if !devContainer.HasImage() && !devContainer.HasDockerfile() {
367+
defaultParams, err := defaultBuildParams()
368+
if err != nil {
369+
return fmt.Errorf("no Dockerfile or image found: %w", err)
370+
}
371+
logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...")
372+
fallbackDockerfile = defaultParams.DockerfilePath
373+
}
374+
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile)
368375
if err != nil {
369376
return fmt.Errorf("compile devcontainer.json: %w", err)
370377
}
@@ -393,7 +400,8 @@ func Run(ctx context.Context, options Options) error {
393400
if buildParams == nil {
394401
// If there isn't a devcontainer.json file in the repository,
395402
// we fallback to whatever the `DefaultImage` is.
396-
err := defaultBuildParams()
403+
var err error
404+
buildParams, err = defaultBuildParams()
397405
if err != nil {
398406
return fmt.Errorf("no Dockerfile or devcontainer.json found: %w", err)
399407
}
@@ -543,7 +551,7 @@ func Run(ctx context.Context, options Options) error {
543551
}
544552
logf(codersdk.LogLevelError, "Failed to build: %s", err)
545553
logf(codersdk.LogLevelError, "Falling back to the default image...")
546-
err = defaultBuildParams()
554+
buildParams, err = defaultBuildParams()
547555
if err != nil {
548556
return err
549557
}

integration/integration_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,22 @@ RUN exit 1`,
264264
})
265265
require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error())
266266
})
267+
t.Run("NoImageOrDockerfile", func(t *testing.T) {
268+
t.Parallel()
269+
url := createGitServer(t, gitServerOptions{
270+
files: map[string]string{
271+
".devcontainer/devcontainer.json": "{}",
272+
},
273+
})
274+
ctr, err := runEnvbuilder(t, []string{
275+
"GIT_URL=" + url,
276+
"FALLBACK_IMAGE=alpine:latest",
277+
})
278+
require.NoError(t, err)
279+
280+
output := execContainer(t, ctr, "echo hello")
281+
require.Equal(t, "hello", strings.TrimSpace(output))
282+
})
267283
}
268284

269285
func TestPrivateRegistry(t *testing.T) {

0 commit comments

Comments
 (0)