mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
dockerfile: add FROM --reset
Allow resetting configuration inherited from the base image with FROM --reset=<field>[,<field>...]. Fields are named after the instructions that set them: cmd, entrypoint, env, expose, healthcheck, label, onbuild, shell, stopsignal, user, volume and workdir. "all" resets all of them. Resetting onbuild skips the ONBUILD triggers of the base image. Available in the labs channel (dfreset build tag). Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
9 files changed
+457
-12
No files matched your search
+1
-1
@@ -272,7 +272,7 @@ target "lint" {
|
||||
matrix = {
|
||||
buildtags = [
|
||||
{ name = "default", tags = "", target = "golangci-lint" },
|
||||
{ name = "labs", tags = "dfunset", target = "golangci-lint" },
|
||||
{ name = "labs", tags = "dfunset dfreset", target = "golangci-lint" },
|
||||
{ name = "nydus", tags = "nydus", target = "golangci-lint" },
|
||||
{ name = "yaml", tags = "", target = "yamllint" },
|
||||
{ name = "golangci-verify", tags = "", target = "golangci-verify" },
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/client/llb/imagemetaresolver"
|
||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||
dfcommand "github.com/moby/buildkit/frontend/dockerfile/command"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/linter"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
||||
@@ -568,6 +569,10 @@ func (dctx *dispatchContext) resolveStages(ctx context.Context, target *dispatch
|
||||
}
|
||||
d.onBuildInit = true
|
||||
}
|
||||
if slices.Contains(d.stage.Reset, dfcommand.Onbuild) {
|
||||
onbuilds = nil
|
||||
d.image.Config.OnBuild = nil
|
||||
}
|
||||
|
||||
if len(onbuilds) > 0 {
|
||||
if b, err := initOnBuildTriggers(d, onbuilds, dctx.allDispatchStates, dctx.shlex); err != nil {
|
||||
@@ -789,6 +794,8 @@ func (dctx *dispatchContext) dispatchStages(ctx context.Context, allReachable ma
|
||||
d.platform = &d.opt.targetPlatform
|
||||
}
|
||||
|
||||
resetBaseConfig(d)
|
||||
|
||||
// make sure that PATH is always set
|
||||
if _, ok := shell.EnvsFromSlice(d.image.Config.Env).Get("PATH"); !ok {
|
||||
var osName string
|
||||
@@ -1337,15 +1344,7 @@ func dispatchEnv(d *dispatchState, c *instructions.EnvCommand, lint *linter.Lint
|
||||
if e.Key == "" {
|
||||
return errors.New("ENV --unset names can not be blank")
|
||||
}
|
||||
for _, k := range getEnv(d.state).Keys() {
|
||||
if shell.EqualEnvKeys(k, e.Key) {
|
||||
d.state = d.state.DelEnv(k)
|
||||
}
|
||||
}
|
||||
d.image.Config.Env = delEnv(d.image.Config.Env, e.Key)
|
||||
d.buildArgs = slices.DeleteFunc(d.buildArgs, func(arg instructions.KeyValuePairOptional) bool {
|
||||
return shell.EqualEnvKeys(arg.Key, e.Key)
|
||||
})
|
||||
unsetEnv(d, e.Key)
|
||||
keys = append(keys, e.Key)
|
||||
}
|
||||
return commitToHistory(&d.image, "ENV --unset "+strings.Join(keys, " "), false, nil, d.epoch)
|
||||
@@ -1364,6 +1363,61 @@ func dispatchEnv(d *dispatchState, c *instructions.EnvCommand, lint *linter.Lint
|
||||
return commitToHistory(&d.image, commitMessage.String(), false, nil, d.epoch)
|
||||
}
|
||||
|
||||
// unsetEnv removes an environment variable and a build arg with the same name.
|
||||
func unsetEnv(d *dispatchState, key string) {
|
||||
for _, k := range getEnv(d.state).Keys() {
|
||||
if shell.EqualEnvKeys(k, key) {
|
||||
d.state = d.state.DelEnv(k)
|
||||
}
|
||||
}
|
||||
d.image.Config.Env = delEnv(d.image.Config.Env, key)
|
||||
d.buildArgs = slices.DeleteFunc(d.buildArgs, func(arg instructions.KeyValuePairOptional) bool {
|
||||
return shell.EqualEnvKeys(arg.Key, key)
|
||||
})
|
||||
}
|
||||
|
||||
// resetBaseConfig clears the base image config fields set with FROM --reset.
|
||||
// ONBUILD triggers are skipped when the stages are resolved.
|
||||
func resetBaseConfig(d *dispatchState) {
|
||||
for _, k := range d.stage.Reset {
|
||||
switch k {
|
||||
case dfcommand.Cmd:
|
||||
d.image.Config.Cmd = nil
|
||||
case dfcommand.Entrypoint:
|
||||
d.image.Config.Entrypoint = nil
|
||||
case dfcommand.Env:
|
||||
for _, env := range slices.Clone(d.image.Config.Env) {
|
||||
key, _ := parseKeyValue(env)
|
||||
unsetEnv(d, key)
|
||||
}
|
||||
case dfcommand.Expose:
|
||||
d.image.Config.ExposedPorts = nil
|
||||
case dfcommand.Healthcheck:
|
||||
d.image.Config.Healthcheck = nil
|
||||
case dfcommand.Label:
|
||||
d.image.Config.Labels = nil
|
||||
case dfcommand.Shell:
|
||||
d.image.Config.Shell = nil
|
||||
case dfcommand.StopSignal:
|
||||
d.image.Config.StopSignal = ""
|
||||
case dfcommand.User:
|
||||
d.image.Config.User = ""
|
||||
d.state = d.state.User("")
|
||||
case dfcommand.Volume:
|
||||
d.image.Config.Volumes = nil
|
||||
case dfcommand.Workdir:
|
||||
d.image.Config.WorkingDir = ""
|
||||
d.state = d.state.Dir("/")
|
||||
// workdir no longer depends on the base image
|
||||
d.workdirSet = true
|
||||
}
|
||||
}
|
||||
// ArgsEscaped applies to both cmd and entrypoint
|
||||
if len(d.stage.Reset) > 0 && len(d.image.Config.Cmd) == 0 && len(d.image.Config.Entrypoint) == 0 {
|
||||
d.image.Config.ArgsEscaped = false //nolint:staticcheck // ignore SA1019: field is deprecated in OCI Image spec, but used for backward-compatibility with Docker image spec.
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyEnv, sources []*dispatchState, dopt dispatchOpt) error {
|
||||
var opt []llb.RunOption
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//go:build dfreset
|
||||
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/frontend/dockerui"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/moby/buildkit/util/system"
|
||||
"github.com/moby/buildkit/util/testutil"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allTests = append(allTests, integration.TestFuncs(
|
||||
testResetImageConfig,
|
||||
testResetStageBase,
|
||||
testResetOnbuildDeps,
|
||||
testResetInvalid,
|
||||
)...)
|
||||
}
|
||||
|
||||
func testResetImageConfig(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := client.New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
solve := func(dockerfile, target string) {
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600),
|
||||
)
|
||||
_, err := f.Solve(sb.Context(), c, client.SolveOpt{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: client.ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
baseTarget := registry + "/buildkit/testreset:base"
|
||||
solve(`
|
||||
FROM busybox
|
||||
RUN mkdir -m 0777 /work
|
||||
ENV FOO=foo
|
||||
LABEL a=1
|
||||
VOLUME /data
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK CMD true
|
||||
SHELL ["/bin/sh", "-ec"]
|
||||
STOPSIGNAL SIGINT
|
||||
WORKDIR /work
|
||||
USER 1000
|
||||
ONBUILD RUN touch /tmp/onbuild
|
||||
ENTRYPOINT ["/bin/echo"]
|
||||
CMD ["hello"]
|
||||
`, baseTarget)
|
||||
|
||||
allTarget := registry + "/buildkit/testreset:all"
|
||||
solve(`
|
||||
FROM --reset=all `+baseTarget+`
|
||||
RUN [ ! -e /tmp/onbuild ] && [ "$(id -u)" = 0 ] && [ "$(pwd)" = / ] && [ -z "${FOO:-}" ]
|
||||
`, allTarget)
|
||||
|
||||
readConfig := func(target string) dockerspec.DockerOCIImageConfig {
|
||||
desc, provider, err := contentutil.ProviderFromRef(sb.Context(), target)
|
||||
require.NoError(t, err)
|
||||
img, err := testutil.ReadImage(sb.Context(), provider, desc)
|
||||
require.NoError(t, err)
|
||||
dt, err := content.ReadBlob(sb.Context(), provider, img.Manifest.Config)
|
||||
require.NoError(t, err)
|
||||
var cfg dockerspec.DockerOCIImage
|
||||
require.NoError(t, json.Unmarshal(dt, &cfg))
|
||||
return cfg.Config
|
||||
}
|
||||
|
||||
cfg := readConfig(allTarget)
|
||||
require.Equal(t, []string{"PATH=" + system.DefaultPathEnvUnix}, cfg.Env)
|
||||
require.Empty(t, cfg.Labels)
|
||||
require.Empty(t, cfg.Volumes)
|
||||
require.Empty(t, cfg.ExposedPorts)
|
||||
require.Nil(t, cfg.Healthcheck)
|
||||
require.Empty(t, cfg.Shell)
|
||||
require.Empty(t, cfg.StopSignal)
|
||||
require.Empty(t, cfg.WorkingDir)
|
||||
require.Empty(t, cfg.User)
|
||||
require.Empty(t, cfg.OnBuild)
|
||||
require.Empty(t, cfg.Entrypoint)
|
||||
require.Empty(t, cfg.Cmd)
|
||||
|
||||
partialTarget := registry + "/buildkit/testreset:partial"
|
||||
solve(`
|
||||
FROM --reset=volume --reset=label,volume `+baseTarget+`
|
||||
RUN [ -e /tmp/onbuild ] && [ "$(id -u)" = 1000 ] && [ "$(pwd)" = /work ] && [ "$FOO" = foo ]
|
||||
`, partialTarget)
|
||||
|
||||
cfg = readConfig(partialTarget)
|
||||
require.Empty(t, cfg.Labels)
|
||||
require.Empty(t, cfg.Volumes)
|
||||
require.Contains(t, cfg.Env, "FOO=foo")
|
||||
require.Contains(t, cfg.ExposedPorts, "8080/tcp")
|
||||
require.NotNil(t, cfg.Healthcheck)
|
||||
require.Equal(t, []string{"/bin/sh", "-ec"}, cfg.Shell)
|
||||
require.Equal(t, "SIGINT", cfg.StopSignal)
|
||||
require.Equal(t, "/work", cfg.WorkingDir)
|
||||
require.Equal(t, "1000", cfg.User)
|
||||
require.Equal(t, []string{"/bin/echo"}, cfg.Entrypoint)
|
||||
require.Equal(t, []string{"hello"}, cfg.Cmd)
|
||||
|
||||
// relative WORKDIR after a workdir reset doesn't depend on the base image
|
||||
solve(`# check=error=true
|
||||
FROM --reset=workdir `+baseTarget+`
|
||||
WORKDIR app
|
||||
RUN [ "$(pwd)" = /app ]
|
||||
`, registry+"/buildkit/testreset:workdir")
|
||||
}
|
||||
|
||||
func testResetStageBase(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
dockerfile := []byte(`
|
||||
FROM busybox AS base
|
||||
RUN mkdir -m 0777 /work
|
||||
ENV FOO=foo
|
||||
ARG BAR=bar
|
||||
WORKDIR /work
|
||||
USER 1000
|
||||
ONBUILD RUN touch /tmp/onbuild
|
||||
|
||||
FROM --reset=env,workdir,user,onbuild base AS reset
|
||||
RUN env | sort > /tmp/env && pwd > /tmp/pwd && id -u > /tmp/uid && ls /tmp > /tmp/ls
|
||||
|
||||
FROM base AS keep
|
||||
RUN pwd > /tmp/pwd && id -u > /tmp/uid && ls /tmp > /tmp/ls
|
||||
|
||||
FROM scratch
|
||||
COPY --from=reset /tmp/env /tmp/pwd /tmp/uid /tmp/ls /reset/
|
||||
COPY --from=keep /tmp/pwd /tmp/uid /tmp/ls /keep/
|
||||
`)
|
||||
|
||||
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{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: client.ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
read := func(p string) string {
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, p))
|
||||
require.NoError(t, err)
|
||||
return string(dt)
|
||||
}
|
||||
|
||||
env := read("reset/env")
|
||||
require.NotContains(t, env, "FOO=")
|
||||
require.Contains(t, env, "BAR=bar\n")
|
||||
require.Contains(t, env, "PATH=")
|
||||
require.Equal(t, "/\n", read("reset/pwd"))
|
||||
require.Equal(t, "0\n", read("reset/uid"))
|
||||
require.NotContains(t, read("reset/ls"), "onbuild")
|
||||
|
||||
require.Equal(t, "/work\n", read("keep/pwd"))
|
||||
require.Equal(t, "1000\n", read("keep/uid"))
|
||||
require.Contains(t, read("keep/ls"), "onbuild")
|
||||
}
|
||||
|
||||
// testResetOnbuildDeps checks that skipped ONBUILD triggers don't add
|
||||
// dependencies to the build.
|
||||
func testResetOnbuildDeps(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
dockerfile := []byte(`
|
||||
FROM busybox AS base
|
||||
ONBUILD COPY --from=doesnotexist /foo /foo
|
||||
|
||||
FROM --reset=onbuild base
|
||||
RUN true
|
||||
`)
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
)
|
||||
|
||||
c, err := client.New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testResetInvalid(t *testing.T, sb integration.Sandbox) {
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
c, err := client.New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
for _, tc := range []struct {
|
||||
value string
|
||||
err string
|
||||
}{
|
||||
{value: "volumes", err: `invalid FROM --reset value: "volumes" (did you mean volume?)`},
|
||||
{value: "ENV", err: `invalid FROM --reset value: "ENV" (did you mean env?)`},
|
||||
{value: "ALL", err: `invalid FROM --reset value: "ALL" (did you mean all?)`},
|
||||
} {
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte("FROM --reset="+tc.value+" scratch\n"), 0600),
|
||||
)
|
||||
|
||||
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.ErrorContains(t, err, tc.err)
|
||||
}
|
||||
}
|
||||
@@ -653,6 +653,44 @@ for example [automatic platform ARGs](#automatic-platform-args-in-the-global-sco
|
||||
allow you to force a stage to native build platform (`--platform=$BUILDPLATFORM`),
|
||||
and use it to cross-compile to the target platform inside the stage.
|
||||
|
||||
### FROM --reset
|
||||
|
||||
> [!NOTE]
|
||||
> Not yet available in stable syntax, use [`docker/dockerfile:1-labs`](#syntax)
|
||||
> version.
|
||||
|
||||
```dockerfile
|
||||
FROM --reset=<field>[,<field>...] <image> [AS <name>]
|
||||
```
|
||||
|
||||
The `--reset` flag clears configuration inherited from the base image, so
|
||||
that changes in the base image don't affect these parts of your build. The
|
||||
base image can be an image or another build stage. The flag can be repeated,
|
||||
and its values aren't expanded with build arguments. The following fields can
|
||||
be reset:
|
||||
|
||||
| Field | Effect |
|
||||
|---------------|---------------------------------------------------------------------------|
|
||||
| `cmd` | Removes the default command |
|
||||
| `entrypoint` | Removes the entrypoint |
|
||||
| `env` | Removes environment variables like [`ENV --unset`](#env---unset), the default `PATH` is set again on Linux |
|
||||
| `expose` | Removes exposed ports |
|
||||
| `healthcheck` | Removes the health check |
|
||||
| `label` | Removes labels |
|
||||
| `onbuild` | Skips the [`ONBUILD`](#onbuild) triggers of the base image |
|
||||
| `shell` | Restores the default shell |
|
||||
| `stopsignal` | Removes the stop signal |
|
||||
| `user` | Restores the default user |
|
||||
| `volume` | Removes volumes |
|
||||
| `workdir` | Restores the default working directory (`/`) |
|
||||
| `all` | Resets all of the above, and any fields added in future versions |
|
||||
|
||||
```dockerfile
|
||||
# syntax=docker/dockerfile:1-labs
|
||||
FROM --reset=onbuild,volume,label postgres
|
||||
LABEL org.opencontainers.image.title="my-postgres"
|
||||
```
|
||||
|
||||
### Understand how ARG and FROM interact
|
||||
|
||||
`FROM` instructions support variables that are declared by any `ARG`
|
||||
|
||||
@@ -527,6 +527,7 @@ type Stage struct {
|
||||
OrigCmd string // original FROM command, used for rule checks
|
||||
BaseName string // name of the base stage or source
|
||||
Platform string // platform of base source to use
|
||||
Reset []string // base image config fields to reset, see ResetKeys
|
||||
|
||||
DocComment string // doc-comment directly above the stage
|
||||
|
||||
|
||||
@@ -36,7 +36,30 @@ var (
|
||||
parseRunPostHooks []func(*RunCommand, parseRequest) error
|
||||
)
|
||||
|
||||
var unsetEnabled = false
|
||||
var (
|
||||
unsetEnabled = false
|
||||
resetEnabled = false
|
||||
)
|
||||
|
||||
// ResetAll is the FROM --reset value that resets all ResetKeys.
|
||||
const ResetAll = "all"
|
||||
|
||||
// ResetKeys are the base image config fields that can be reset with
|
||||
// FROM --reset. Each field is named after the command that sets it.
|
||||
var ResetKeys = []string{
|
||||
command.Cmd,
|
||||
command.Entrypoint,
|
||||
command.Env,
|
||||
command.Expose,
|
||||
command.Healthcheck,
|
||||
command.Label,
|
||||
command.Onbuild,
|
||||
command.Shell,
|
||||
command.StopSignal,
|
||||
command.User,
|
||||
command.Volume,
|
||||
command.Workdir,
|
||||
}
|
||||
|
||||
func nodeArgs(node *parser.Node) []string {
|
||||
result := []string{}
|
||||
@@ -451,10 +474,22 @@ func parseFrom(req parseRequest) (*Stage, error) {
|
||||
}
|
||||
|
||||
flPlatform := req.flags.AddString("platform", "")
|
||||
var flReset *Flag
|
||||
if resetEnabled {
|
||||
flReset = req.flags.AddStrings("reset")
|
||||
}
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reset []string
|
||||
if flReset != nil {
|
||||
reset, err = parseResetKeys(flReset.StringValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(req.original)
|
||||
return &Stage{
|
||||
BaseName: req.args[0],
|
||||
@@ -463,12 +498,35 @@ func parseFrom(req parseRequest) (*Stage, error) {
|
||||
SourceCode: code,
|
||||
Commands: []Command{},
|
||||
Platform: flPlatform.Value,
|
||||
Reset: reset,
|
||||
Location: req.location,
|
||||
Comments: req.comments,
|
||||
DocComment: getDocComment(req.comments, stageName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseResetKeys returns the sorted list of fields for FROM --reset values.
|
||||
func parseResetKeys(values []string) ([]string, error) {
|
||||
var keys []string
|
||||
for _, v := range values {
|
||||
for k := range strings.SplitSeq(v, ",") {
|
||||
switch {
|
||||
case k == ResetAll:
|
||||
keys = append(keys, ResetKeys...)
|
||||
case slices.Contains(ResetKeys, k):
|
||||
keys = append(keys, k)
|
||||
case strings.ToLower(k) == ResetAll || slices.Contains(ResetKeys, strings.ToLower(k)):
|
||||
return nil, errors.Errorf("invalid FROM --reset value: %q (did you mean %s?)", k, strings.ToLower(k))
|
||||
default:
|
||||
err := errors.Errorf("invalid FROM --reset value: %q", k)
|
||||
return nil, suggest.WrapError(err, k, append([]string{ResetAll}, ResetKeys...), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(keys)
|
||||
return slices.Compact(keys), nil
|
||||
}
|
||||
|
||||
var validStageName = regexp.MustCompile("^[a-z][a-z0-9-_.]*$")
|
||||
|
||||
func parseBuildStageName(args []string) (stageName string, err error) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build dfreset
|
||||
|
||||
package instructions
|
||||
|
||||
func init() {
|
||||
resetEnabled = true
|
||||
}
|
||||
@@ -286,6 +286,16 @@ func TestUnsetDisabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetDisabled(t *testing.T) {
|
||||
if resetEnabled {
|
||||
t.Skip("--reset is enabled in this build")
|
||||
}
|
||||
ast, err := parser.Parse(strings.NewReader("FROM --reset=all busybox"))
|
||||
require.NoError(t, err)
|
||||
_, err = ParseInstruction(ast.AST.Children[0])
|
||||
require.ErrorContains(t, err, "unknown flag: --reset")
|
||||
}
|
||||
|
||||
func TestRunCmdFlagsUsed(t *testing.T) {
|
||||
dockerfile := "RUN --mount=type=tmpfs,target=/foo/ echo hello"
|
||||
r := strings.NewReader(dockerfile)
|
||||
|
||||
@@ -1 +1 @@
|
||||
dfunset
|
||||
dfunset dfreset
|
||||
Reference in new issue
Block a user