diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index b34180eff1fd..faf054c8952e 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -258,6 +258,9 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS if err != nil { return nil, err } + if len(stages) == 0 { + return nil, errors.New("dockerfile contains no stages to build") + } validateStageNames(stages, lint) shlex := shell.NewLex(dockerfile.EscapeToken) diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 3ca2ef9d1a68..97c7624ed5aa 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -196,6 +196,7 @@ var allTests = integration.TestFuncs( testInvalidJSONCommands, testHistoryError, testHistoryFinalizeTrace, + testEmptyStages, ) // Tests that depend on the `security.*` entitlements @@ -5479,6 +5480,40 @@ RUN echo $(hostname) | grep foo } } +func testEmptyStages(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + f := getFrontend(t, sb) + dockerfile := []byte(` +ARG foo=bar +`) + + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + ) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir := t.TempDir() + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir, + dockerui.DefaultLocalNameContext: dir, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir, + }, + }, + }, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "dockerfile contains no stages to build") +} + func testShmSize(t *testing.T, sb integration.Sandbox) { integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb)