github.com/rumpl/bof@v23.0.0-rc.2+incompatible/builder/dockerfile/dispatchers_test.go (about)

     1  package dockerfile // import "github.com/docker/docker/builder/dockerfile"
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"runtime"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/backend"
    13  	"github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/api/types/strslice"
    15  	"github.com/docker/docker/builder"
    16  	"github.com/docker/docker/image"
    17  	"github.com/docker/docker/pkg/system"
    18  	"github.com/docker/go-connections/nat"
    19  	"github.com/moby/buildkit/frontend/dockerfile/instructions"
    20  	"github.com/moby/buildkit/frontend/dockerfile/parser"
    21  	"github.com/moby/buildkit/frontend/dockerfile/shell"
    22  	"gotest.tools/v3/assert"
    23  	is "gotest.tools/v3/assert/cmp"
    24  )
    25  
    26  func newBuilderWithMockBackend() *Builder {
    27  	mockBackend := &MockBackend{}
    28  	opts := &types.ImageBuildOptions{}
    29  	ctx := context.Background()
    30  	b := &Builder{
    31  		options:       opts,
    32  		docker:        mockBackend,
    33  		Stdout:        new(bytes.Buffer),
    34  		clientCtx:     ctx,
    35  		disableCommit: true,
    36  		imageSources: newImageSources(ctx, builderOptions{
    37  			Options: opts,
    38  			Backend: mockBackend,
    39  		}),
    40  		imageProber:      newImageProber(mockBackend, nil, false),
    41  		containerManager: newContainerManager(mockBackend),
    42  	}
    43  	return b
    44  }
    45  
    46  func TestEnv2Variables(t *testing.T) {
    47  	b := newBuilderWithMockBackend()
    48  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
    49  	envCommand := &instructions.EnvCommand{
    50  		Env: instructions.KeyValuePairs{
    51  			instructions.KeyValuePair{Key: "var1", Value: "val1"},
    52  			instructions.KeyValuePair{Key: "var2", Value: "val2"},
    53  		},
    54  	}
    55  	err := dispatch(sb, envCommand)
    56  	assert.NilError(t, err)
    57  
    58  	expected := []string{
    59  		"var1=val1",
    60  		"var2=val2",
    61  	}
    62  	assert.Check(t, is.DeepEqual(expected, sb.state.runConfig.Env))
    63  }
    64  
    65  func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
    66  	b := newBuilderWithMockBackend()
    67  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
    68  	sb.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
    69  	envCommand := &instructions.EnvCommand{
    70  		Env: instructions.KeyValuePairs{
    71  			instructions.KeyValuePair{Key: "var1", Value: "val1"},
    72  		},
    73  	}
    74  	err := dispatch(sb, envCommand)
    75  	assert.NilError(t, err)
    76  	expected := []string{
    77  		"var1=val1",
    78  		"var2=fromenv",
    79  	}
    80  	assert.Check(t, is.DeepEqual(expected, sb.state.runConfig.Env))
    81  }
    82  
    83  func TestMaintainer(t *testing.T) {
    84  	maintainerEntry := "Some Maintainer <maintainer@example.com>"
    85  	b := newBuilderWithMockBackend()
    86  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
    87  	cmd := &instructions.MaintainerCommand{Maintainer: maintainerEntry}
    88  	err := dispatch(sb, cmd)
    89  	assert.NilError(t, err)
    90  	assert.Check(t, is.Equal(maintainerEntry, sb.state.maintainer))
    91  }
    92  
    93  func TestLabel(t *testing.T) {
    94  	labelName := "label"
    95  	labelValue := "value"
    96  
    97  	b := newBuilderWithMockBackend()
    98  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
    99  	cmd := &instructions.LabelCommand{
   100  		Labels: instructions.KeyValuePairs{
   101  			instructions.KeyValuePair{Key: labelName, Value: labelValue},
   102  		},
   103  	}
   104  	err := dispatch(sb, cmd)
   105  	assert.NilError(t, err)
   106  
   107  	assert.Assert(t, is.Contains(sb.state.runConfig.Labels, labelName))
   108  	assert.Check(t, is.Equal(sb.state.runConfig.Labels[labelName], labelValue))
   109  }
   110  
   111  func TestFromScratch(t *testing.T) {
   112  	b := newBuilderWithMockBackend()
   113  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   114  	cmd := &instructions.Stage{
   115  		BaseName: "scratch",
   116  	}
   117  	err := initializeStage(sb, cmd)
   118  
   119  	if runtime.GOOS == "windows" {
   120  		assert.Check(t, is.Error(err, "Windows does not support FROM scratch"))
   121  		return
   122  	}
   123  
   124  	assert.NilError(t, err)
   125  	assert.Check(t, sb.state.hasFromImage())
   126  	assert.Check(t, is.Equal("", sb.state.imageID))
   127  	expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS)
   128  	assert.Check(t, is.DeepEqual([]string{expected}, sb.state.runConfig.Env))
   129  }
   130  
   131  func TestFromWithArg(t *testing.T) {
   132  	tag, expected := ":sometag", "expectedthisid"
   133  
   134  	getImage := func(name string) (builder.Image, builder.ROLayer, error) {
   135  		assert.Check(t, is.Equal("alpine"+tag, name))
   136  		return &mockImage{id: "expectedthisid"}, nil, nil
   137  	}
   138  	b := newBuilderWithMockBackend()
   139  	b.docker.(*MockBackend).getImageFunc = getImage
   140  	args := NewBuildArgs(make(map[string]*string))
   141  
   142  	val := "sometag"
   143  	metaArg := instructions.ArgCommand{Args: []instructions.KeyValuePairOptional{{
   144  		Key:   "THETAG",
   145  		Value: &val,
   146  	}}}
   147  	cmd := &instructions.Stage{
   148  		BaseName: "alpine:${THETAG}",
   149  	}
   150  	err := processMetaArg(metaArg, shell.NewLex('\\'), args)
   151  
   152  	sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults())
   153  	assert.NilError(t, err)
   154  	err = initializeStage(sb, cmd)
   155  	assert.NilError(t, err)
   156  
   157  	assert.Check(t, is.Equal(expected, sb.state.imageID))
   158  	assert.Check(t, is.Equal(expected, sb.state.baseImage.ImageID()))
   159  	assert.Check(t, is.Len(sb.state.buildArgs.GetAllAllowed(), 0))
   160  	assert.Check(t, is.Len(sb.state.buildArgs.GetAllMeta(), 1))
   161  }
   162  
   163  func TestFromWithArgButBuildArgsNotGiven(t *testing.T) {
   164  	b := newBuilderWithMockBackend()
   165  	args := NewBuildArgs(make(map[string]*string))
   166  
   167  	metaArg := instructions.ArgCommand{}
   168  	cmd := &instructions.Stage{
   169  		BaseName: "${THETAG}",
   170  	}
   171  	err := processMetaArg(metaArg, shell.NewLex('\\'), args)
   172  
   173  	sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults())
   174  	assert.NilError(t, err)
   175  	err = initializeStage(sb, cmd)
   176  	assert.Error(t, err, "base name (${THETAG}) should not be blank")
   177  }
   178  
   179  func TestFromWithUndefinedArg(t *testing.T) {
   180  	tag, expected := "sometag", "expectedthisid"
   181  
   182  	getImage := func(name string) (builder.Image, builder.ROLayer, error) {
   183  		assert.Check(t, is.Equal("alpine", name))
   184  		return &mockImage{id: "expectedthisid"}, nil, nil
   185  	}
   186  	b := newBuilderWithMockBackend()
   187  	b.docker.(*MockBackend).getImageFunc = getImage
   188  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   189  
   190  	b.options.BuildArgs = map[string]*string{"THETAG": &tag}
   191  
   192  	cmd := &instructions.Stage{
   193  		BaseName: "alpine${THETAG}",
   194  	}
   195  	err := initializeStage(sb, cmd)
   196  	assert.NilError(t, err)
   197  	assert.Check(t, is.Equal(expected, sb.state.imageID))
   198  }
   199  
   200  func TestFromMultiStageWithNamedStage(t *testing.T) {
   201  	b := newBuilderWithMockBackend()
   202  	firstFrom := &instructions.Stage{BaseName: "someimg", Name: "base"}
   203  	secondFrom := &instructions.Stage{BaseName: "base"}
   204  	previousResults := newStagesBuildResults()
   205  	firstSB := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), previousResults)
   206  	secondSB := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), previousResults)
   207  	err := initializeStage(firstSB, firstFrom)
   208  	assert.NilError(t, err)
   209  	assert.Check(t, firstSB.state.hasFromImage())
   210  	previousResults.indexed["base"] = firstSB.state.runConfig
   211  	previousResults.flat = append(previousResults.flat, firstSB.state.runConfig)
   212  	err = initializeStage(secondSB, secondFrom)
   213  	assert.NilError(t, err)
   214  	assert.Check(t, secondSB.state.hasFromImage())
   215  }
   216  
   217  func TestOnbuild(t *testing.T) {
   218  	b := newBuilderWithMockBackend()
   219  	sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   220  	cmd := &instructions.OnbuildCommand{
   221  		Expression: "ADD . /app/src",
   222  	}
   223  	err := dispatch(sb, cmd)
   224  	assert.NilError(t, err)
   225  	assert.Check(t, is.Equal("ADD . /app/src", sb.state.runConfig.OnBuild[0]))
   226  }
   227  
   228  func TestWorkdir(t *testing.T) {
   229  	b := newBuilderWithMockBackend()
   230  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   231  	sb.state.baseImage = &mockImage{}
   232  	workingDir := "/app"
   233  	if runtime.GOOS == "windows" {
   234  		workingDir = "C:\\app"
   235  	}
   236  	cmd := &instructions.WorkdirCommand{
   237  		Path: workingDir,
   238  	}
   239  
   240  	err := dispatch(sb, cmd)
   241  	assert.NilError(t, err)
   242  	assert.Check(t, is.Equal(workingDir, sb.state.runConfig.WorkingDir))
   243  }
   244  
   245  func TestCmd(t *testing.T) {
   246  	b := newBuilderWithMockBackend()
   247  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   248  	sb.state.baseImage = &mockImage{}
   249  	command := "./executable"
   250  
   251  	cmd := &instructions.CmdCommand{
   252  		ShellDependantCmdLine: instructions.ShellDependantCmdLine{
   253  			CmdLine:      strslice.StrSlice{command},
   254  			PrependShell: true,
   255  		},
   256  	}
   257  	err := dispatch(sb, cmd)
   258  	assert.NilError(t, err)
   259  
   260  	var expectedCommand strslice.StrSlice
   261  	if runtime.GOOS == "windows" {
   262  		expectedCommand = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", command))
   263  	} else {
   264  		expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
   265  	}
   266  
   267  	assert.Check(t, is.DeepEqual(expectedCommand, sb.state.runConfig.Cmd))
   268  	assert.Check(t, sb.state.cmdSet)
   269  }
   270  
   271  func TestHealthcheckNone(t *testing.T) {
   272  	b := newBuilderWithMockBackend()
   273  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   274  	cmd := &instructions.HealthCheckCommand{
   275  		Health: &container.HealthConfig{
   276  			Test: []string{"NONE"},
   277  		},
   278  	}
   279  	err := dispatch(sb, cmd)
   280  	assert.NilError(t, err)
   281  
   282  	assert.Assert(t, sb.state.runConfig.Healthcheck != nil)
   283  	assert.Check(t, is.DeepEqual([]string{"NONE"}, sb.state.runConfig.Healthcheck.Test))
   284  }
   285  
   286  func TestHealthcheckCmd(t *testing.T) {
   287  	b := newBuilderWithMockBackend()
   288  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   289  	expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
   290  	cmd := &instructions.HealthCheckCommand{
   291  		Health: &container.HealthConfig{
   292  			Test: expectedTest,
   293  		},
   294  	}
   295  	err := dispatch(sb, cmd)
   296  	assert.NilError(t, err)
   297  
   298  	assert.Assert(t, sb.state.runConfig.Healthcheck != nil)
   299  	assert.Check(t, is.DeepEqual(expectedTest, sb.state.runConfig.Healthcheck.Test))
   300  }
   301  
   302  func TestEntrypoint(t *testing.T) {
   303  	b := newBuilderWithMockBackend()
   304  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   305  	sb.state.baseImage = &mockImage{}
   306  	entrypointCmd := "/usr/sbin/nginx"
   307  
   308  	cmd := &instructions.EntrypointCommand{
   309  		ShellDependantCmdLine: instructions.ShellDependantCmdLine{
   310  			CmdLine:      strslice.StrSlice{entrypointCmd},
   311  			PrependShell: true,
   312  		},
   313  	}
   314  	err := dispatch(sb, cmd)
   315  	assert.NilError(t, err)
   316  	assert.Assert(t, sb.state.runConfig.Entrypoint != nil)
   317  
   318  	var expectedEntrypoint strslice.StrSlice
   319  	if runtime.GOOS == "windows" {
   320  		expectedEntrypoint = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", entrypointCmd))
   321  	} else {
   322  		expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd))
   323  	}
   324  	assert.Check(t, is.DeepEqual(expectedEntrypoint, sb.state.runConfig.Entrypoint))
   325  }
   326  
   327  func TestExpose(t *testing.T) {
   328  	b := newBuilderWithMockBackend()
   329  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   330  
   331  	exposedPort := "80"
   332  	cmd := &instructions.ExposeCommand{
   333  		Ports: []string{exposedPort},
   334  	}
   335  	err := dispatch(sb, cmd)
   336  	assert.NilError(t, err)
   337  
   338  	assert.Assert(t, sb.state.runConfig.ExposedPorts != nil)
   339  	assert.Assert(t, is.Len(sb.state.runConfig.ExposedPorts, 1))
   340  
   341  	portsMapping, err := nat.ParsePortSpec(exposedPort)
   342  	assert.NilError(t, err)
   343  	assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, portsMapping[0].Port))
   344  }
   345  
   346  func TestUser(t *testing.T) {
   347  	b := newBuilderWithMockBackend()
   348  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   349  
   350  	cmd := &instructions.UserCommand{
   351  		User: "test",
   352  	}
   353  	err := dispatch(sb, cmd)
   354  	assert.NilError(t, err)
   355  	assert.Check(t, is.Equal("test", sb.state.runConfig.User))
   356  }
   357  
   358  func TestVolume(t *testing.T) {
   359  	b := newBuilderWithMockBackend()
   360  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   361  
   362  	exposedVolume := "/foo"
   363  
   364  	cmd := &instructions.VolumeCommand{
   365  		Volumes: []string{exposedVolume},
   366  	}
   367  	err := dispatch(sb, cmd)
   368  	assert.NilError(t, err)
   369  	assert.Assert(t, sb.state.runConfig.Volumes != nil)
   370  	assert.Check(t, is.Len(sb.state.runConfig.Volumes, 1))
   371  	assert.Check(t, is.Contains(sb.state.runConfig.Volumes, exposedVolume))
   372  }
   373  
   374  func TestStopSignal(t *testing.T) {
   375  	if runtime.GOOS == "windows" {
   376  		t.Skip("Windows does not support stopsignal")
   377  		return
   378  	}
   379  	b := newBuilderWithMockBackend()
   380  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   381  	sb.state.baseImage = &mockImage{}
   382  	signal := "SIGKILL"
   383  
   384  	cmd := &instructions.StopSignalCommand{
   385  		Signal: signal,
   386  	}
   387  	err := dispatch(sb, cmd)
   388  	assert.NilError(t, err)
   389  	assert.Check(t, is.Equal(signal, sb.state.runConfig.StopSignal))
   390  }
   391  
   392  func TestArg(t *testing.T) {
   393  	b := newBuilderWithMockBackend()
   394  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   395  
   396  	argName := "foo"
   397  	argVal := "bar"
   398  	cmd := &instructions.ArgCommand{Args: []instructions.KeyValuePairOptional{{Key: argName, Value: &argVal}}}
   399  	err := dispatch(sb, cmd)
   400  	assert.NilError(t, err)
   401  
   402  	expected := map[string]string{argName: argVal}
   403  	assert.Check(t, is.DeepEqual(expected, sb.state.buildArgs.GetAllAllowed()))
   404  }
   405  
   406  func TestShell(t *testing.T) {
   407  	b := newBuilderWithMockBackend()
   408  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   409  
   410  	shellCmd := "powershell"
   411  	cmd := &instructions.ShellCommand{Shell: strslice.StrSlice{shellCmd}}
   412  
   413  	err := dispatch(sb, cmd)
   414  	assert.NilError(t, err)
   415  
   416  	expectedShell := strslice.StrSlice([]string{shellCmd})
   417  	assert.Check(t, is.DeepEqual(expectedShell, sb.state.runConfig.Shell))
   418  }
   419  
   420  func TestPrependEnvOnCmd(t *testing.T) {
   421  	buildArgs := NewBuildArgs(nil)
   422  	buildArgs.AddArg("NO_PROXY", nil)
   423  
   424  	args := []string{"sorted=nope", "args=not", "http_proxy=foo", "NO_PROXY=YA"}
   425  	cmd := []string{"foo", "bar"}
   426  	cmdWithEnv := prependEnvOnCmd(buildArgs, args, cmd)
   427  	expected := strslice.StrSlice([]string{
   428  		"|3", "NO_PROXY=YA", "args=not", "sorted=nope", "foo", "bar"})
   429  	assert.Check(t, is.DeepEqual(expected, cmdWithEnv))
   430  }
   431  
   432  func TestRunWithBuildArgs(t *testing.T) {
   433  	b := newBuilderWithMockBackend()
   434  	args := NewBuildArgs(make(map[string]*string))
   435  	args.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
   436  	b.disableCommit = false
   437  	sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults())
   438  
   439  	runConfig := &container.Config{}
   440  	origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
   441  
   442  	var cmdWithShell strslice.StrSlice
   443  	if runtime.GOOS == "windows" {
   444  		cmdWithShell = strslice.StrSlice([]string{strings.Join(append(getShell(runConfig, runtime.GOOS), []string{"echo foo"}...), " ")})
   445  	} else {
   446  		cmdWithShell = strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo"))
   447  	}
   448  
   449  	envVars := []string{"|1", "one=two"}
   450  	cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...))
   451  
   452  	imageCache := &mockImageCache{
   453  		getCacheFunc: func(parentID string, cfg *container.Config) (string, error) {
   454  			// Check the runConfig.Cmd sent to probeCache()
   455  			assert.Check(t, is.DeepEqual(cachedCmd, cfg.Cmd))
   456  			assert.Check(t, is.DeepEqual(strslice.StrSlice(nil), cfg.Entrypoint))
   457  			return "", nil
   458  		},
   459  	}
   460  
   461  	mockBackend := b.docker.(*MockBackend)
   462  	mockBackend.makeImageCacheFunc = func(_ []string) builder.ImageCache {
   463  		return imageCache
   464  	}
   465  	b.imageProber = newImageProber(mockBackend, nil, false)
   466  	mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ROLayer, error) {
   467  		return &mockImage{
   468  			id:     "abcdef",
   469  			config: &container.Config{Cmd: origCmd},
   470  		}, nil, nil
   471  	}
   472  	mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.CreateResponse, error) {
   473  		// Check the runConfig.Cmd sent to create()
   474  		assert.Check(t, is.DeepEqual(cmdWithShell, config.Config.Cmd))
   475  		assert.Check(t, is.Contains(config.Config.Env, "one=two"))
   476  		assert.Check(t, is.DeepEqual(strslice.StrSlice{""}, config.Config.Entrypoint))
   477  		return container.CreateResponse{ID: "12345"}, nil
   478  	}
   479  	mockBackend.commitFunc = func(cfg backend.CommitConfig) (image.ID, error) {
   480  		// Check the runConfig.Cmd sent to commit()
   481  		assert.Check(t, is.DeepEqual(origCmd, cfg.Config.Cmd))
   482  		assert.Check(t, is.DeepEqual(cachedCmd, cfg.ContainerConfig.Cmd))
   483  		assert.Check(t, is.DeepEqual(strslice.StrSlice(nil), cfg.Config.Entrypoint))
   484  		return "", nil
   485  	}
   486  	from := &instructions.Stage{BaseName: "abcdef"}
   487  	err := initializeStage(sb, from)
   488  	assert.NilError(t, err)
   489  	sb.state.buildArgs.AddArg("one", strPtr("two"))
   490  
   491  	// This is hugely annoying. On the Windows side, it relies on the
   492  	// RunCommand being able to emit String() and Name() (as implemented by
   493  	// withNameAndCode). Unfortunately, that is internal, and no way to directly
   494  	// set. However, we can fortunately use ParseInstruction in the instructions
   495  	// package to parse a fake node which can be used as our instructions.RunCommand
   496  	// instead.
   497  	node := &parser.Node{
   498  		Original: `RUN echo foo`,
   499  		Value:    "run",
   500  	}
   501  	runint, err := instructions.ParseInstruction(node)
   502  	assert.NilError(t, err)
   503  	runinst := runint.(*instructions.RunCommand)
   504  	runinst.CmdLine = strslice.StrSlice{"echo foo"}
   505  	runinst.PrependShell = true
   506  
   507  	assert.NilError(t, dispatch(sb, runinst))
   508  
   509  	// Check that runConfig.Cmd has not been modified by run
   510  	assert.Check(t, is.DeepEqual(origCmd, sb.state.runConfig.Cmd))
   511  }
   512  
   513  func TestRunIgnoresHealthcheck(t *testing.T) {
   514  	b := newBuilderWithMockBackend()
   515  	args := NewBuildArgs(make(map[string]*string))
   516  	sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults())
   517  	b.disableCommit = false
   518  
   519  	origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
   520  
   521  	imageCache := &mockImageCache{
   522  		getCacheFunc: func(parentID string, cfg *container.Config) (string, error) {
   523  			return "", nil
   524  		},
   525  	}
   526  
   527  	mockBackend := b.docker.(*MockBackend)
   528  	mockBackend.makeImageCacheFunc = func(_ []string) builder.ImageCache {
   529  		return imageCache
   530  	}
   531  	b.imageProber = newImageProber(mockBackend, nil, false)
   532  	mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ROLayer, error) {
   533  		return &mockImage{
   534  			id:     "abcdef",
   535  			config: &container.Config{Cmd: origCmd},
   536  		}, nil, nil
   537  	}
   538  	mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.CreateResponse, error) {
   539  		return container.CreateResponse{ID: "12345"}, nil
   540  	}
   541  	mockBackend.commitFunc = func(cfg backend.CommitConfig) (image.ID, error) {
   542  		return "", nil
   543  	}
   544  	from := &instructions.Stage{BaseName: "abcdef"}
   545  	err := initializeStage(sb, from)
   546  	assert.NilError(t, err)
   547  
   548  	expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
   549  	healthint, err := instructions.ParseInstruction(&parser.Node{
   550  		Original: `HEALTHCHECK CMD curl -f http://localhost/ || exit 1`,
   551  		Value:    "healthcheck",
   552  		Next: &parser.Node{
   553  			Value: "cmd",
   554  			Next: &parser.Node{
   555  				Value: `curl -f http://localhost/ || exit 1`,
   556  			},
   557  		},
   558  	})
   559  	assert.NilError(t, err)
   560  	cmd := healthint.(*instructions.HealthCheckCommand)
   561  
   562  	assert.NilError(t, dispatch(sb, cmd))
   563  	assert.Assert(t, sb.state.runConfig.Healthcheck != nil)
   564  
   565  	mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.CreateResponse, error) {
   566  		// Check the Healthcheck is disabled.
   567  		assert.Check(t, is.DeepEqual([]string{"NONE"}, config.Config.Healthcheck.Test))
   568  		return container.CreateResponse{ID: "123456"}, nil
   569  	}
   570  
   571  	sb.state.buildArgs.AddArg("one", strPtr("two"))
   572  	runint, err := instructions.ParseInstruction(&parser.Node{Original: `RUN echo foo`, Value: "run"})
   573  	assert.NilError(t, err)
   574  	run := runint.(*instructions.RunCommand)
   575  	run.PrependShell = true
   576  
   577  	assert.NilError(t, dispatch(sb, run))
   578  	assert.Check(t, is.DeepEqual(expectedTest, sb.state.runConfig.Healthcheck.Test))
   579  }
   580  
   581  func TestDispatchUnsupportedOptions(t *testing.T) {
   582  	b := newBuilderWithMockBackend()
   583  	sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults())
   584  	sb.state.baseImage = &mockImage{}
   585  	sb.state.operatingSystem = runtime.GOOS
   586  
   587  	t.Run("ADD with chmod", func(t *testing.T) {
   588  		cmd := &instructions.AddCommand{
   589  			SourcesAndDest: instructions.SourcesAndDest{
   590  				SourcePaths: []string{"."},
   591  				DestPath:    ".",
   592  			},
   593  			Chmod: "0655",
   594  		}
   595  		err := dispatch(sb, cmd)
   596  		assert.Error(t, err, "the --chmod option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled")
   597  	})
   598  
   599  	t.Run("COPY with chmod", func(t *testing.T) {
   600  		cmd := &instructions.CopyCommand{
   601  			SourcesAndDest: instructions.SourcesAndDest{
   602  				SourcePaths: []string{"."},
   603  				DestPath:    ".",
   604  			},
   605  			Chmod: "0655",
   606  		}
   607  		err := dispatch(sb, cmd)
   608  		assert.Error(t, err, "the --chmod option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled")
   609  	})
   610  
   611  	t.Run("RUN with unsupported options", func(t *testing.T) {
   612  		runint, err := instructions.ParseInstruction(&parser.Node{Original: `RUN echo foo`, Value: "run"})
   613  		assert.NilError(t, err)
   614  		cmd := runint.(*instructions.RunCommand)
   615  
   616  		// classic builder "RUN" currently doesn't support any flags, but testing
   617  		// both "known" flags and "bogus" flags for completeness, and in case
   618  		// one or more of these flags will be supported in future
   619  		for _, f := range []string{"mount", "network", "security", "any-flag"} {
   620  			cmd.FlagsUsed = []string{f}
   621  			err := dispatch(sb, cmd)
   622  			assert.Error(t, err, fmt.Sprintf("the --%s option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled", f))
   623  		}
   624  	})
   625  }