mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
Merge pull request #7344 from tonistiigi/dockerfile-reset
dockerfile: add `--unset`, `FROM --reset` and `BUILDKIT_DISABLE_ONBUILD`
This commit is contained in:
19 files changed
+1132
-5
No files matched your search
@@ -61,6 +61,21 @@ func addEnvf(key, value string, replace bool, v ...any) StateOption {
|
||||
}
|
||||
}
|
||||
|
||||
// DelEnv returns a [StateOption] which removes an environment variable from the state.
|
||||
// This is the equivalent of [State.DelEnv]
|
||||
func DelEnv(key string) StateOption {
|
||||
return func(s State) State {
|
||||
return s.withValue(keyEnv, func(ctx context.Context, c *Constraints) (any, error) {
|
||||
env, err := getEnv(s)(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deleted := env.Delete(key)
|
||||
return &deleted, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Dir returns a [StateOption] sets the working directory for the state which will be used to resolve
|
||||
// relative paths as well as the working directory for [State.Run].
|
||||
// See [State.With] for where to use this.
|
||||
|
||||
@@ -326,6 +326,12 @@ func (s State) AddEnvf(key, value string, v ...any) State {
|
||||
return AddEnvf(key, value, v...)(s)
|
||||
}
|
||||
|
||||
// DelEnv returns a new [State] with the provided environment variable removed.
|
||||
// See [DelEnv]
|
||||
func (s State) DelEnv(key string) State {
|
||||
return DelEnv(key)(s)
|
||||
}
|
||||
|
||||
// Dir returns a new [State] with the provided working directory set.
|
||||
// See [Dir]
|
||||
func (s State) Dir(str string) State {
|
||||
|
||||
@@ -32,6 +32,43 @@ func TestStateMeta(t *testing.T) {
|
||||
assert.Equal(t, "abc", v)
|
||||
}
|
||||
|
||||
func TestStateDelEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := Image("foo")
|
||||
s = s.AddEnv("FOO", "abc").AddEnv("BAR", "def")
|
||||
s2 := s.DelEnv("FOO").DelEnv("NOTSET")
|
||||
|
||||
_, ok := getEnvHelper(t, s2, "FOO")
|
||||
assert.False(t, ok)
|
||||
|
||||
env, err := s2.Env(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"BAR=def"}, env.ToArray())
|
||||
|
||||
// original state is not modified
|
||||
v, ok := getEnvHelper(t, s, "FOO")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "abc", v)
|
||||
|
||||
s3 := s2.AddEnv("FOO", "ghi")
|
||||
env, err = s3.Env(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"BAR=def", "FOO=ghi"}, env.ToArray())
|
||||
|
||||
def, err := s2.Run(Shlex("true")).Marshal(t.Context())
|
||||
require.NoError(t, err)
|
||||
var execEnv []string
|
||||
for _, dt := range def.Def {
|
||||
var op pb.Op
|
||||
require.NoError(t, op.UnmarshalVT(dt))
|
||||
if e := op.GetExec(); e != nil {
|
||||
execEnv = e.Meta.Env
|
||||
}
|
||||
}
|
||||
assert.Equal(t, []string{"BAR=def"}, execEnv)
|
||||
}
|
||||
|
||||
func TestFormattingPatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+1
-1
@@ -272,7 +272,7 @@ target "lint" {
|
||||
matrix = {
|
||||
buildtags = [
|
||||
{ name = "default", tags = "", target = "golangci-lint" },
|
||||
{ name = "labs", tags = "", 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"
|
||||
@@ -47,6 +48,7 @@ const (
|
||||
|
||||
sbomScanContext = "BUILDKIT_SBOM_SCAN_CONTEXT"
|
||||
sbomScanStage = "BUILDKIT_SBOM_SCAN_STAGE"
|
||||
disableOnbuild = "BUILDKIT_DISABLE_ONBUILD"
|
||||
)
|
||||
|
||||
var nonEnvArgs = map[string]struct{}{
|
||||
@@ -216,6 +218,7 @@ type dispatchContext struct {
|
||||
allDispatchStates *dispatchStates
|
||||
proxyEnv *llb.ProxyEnv
|
||||
namedContext func(string, dockerui.ContextOpt) (*dockerui.NamedContext, error)
|
||||
disableOnbuild bool
|
||||
}
|
||||
|
||||
func namedContextFunc(opt ConvertOpt) func(string, dockerui.ContextOpt) (*dockerui.NamedContext, error) {
|
||||
@@ -310,6 +313,14 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
|
||||
globalArgs = setBuildArgValue(opt.BuildArgs, globalArgs, "SOURCE_DATE_EPOCH", formatSourceDateEpochValue(resolvedEpoch))
|
||||
}
|
||||
|
||||
var skipOnbuild bool
|
||||
if v, ok := getBuildArgValue(opt.BuildArgs, globalArgs, disableOnbuild); ok && v != "" {
|
||||
skipOnbuild, err = strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid %s value %q", disableOnbuild, v)
|
||||
}
|
||||
}
|
||||
|
||||
metaResolver := opt.MetaResolver
|
||||
if metaResolver == nil {
|
||||
metaResolver = imagemetaresolver.Default()
|
||||
@@ -327,6 +338,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
|
||||
allDispatchStates: newDispatchStates(),
|
||||
proxyEnv: proxyEnvFromBuildArgs(opt.BuildArgs),
|
||||
namedContext: namedContextFunc(opt),
|
||||
disableOnbuild: skipOnbuild,
|
||||
}
|
||||
|
||||
if err := dctx.buildDispatchStates(stages); err != nil {
|
||||
@@ -568,6 +580,10 @@ func (dctx *dispatchContext) resolveStages(ctx context.Context, target *dispatch
|
||||
}
|
||||
d.onBuildInit = true
|
||||
}
|
||||
if dctx.disableOnbuild || 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 +805,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
|
||||
@@ -1331,6 +1349,17 @@ func initOnBuildTriggers(d *dispatchState, triggers []string, allDispatchStates
|
||||
}
|
||||
|
||||
func dispatchEnv(d *dispatchState, c *instructions.EnvCommand, lint *linter.Linter) error {
|
||||
if c.Unset {
|
||||
keys := make([]string, 0, len(c.Env))
|
||||
for _, e := range c.Env {
|
||||
if e.Key == "" {
|
||||
return errors.New("ENV --unset names can not be blank")
|
||||
}
|
||||
unsetEnv(d, e.Key)
|
||||
keys = append(keys, e.Key)
|
||||
}
|
||||
return commitToHistory(&d.image, "ENV --unset "+strings.Join(keys, " "), false, nil, d.epoch)
|
||||
}
|
||||
commitMessage := bytes.NewBufferString("ENV")
|
||||
for _, e := range c.Env {
|
||||
if e.NoDelim {
|
||||
@@ -1345,6 +1374,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
|
||||
|
||||
@@ -1575,6 +1659,17 @@ func dispatchMaintainer(d *dispatchState, c *instructions.MaintainerCommand) err
|
||||
}
|
||||
|
||||
func dispatchLabel(d *dispatchState, c *instructions.LabelCommand, lint *linter.Linter) error {
|
||||
if c.Unset {
|
||||
keys := make([]string, 0, len(c.Labels))
|
||||
for _, v := range c.Labels {
|
||||
if v.Key == "" {
|
||||
return errors.New("LABEL --unset names can not be blank")
|
||||
}
|
||||
delete(d.image.Config.Labels, v.Key)
|
||||
keys = append(keys, v.Key)
|
||||
}
|
||||
return commitToHistory(&d.image, "LABEL --unset "+strings.Join(keys, " "), false, nil, d.epoch)
|
||||
}
|
||||
commitMessage := bytes.NewBufferString("LABEL")
|
||||
if d.image.Config.Labels == nil {
|
||||
d.image.Config.Labels = make(map[string]string, len(c.Labels))
|
||||
@@ -1652,13 +1747,24 @@ func dispatchUser(d *dispatchState, c *instructions.UserCommand, commit bool) er
|
||||
}
|
||||
|
||||
func dispatchVolume(d *dispatchState, c *instructions.VolumeCommand) error {
|
||||
if slices.Contains(c.Volumes, "") {
|
||||
return errors.New("VOLUME specified can not be an empty string")
|
||||
}
|
||||
if c.Unset {
|
||||
for _, v := range c.Volumes {
|
||||
v = path.Clean(v)
|
||||
for k := range d.image.Config.Volumes {
|
||||
if path.Clean(k) == v {
|
||||
delete(d.image.Config.Volumes, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return commitToHistory(&d.image, fmt.Sprintf("VOLUME --unset %v", c.Volumes), false, nil, d.epoch)
|
||||
}
|
||||
if d.image.Config.Volumes == nil {
|
||||
d.image.Config.Volumes = map[string]struct{}{}
|
||||
}
|
||||
for _, v := range c.Volumes {
|
||||
if v == "" {
|
||||
return errors.New("VOLUME specified can not be an empty string")
|
||||
}
|
||||
d.image.Config.Volumes[v] = struct{}{}
|
||||
}
|
||||
return commitToHistory(&d.image, fmt.Sprintf("VOLUME %v", c.Volumes), false, nil, d.epoch)
|
||||
@@ -1770,6 +1876,13 @@ func addEnv(env []string, k, v string) []string {
|
||||
return env
|
||||
}
|
||||
|
||||
func delEnv(env []string, k string) []string {
|
||||
return slices.DeleteFunc(env, func(envVar string) bool {
|
||||
key, _ := parseKeyValue(envVar)
|
||||
return shell.EqualEnvKeys(key, k)
|
||||
})
|
||||
}
|
||||
|
||||
func parseKeyValue(env string) (string, string) {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
v := ""
|
||||
|
||||
@@ -701,3 +701,55 @@ RUN --mount=type=cache,target=C:\cache type C:\cache\foo | findstr "42" && type
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testOnBuildDisabled(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
c, err := client.New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
solve := func(dockerfile string, buildArgs map[string]string) error {
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600),
|
||||
)
|
||||
attrs := map[string]string{}
|
||||
for k, v := range buildArgs {
|
||||
attrs["build-arg:"+k] = v
|
||||
}
|
||||
_, err := f.Solve(sb.Context(), c, client.SolveOpt{
|
||||
FrontendAttrs: attrs,
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
dockerfile := `
|
||||
FROM busybox AS base
|
||||
ONBUILD RUN false
|
||||
|
||||
FROM base
|
||||
RUN true
|
||||
`
|
||||
err = solve(dockerfile, nil)
|
||||
require.ErrorContains(t, err, "did not complete successfully")
|
||||
|
||||
err = solve(dockerfile, map[string]string{"BUILDKIT_DISABLE_ONBUILD": "1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = solve(dockerfile, map[string]string{"BUILDKIT_DISABLE_ONBUILD": "maybe"})
|
||||
require.ErrorContains(t, err, `invalid BUILDKIT_DISABLE_ONBUILD value "maybe"`)
|
||||
|
||||
dockerfile = "ARG BUILDKIT_DISABLE_ONBUILD=true\n" + dockerfile
|
||||
|
||||
err = solve(dockerfile, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = solve(dockerfile, map[string]string{"BUILDKIT_DISABLE_ONBUILD": "false"})
|
||||
require.ErrorContains(t, err, "did not complete successfully")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,7 @@ var allTests = integration.TestFuncs(
|
||||
testOnBuildInheritedStageWithFrom,
|
||||
testOnBuildNewDeps,
|
||||
testOnBuildWithCacheMount,
|
||||
testOnBuildDisabled,
|
||||
|
||||
// dockerfile_platform_test.go
|
||||
testPlatformArgsImplicit,
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
//go:build dfunset
|
||||
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/testutil"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allTests = append(allTests, integration.TestFuncs(
|
||||
testUnsetImageConfig,
|
||||
testUnsetRunEnv,
|
||||
testUnsetInvalid,
|
||||
)...)
|
||||
}
|
||||
|
||||
func testUnsetImageConfig(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()
|
||||
|
||||
dockerfile := []byte(`
|
||||
FROM busybox
|
||||
ENV FOO=foo BAR=bar BAZ=baz
|
||||
LABEL a=1 b=2 c=3
|
||||
VOLUME ["/data/", "/cache", "/keep"]
|
||||
`)
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
)
|
||||
|
||||
baseTarget := registry + "/buildkit/testunset:base"
|
||||
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: client.ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": baseTarget,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dockerfile = []byte(`
|
||||
FROM ` + baseTarget + ` AS base
|
||||
ENV --unset FOO BAZ
|
||||
LABEL --unset a
|
||||
ARG name=b
|
||||
LABEL --unset ${name}
|
||||
VOLUME --unset /data /cache/ /notexist
|
||||
ARG BOTH=arg
|
||||
ENV BOTH=env
|
||||
ENV --unset BOTH
|
||||
RUN true
|
||||
|
||||
FROM base
|
||||
ENV --unset=false NEW=new
|
||||
`)
|
||||
dir = integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
)
|
||||
|
||||
target := registry + "/buildkit/testunset:child"
|
||||
_, 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)
|
||||
|
||||
desc, provider, err := contentutil.ProviderFromRef(sb.Context(), target)
|
||||
require.NoError(t, err)
|
||||
|
||||
img, err := testutil.ReadImage(sb.Context(), provider, desc)
|
||||
require.NoError(t, err)
|
||||
|
||||
var envKeys []string
|
||||
for _, env := range img.Img.Config.Env {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
envKeys = append(envKeys, k)
|
||||
}
|
||||
require.Contains(t, envKeys, "PATH")
|
||||
require.Contains(t, img.Img.Config.Env, "BAR=bar")
|
||||
require.Contains(t, img.Img.Config.Env, "NEW=new")
|
||||
require.NotContains(t, envKeys, "FOO")
|
||||
require.NotContains(t, envKeys, "BAZ")
|
||||
|
||||
require.Equal(t, map[string]string{"c": "3"}, img.Img.Config.Labels)
|
||||
|
||||
require.Equal(t, []string{"/keep"}, slices.Collect(maps.Keys(img.Img.Config.Volumes)))
|
||||
|
||||
var history []string
|
||||
for _, h := range img.Img.History {
|
||||
history = append(history, h.CreatedBy)
|
||||
}
|
||||
require.Contains(t, history, "ENV --unset FOO BAZ")
|
||||
require.Contains(t, history, "LABEL --unset b")
|
||||
require.Contains(t, history, "VOLUME --unset [/data /cache/ /notexist]")
|
||||
|
||||
var runHistory string
|
||||
for _, h := range history {
|
||||
if strings.Contains(h, "/bin/sh -c true") {
|
||||
runHistory = h
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, runHistory)
|
||||
require.NotContains(t, runHistory, "BOTH")
|
||||
}
|
||||
|
||||
func testUnsetRunEnv(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
f := getFrontend(t, sb)
|
||||
|
||||
dockerfile := []byte(`
|
||||
FROM busybox AS base
|
||||
ARG FROMCLI
|
||||
ENV ENVVAR=env KEEP=keep
|
||||
RUN env | sort > /env0
|
||||
ENV --unset ENVVAR FROMCLI
|
||||
ENV EXPANDED=${ENVVAR:-none}-${FROMCLI:-none}
|
||||
RUN env | sort > /env1
|
||||
|
||||
ARG BOTH=arg
|
||||
ENV BOTH=env
|
||||
ENV --unset BOTH
|
||||
RUN env | sort > /env2
|
||||
|
||||
ONBUILD ENV --unset KEEP
|
||||
|
||||
FROM base AS child
|
||||
RUN env | sort > /env3
|
||||
ARG FROMCLI
|
||||
RUN env | sort > /env4
|
||||
|
||||
FROM scratch
|
||||
COPY --from=child /env0 /env1 /env2 /env3 /env4 /
|
||||
`)
|
||||
|
||||
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{
|
||||
FrontendAttrs: map[string]string{
|
||||
"build-arg:FROMCLI": "cli",
|
||||
},
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: client.ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
dockerui.DefaultLocalNameDockerfile: dir,
|
||||
dockerui.DefaultLocalNameContext: dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "env0"))
|
||||
require.NoError(t, err)
|
||||
env := string(dt)
|
||||
require.Contains(t, env, "ENVVAR=env\n")
|
||||
require.Contains(t, env, "FROMCLI=cli\n")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "env1"))
|
||||
require.NoError(t, err)
|
||||
env = string(dt)
|
||||
require.Contains(t, env, "KEEP=keep\n")
|
||||
require.Contains(t, env, "EXPANDED=none-none\n")
|
||||
require.NotContains(t, env, "ENVVAR=")
|
||||
require.NotContains(t, env, "FROMCLI=")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "env2"))
|
||||
require.NoError(t, err)
|
||||
env = string(dt)
|
||||
require.NotContains(t, env, "BOTH=")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "env3"))
|
||||
require.NoError(t, err)
|
||||
env = string(dt)
|
||||
require.NotContains(t, env, "KEEP=")
|
||||
require.NotContains(t, env, "FROMCLI=")
|
||||
require.Contains(t, env, "EXPANDED=none-none\n")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "env4"))
|
||||
require.NoError(t, err)
|
||||
env = string(dt)
|
||||
require.Contains(t, env, "FROMCLI=cli\n")
|
||||
}
|
||||
|
||||
func testUnsetInvalid(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 {
|
||||
dockerfile string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
dockerfile: "FROM scratch\nENV --unset FOO=bar\n",
|
||||
err: "ENV --unset does not accept values: FOO=bar",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nLABEL --unset foo=bar\n",
|
||||
err: "LABEL --unset does not accept values: foo=bar",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nENV --unset\n",
|
||||
err: "ENV --unset requires at least one argument",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nARG --unset FOO\n",
|
||||
err: "unknown flag: --unset",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nENV --unset=yes FOO\n",
|
||||
err: "expecting boolean value for flag --unset, not: yes",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nARG EMPTY\nVOLUME --unset ${EMPTY}\n",
|
||||
err: "VOLUME specified can not be an empty string",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nARG EMPTY\nENV --unset ${EMPTY}\n",
|
||||
err: "ENV --unset names can not be blank",
|
||||
},
|
||||
{
|
||||
dockerfile: "FROM scratch\nARG EMPTY\nLABEL --unset ${EMPTY}\n",
|
||||
err: "LABEL --unset names can not be blank",
|
||||
},
|
||||
} {
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", []byte(tc.dockerfile), 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, tc.dockerfile)
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
@@ -1216,6 +1254,25 @@ $ docker image inspect --format='{{json .Config.Labels}}' myimage
|
||||
}
|
||||
```
|
||||
|
||||
### LABEL --unset
|
||||
|
||||
> [!NOTE]
|
||||
> Not yet available in stable syntax, use [`docker/dockerfile:1-labs`](#syntax)
|
||||
> version.
|
||||
|
||||
```dockerfile
|
||||
LABEL --unset <key> [<key>...]
|
||||
```
|
||||
|
||||
The `--unset` flag removes labels from the image, including labels inherited
|
||||
from the base image. Removing a label that isn't set is not an error.
|
||||
|
||||
```dockerfile
|
||||
# syntax=docker/dockerfile:1-labs
|
||||
FROM alpine
|
||||
LABEL --unset org.opencontainers.image.description org.opencontainers.image.url
|
||||
```
|
||||
|
||||
## MAINTAINER (deprecated)
|
||||
|
||||
```dockerfile
|
||||
@@ -1359,6 +1416,35 @@ RUN apt-get update && apt-get install -y ...
|
||||
> The alternative syntax is supported for backward compatibility, but discouraged
|
||||
> for the reasons outlined above, and may be removed in a future release.
|
||||
|
||||
### ENV --unset
|
||||
|
||||
> [!NOTE]
|
||||
> Not yet available in stable syntax, use [`docker/dockerfile:1-labs`](#syntax)
|
||||
> version.
|
||||
|
||||
```dockerfile
|
||||
ENV --unset <key> [<key>...]
|
||||
```
|
||||
|
||||
The `--unset` flag removes environment variables, including variables inherited
|
||||
from the base image. The variables are removed from the image configuration and
|
||||
from the environment of subsequent build steps. A build argument with the same
|
||||
name is removed as well. Declare the build argument again with [`ARG`](#arg) to
|
||||
make it available to later build steps. Removing a variable that isn't set is
|
||||
not an error. Values set by earlier instructions remain in the image history.
|
||||
|
||||
When `PATH` is removed, `RUN` instructions on Linux use the default `PATH`, and
|
||||
stages based on this stage add the default `PATH` back to their image
|
||||
configuration.
|
||||
|
||||
```dockerfile
|
||||
# syntax=docker/dockerfile:1-labs
|
||||
FROM busybox
|
||||
ENV TEMP_TOKEN_PATH=/tmp/token
|
||||
RUN ./setup.sh
|
||||
ENV --unset TEMP_TOKEN_PATH
|
||||
```
|
||||
|
||||
## ADD
|
||||
|
||||
ADD has two forms.
|
||||
@@ -2461,6 +2547,28 @@ Keep the following things in mind about volumes in the Dockerfile.
|
||||
within the Dockerfile. The `VOLUME` instruction does not support specifying a `host-dir`
|
||||
parameter. You must specify the mountpoint when you create or run the container.
|
||||
|
||||
### VOLUME --unset
|
||||
|
||||
> [!NOTE]
|
||||
> Not yet available in stable syntax, use [`docker/dockerfile:1-labs`](#syntax)
|
||||
> version.
|
||||
|
||||
```dockerfile
|
||||
VOLUME --unset ["/data"]
|
||||
VOLUME --unset /var/log /var/db
|
||||
```
|
||||
|
||||
The `--unset` flag removes volumes from the image configuration, including
|
||||
volumes inherited from the base image. Trailing and duplicate slashes are
|
||||
ignored when comparing paths, so `/data/` matches a volume defined as `/data`.
|
||||
Removing a volume that isn't defined is not an error.
|
||||
|
||||
```dockerfile
|
||||
# syntax=docker/dockerfile:1-labs
|
||||
FROM postgres
|
||||
VOLUME --unset /var/lib/postgresql/data
|
||||
```
|
||||
|
||||
## USER
|
||||
|
||||
```dockerfile
|
||||
@@ -2766,6 +2874,7 @@ RUN echo "I'm building for $TARGETPLATFORM"
|
||||
| `BUILDKIT_BUILD_NAME` | String | Override the build name shown in [`buildx history` command](https://docs.docker.com/reference/cli/docker/buildx/history/) and [Docker Desktop Builds view](https://docs.docker.com/desktop/use-desktop/builds/). |
|
||||
| `BUILDKIT_CACHE_MOUNT_NS` | String | Set optional cache ID namespace. |
|
||||
| `BUILDKIT_CONTEXT_KEEP_GIT_DIR` | Bool | Trigger Git context to keep the `.git` directory. |
|
||||
| `BUILDKIT_DISABLE_ONBUILD` | Bool | Skip the [`ONBUILD`](#onbuild) triggers of all base images and stages. This is not a security boundary: a custom frontend selected with `# syntax=` can ignore it. |
|
||||
| `BUILDKIT_GIT_ADVICE` | Bool | Show Git advice messages from BuildKit-managed Git operations. Defaults to `false`. |
|
||||
| `BUILDKIT_GIT_DEBUG_COMMANDS` | Bool | Print executed Git commands for Git contexts and `ADD` sources in the build log. Defaults to `false`. |
|
||||
| `BUILDKIT_INLINE_CACHE`[^2] | Bool | Inline cache metadata to image config or not. |
|
||||
|
||||
@@ -148,9 +148,11 @@ func expandSliceInPlace(values []string, expander SingleWordExpander) error {
|
||||
// EnvCommand allows setting an variable in the container's environment.
|
||||
//
|
||||
// ENV key1 value1 [keyN valueN...]
|
||||
// ENV --unset key1 [keyN...]
|
||||
type EnvCommand struct {
|
||||
withNameAndCode
|
||||
Env KeyValuePairs
|
||||
Env KeyValuePairs
|
||||
Unset bool // Env keys are removed, values are empty
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Expand(expander SingleWordExpander) error {
|
||||
@@ -185,9 +187,11 @@ func NewLabelCommand(k string, v string, noExp bool) *LabelCommand {
|
||||
// LabelCommand sets an image label in the output
|
||||
//
|
||||
// LABEL some json data describing the image
|
||||
// LABEL --unset key1 [keyN...]
|
||||
type LabelCommand struct {
|
||||
withNameAndCode
|
||||
Labels KeyValuePairs
|
||||
Unset bool // Labels keys are removed, values are empty
|
||||
noExpand bool
|
||||
}
|
||||
|
||||
@@ -436,9 +440,11 @@ func (c *UserCommand) Expand(expander SingleWordExpander) error {
|
||||
// VolumeCommand exposes the specified volume for use in the build environment.
|
||||
//
|
||||
// VOLUME /foo
|
||||
// VOLUME --unset /foo
|
||||
type VolumeCommand struct {
|
||||
withNameAndCode
|
||||
Volumes []string
|
||||
Unset bool // Volumes are removed
|
||||
}
|
||||
|
||||
func (c *VolumeCommand) Expand(expander SingleWordExpander) error {
|
||||
@@ -521,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,6 +36,31 @@ var (
|
||||
parseRunPostHooks []func(*RunCommand, parseRequest) error
|
||||
)
|
||||
|
||||
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{}
|
||||
for ; node.Next != nil; node = node.Next {
|
||||
@@ -230,10 +255,48 @@ func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// addUnsetFlag adds the --unset flag if it is enabled in this build.
|
||||
func addUnsetFlag(req parseRequest) *Flag {
|
||||
if !unsetEnabled {
|
||||
return nil
|
||||
}
|
||||
return req.flags.AddBool("unset", false)
|
||||
}
|
||||
|
||||
// parseUnsetKeys returns the keys for the --unset form of a command.
|
||||
func parseUnsetKeys(args []string, cmdName string) (KeyValuePairs, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errAtLeastOneArgument(cmdName + " --unset")
|
||||
}
|
||||
var res KeyValuePairs
|
||||
for _, name := range args {
|
||||
if name == "" {
|
||||
return nil, errBlankCommandNames(cmdName)
|
||||
}
|
||||
if strings.Contains(name, "=") {
|
||||
return nil, errors.Errorf("%s --unset does not accept values: %s", cmdName, name)
|
||||
}
|
||||
res = append(res, KeyValuePair{Key: name})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseEnv(req parseRequest) (*EnvCommand, error) {
|
||||
flUnset := addUnsetFlag(req)
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flUnset != nil && flUnset.Value == "true" {
|
||||
envs, err := parseUnsetKeys(req.args, "ENV")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EnvCommand{
|
||||
Env: envs,
|
||||
Unset: true,
|
||||
withNameAndCode: newWithNameAndCode(req),
|
||||
}, nil
|
||||
}
|
||||
envs, err := parseKvps(req.args, "ENV")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -259,9 +322,21 @@ func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
|
||||
}
|
||||
|
||||
func parseLabel(req parseRequest) (*LabelCommand, error) {
|
||||
flUnset := addUnsetFlag(req)
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flUnset != nil && flUnset.Value == "true" {
|
||||
labels, err := parseUnsetKeys(req.args, "LABEL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LabelCommand{
|
||||
Labels: labels,
|
||||
Unset: true,
|
||||
withNameAndCode: newWithNameAndCode(req),
|
||||
}, nil
|
||||
}
|
||||
|
||||
labels, err := parseKvps(req.args, "LABEL")
|
||||
if err != nil {
|
||||
@@ -399,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],
|
||||
@@ -411,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) {
|
||||
@@ -714,11 +824,13 @@ func parseVolume(req parseRequest) (*VolumeCommand, error) {
|
||||
return nil, errAtLeastOneArgument("VOLUME")
|
||||
}
|
||||
|
||||
flUnset := addUnsetFlag(req)
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &VolumeCommand{
|
||||
Unset: flUnset != nil && flUnset.Value == "true",
|
||||
withNameAndCode: newWithNameAndCode(req),
|
||||
}
|
||||
|
||||
@@ -736,6 +848,9 @@ func parseStopSignal(req parseRequest) (*StopSignalCommand, error) {
|
||||
if len(req.args) != 1 {
|
||||
return nil, errExactlyOneArgument("STOPSIGNAL")
|
||||
}
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig := req.args[0]
|
||||
|
||||
cmd := &StopSignalCommand{
|
||||
@@ -750,6 +865,10 @@ func parseArg(req parseRequest) (*ArgCommand, error) {
|
||||
return nil, errAtLeastOneArgument("ARG")
|
||||
}
|
||||
|
||||
if err := req.flags.Parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pairs := make([]KeyValuePairOptional, len(req.args))
|
||||
|
||||
for i, arg := range req.args {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build dfreset
|
||||
|
||||
package instructions
|
||||
|
||||
func init() {
|
||||
resetEnabled = true
|
||||
}
|
||||
@@ -230,6 +230,16 @@ func TestErrorCases(t *testing.T) {
|
||||
dockerfile: "MAINTAINER --boo joe@example.com",
|
||||
expectedError: "unknown flag: --boo",
|
||||
},
|
||||
{
|
||||
name: "ARG unknown flag",
|
||||
dockerfile: "ARG --boo foo",
|
||||
expectedError: "unknown flag: --boo",
|
||||
},
|
||||
{
|
||||
name: "STOPSIGNAL unknown flag",
|
||||
dockerfile: "STOPSIGNAL --boo SIGTERM",
|
||||
expectedError: "unknown flag: --boo",
|
||||
},
|
||||
{
|
||||
name: "Chaining ONBUILD",
|
||||
dockerfile: `ONBUILD ONBUILD RUN touch foobar`,
|
||||
@@ -259,6 +269,33 @@ func TestErrorCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetDisabled(t *testing.T) {
|
||||
if unsetEnabled {
|
||||
t.Skip("--unset is enabled in this build")
|
||||
}
|
||||
for _, dockerfile := range []string{
|
||||
"ENV --unset FOO",
|
||||
"LABEL --unset foo",
|
||||
"ARG --unset FOO",
|
||||
"VOLUME --unset /foo",
|
||||
} {
|
||||
ast, err := parser.Parse(strings.NewReader(dockerfile))
|
||||
require.NoError(t, err)
|
||||
_, err = ParseInstruction(ast.AST.Children[0])
|
||||
require.ErrorContains(t, err, "unknown flag: --unset", dockerfile)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build dfunset
|
||||
|
||||
package instructions
|
||||
|
||||
func init() {
|
||||
unsetEnabled = true
|
||||
}
|
||||
@@ -137,6 +137,13 @@ var (
|
||||
heredocCompoundDirectives = map[string]bool{
|
||||
command.Onbuild: true,
|
||||
}
|
||||
|
||||
// Directives that parse key-value pairs but take a list of names
|
||||
// when --unset flag is set
|
||||
unsetCommands = map[string]bool{
|
||||
command.Env: true,
|
||||
command.Label: true,
|
||||
}
|
||||
)
|
||||
|
||||
// directives is the structure used during a build run to hold the state of
|
||||
@@ -234,6 +241,10 @@ func newNodeFromLine(line string, d *directives, comments []string) (*Node, erro
|
||||
if fn == nil {
|
||||
fn = parseIgnore
|
||||
}
|
||||
// --unset form takes a list of names instead of key-value pairs
|
||||
if unsetCommands[strings.ToLower(cmd)] && hasUnsetFlag(flags) {
|
||||
fn = parseNameOrNameVal
|
||||
}
|
||||
next, attrs, err := fn(args, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -249,6 +260,17 @@ func newNodeFromLine(line string, d *directives, comments []string) (*Node, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
func hasUnsetFlag(flags []string) bool {
|
||||
for _, f := range flags {
|
||||
name, value, _ := strings.Cut(f, "=")
|
||||
if name == "--unset" {
|
||||
// invalid values are reported by the flag parser
|
||||
return !strings.EqualFold(value, "false")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Result contains the bundled outputs from parsing a Dockerfile.
|
||||
type Result struct {
|
||||
AST *Node
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM busybox
|
||||
ENV --unset FOO BAR
|
||||
ENV --unset=true FOO
|
||||
ENV --unset=false FOO=bar
|
||||
LABEL --unset foo bar.baz
|
||||
LABEL --unset=false foo=bar
|
||||
VOLUME --unset /foo /bar
|
||||
VOLUME --unset ["/foo", "/bar"]
|
||||
ONBUILD ENV --unset FOO
|
||||
@@ -0,0 +1,9 @@
|
||||
(from "busybox")
|
||||
(env ["--unset"] "FOO" "BAR")
|
||||
(env ["--unset=true"] "FOO")
|
||||
(env ["--unset=false"] "FOO" "bar" "=")
|
||||
(label ["--unset"] "foo" "bar.baz")
|
||||
(label ["--unset=false"] "foo" "bar" "=")
|
||||
(volume ["--unset"] "/foo" "/bar")
|
||||
(volume ["--unset"] "/foo" "/bar")
|
||||
(onbuild (env ["--unset"] "FOO"))
|
||||
@@ -0,0 +1 @@
|
||||
dfunset dfreset
|
||||
Reference in new issue
Block a user