| package dockerfile |
| |
| import ( |
| "fmt" |
| "runtime" |
| "testing" |
| |
| "bytes" |
| "context" |
| |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/api/types/backend" |
| "github.com/docker/docker/api/types/container" |
| "github.com/docker/docker/api/types/strslice" |
| "github.com/docker/docker/builder" |
| "github.com/docker/docker/builder/dockerfile/parser" |
| "github.com/docker/docker/pkg/system" |
| "github.com/docker/docker/pkg/testutil" |
| "github.com/docker/go-connections/nat" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| ) |
| |
| type commandWithFunction struct { |
| name string |
| function func(args []string) error |
| } |
| |
| func withArgs(f dispatcher) func([]string) error { |
| return func(args []string) error { |
| return f(dispatchRequest{args: args}) |
| } |
| } |
| |
| func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error { |
| return func(args []string) error { |
| return f(defaultDispatchReq(builder, args...)) |
| } |
| } |
| |
| func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest { |
| return dispatchRequest{ |
| builder: builder, |
| args: args, |
| flags: NewBFlags(), |
| shlex: NewShellLex(parser.DefaultEscapeToken), |
| state: &dispatchState{runConfig: &container.Config{}}, |
| } |
| } |
| |
| func newBuilderWithMockBackend() *Builder { |
| mockBackend := &MockBackend{} |
| ctx := context.Background() |
| b := &Builder{ |
| options: &types.ImageBuildOptions{}, |
| docker: mockBackend, |
| buildArgs: newBuildArgs(make(map[string]*string)), |
| Stdout: new(bytes.Buffer), |
| clientCtx: ctx, |
| disableCommit: true, |
| imageSources: newImageSources(ctx, builderOptions{ |
| Options: &types.ImageBuildOptions{}, |
| Backend: mockBackend, |
| }), |
| buildStages: newBuildStages(), |
| imageProber: newImageProber(mockBackend, nil, runtime.GOOS, false), |
| containerManager: newContainerManager(mockBackend), |
| } |
| return b |
| } |
| |
| func TestCommandsExactlyOneArgument(t *testing.T) { |
| commands := []commandWithFunction{ |
| {"MAINTAINER", withArgs(maintainer)}, |
| {"WORKDIR", withArgs(workdir)}, |
| {"USER", withArgs(user)}, |
| {"STOPSIGNAL", withArgs(stopSignal)}, |
| } |
| |
| for _, command := range commands { |
| err := command.function([]string{}) |
| assert.EqualError(t, err, errExactlyOneArgument(command.name).Error()) |
| } |
| } |
| |
| func TestCommandsAtLeastOneArgument(t *testing.T) { |
| commands := []commandWithFunction{ |
| {"ENV", withArgs(env)}, |
| {"LABEL", withArgs(label)}, |
| {"ONBUILD", withArgs(onbuild)}, |
| {"HEALTHCHECK", withArgs(healthcheck)}, |
| {"EXPOSE", withArgs(expose)}, |
| {"VOLUME", withArgs(volume)}, |
| } |
| |
| for _, command := range commands { |
| err := command.function([]string{}) |
| assert.EqualError(t, err, errAtLeastOneArgument(command.name).Error()) |
| } |
| } |
| |
| func TestCommandsAtLeastTwoArguments(t *testing.T) { |
| commands := []commandWithFunction{ |
| {"ADD", withArgs(add)}, |
| {"COPY", withArgs(dispatchCopy)}} |
| |
| for _, command := range commands { |
| err := command.function([]string{"arg1"}) |
| assert.EqualError(t, err, errAtLeastTwoArguments(command.name).Error()) |
| } |
| } |
| |
| func TestCommandsTooManyArguments(t *testing.T) { |
| commands := []commandWithFunction{ |
| {"ENV", withArgs(env)}, |
| {"LABEL", withArgs(label)}} |
| |
| for _, command := range commands { |
| err := command.function([]string{"arg1", "arg2", "arg3"}) |
| assert.EqualError(t, err, errTooManyArguments(command.name).Error()) |
| } |
| } |
| |
| func TestCommandsBlankNames(t *testing.T) { |
| builder := newBuilderWithMockBackend() |
| commands := []commandWithFunction{ |
| {"ENV", withBuilderAndArgs(builder, env)}, |
| {"LABEL", withBuilderAndArgs(builder, label)}, |
| } |
| |
| for _, command := range commands { |
| err := command.function([]string{"", ""}) |
| assert.EqualError(t, err, errBlankCommandNames(command.name).Error()) |
| } |
| } |
| |
| func TestEnv2Variables(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| args := []string{"var1", "val1", "var2", "val2"} |
| req := defaultDispatchReq(b, args...) |
| err := env(req) |
| require.NoError(t, err) |
| |
| expected := []string{ |
| fmt.Sprintf("%s=%s", args[0], args[1]), |
| fmt.Sprintf("%s=%s", args[2], args[3]), |
| } |
| assert.Equal(t, expected, req.state.runConfig.Env) |
| } |
| |
| func TestEnvValueWithExistingRunConfigEnv(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| args := []string{"var1", "val1"} |
| req := defaultDispatchReq(b, args...) |
| req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"} |
| err := env(req) |
| require.NoError(t, err) |
| |
| expected := []string{ |
| fmt.Sprintf("%s=%s", args[0], args[1]), |
| "var2=fromenv", |
| } |
| assert.Equal(t, expected, req.state.runConfig.Env) |
| } |
| |
| func TestMaintainer(t *testing.T) { |
| maintainerEntry := "Some Maintainer <maintainer@example.com>" |
| |
| b := newBuilderWithMockBackend() |
| req := defaultDispatchReq(b, maintainerEntry) |
| err := maintainer(req) |
| require.NoError(t, err) |
| assert.Equal(t, maintainerEntry, req.state.maintainer) |
| } |
| |
| func TestLabel(t *testing.T) { |
| labelName := "label" |
| labelValue := "value" |
| |
| labelEntry := []string{labelName, labelValue} |
| b := newBuilderWithMockBackend() |
| req := defaultDispatchReq(b, labelEntry...) |
| err := label(req) |
| require.NoError(t, err) |
| |
| require.Contains(t, req.state.runConfig.Labels, labelName) |
| assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue) |
| } |
| |
| func TestFromScratch(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| req := defaultDispatchReq(b, "scratch") |
| err := from(req) |
| |
| if runtime.GOOS == "windows" && !system.LCOWSupported() { |
| assert.EqualError(t, err, "Windows does not support FROM scratch") |
| return |
| } |
| |
| require.NoError(t, err) |
| assert.True(t, req.state.hasFromImage()) |
| assert.Equal(t, "", req.state.imageID) |
| // Windows does not set the default path. TODO @jhowardmsft LCOW support. This will need revisiting as we get further into the implementation |
| expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS) |
| if runtime.GOOS == "windows" { |
| expected = "" |
| } |
| assert.Equal(t, []string{expected}, req.state.runConfig.Env) |
| } |
| |
| func TestFromWithArg(t *testing.T) { |
| tag, expected := ":sometag", "expectedthisid" |
| |
| getImage := func(name string) (builder.Image, builder.ReleaseableLayer, error) { |
| assert.Equal(t, "alpine"+tag, name) |
| return &mockImage{id: "expectedthisid"}, nil, nil |
| } |
| b := newBuilderWithMockBackend() |
| b.docker.(*MockBackend).getImageFunc = getImage |
| |
| require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag))) |
| req := defaultDispatchReq(b, "alpine${THETAG}") |
| err := from(req) |
| |
| require.NoError(t, err) |
| assert.Equal(t, expected, req.state.imageID) |
| assert.Equal(t, expected, req.state.baseImage.ImageID()) |
| assert.Len(t, b.buildArgs.GetAllAllowed(), 0) |
| assert.Len(t, b.buildArgs.GetAllMeta(), 1) |
| } |
| |
| func TestFromWithUndefinedArg(t *testing.T) { |
| tag, expected := "sometag", "expectedthisid" |
| |
| getImage := func(name string) (builder.Image, builder.ReleaseableLayer, error) { |
| assert.Equal(t, "alpine", name) |
| return &mockImage{id: "expectedthisid"}, nil, nil |
| } |
| b := newBuilderWithMockBackend() |
| b.docker.(*MockBackend).getImageFunc = getImage |
| b.options.BuildArgs = map[string]*string{"THETAG": &tag} |
| |
| req := defaultDispatchReq(b, "alpine${THETAG}") |
| err := from(req) |
| require.NoError(t, err) |
| assert.Equal(t, expected, req.state.imageID) |
| } |
| |
| func TestFromMultiStageWithScratchNamedStage(t *testing.T) { |
| if runtime.GOOS == "windows" { |
| t.Skip("Windows does not support scratch") |
| } |
| b := newBuilderWithMockBackend() |
| req := defaultDispatchReq(b, "scratch", "AS", "base") |
| |
| require.NoError(t, from(req)) |
| assert.True(t, req.state.hasFromImage()) |
| |
| req.args = []string{"base"} |
| require.NoError(t, from(req)) |
| assert.True(t, req.state.hasFromImage()) |
| } |
| |
| func TestOnbuildIllegalTriggers(t *testing.T) { |
| triggers := []struct{ command, expectedError string }{ |
| {"ONBUILD", "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed"}, |
| {"MAINTAINER", "MAINTAINER isn't allowed as an ONBUILD trigger"}, |
| {"FROM", "FROM isn't allowed as an ONBUILD trigger"}} |
| |
| for _, trigger := range triggers { |
| b := newBuilderWithMockBackend() |
| |
| err := onbuild(defaultDispatchReq(b, trigger.command)) |
| testutil.ErrorContains(t, err, trigger.expectedError) |
| } |
| } |
| |
| func TestOnbuild(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| req := defaultDispatchReq(b, "ADD", ".", "/app/src") |
| req.original = "ONBUILD ADD . /app/src" |
| req.state.runConfig = &container.Config{} |
| |
| err := onbuild(req) |
| require.NoError(t, err) |
| assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0]) |
| } |
| |
| func TestWorkdir(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| workingDir := "/app" |
| if runtime.GOOS == "windows" { |
| workingDir = "C:\app" |
| } |
| |
| req := defaultDispatchReq(b, workingDir) |
| err := workdir(req) |
| require.NoError(t, err) |
| assert.Equal(t, workingDir, req.state.runConfig.WorkingDir) |
| } |
| |
| func TestCmd(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| command := "./executable" |
| |
| req := defaultDispatchReq(b, command) |
| err := cmd(req) |
| require.NoError(t, err) |
| |
| var expectedCommand strslice.StrSlice |
| if runtime.GOOS == "windows" { |
| expectedCommand = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", command)) |
| } else { |
| expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command)) |
| } |
| |
| assert.Equal(t, expectedCommand, req.state.runConfig.Cmd) |
| assert.True(t, req.state.cmdSet) |
| } |
| |
| func TestHealthcheckNone(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| req := defaultDispatchReq(b, "NONE") |
| err := healthcheck(req) |
| require.NoError(t, err) |
| |
| require.NotNil(t, req.state.runConfig.Healthcheck) |
| assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test) |
| } |
| |
| func TestHealthcheckCmd(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| args := []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit", "1"} |
| req := defaultDispatchReq(b, args...) |
| err := healthcheck(req) |
| require.NoError(t, err) |
| |
| require.NotNil(t, req.state.runConfig.Healthcheck) |
| expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} |
| assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test) |
| } |
| |
| func TestEntrypoint(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| entrypointCmd := "/usr/sbin/nginx" |
| |
| req := defaultDispatchReq(b, entrypointCmd) |
| err := entrypoint(req) |
| require.NoError(t, err) |
| require.NotNil(t, req.state.runConfig.Entrypoint) |
| |
| var expectedEntrypoint strslice.StrSlice |
| if runtime.GOOS == "windows" { |
| expectedEntrypoint = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", entrypointCmd)) |
| } else { |
| expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd)) |
| } |
| assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint) |
| } |
| |
| func TestExpose(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| exposedPort := "80" |
| req := defaultDispatchReq(b, exposedPort) |
| err := expose(req) |
| require.NoError(t, err) |
| |
| require.NotNil(t, req.state.runConfig.ExposedPorts) |
| require.Len(t, req.state.runConfig.ExposedPorts, 1) |
| |
| portsMapping, err := nat.ParsePortSpec(exposedPort) |
| require.NoError(t, err) |
| assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port) |
| } |
| |
| func TestUser(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| userCommand := "foo" |
| |
| req := defaultDispatchReq(b, userCommand) |
| err := user(req) |
| require.NoError(t, err) |
| assert.Equal(t, userCommand, req.state.runConfig.User) |
| } |
| |
| func TestVolume(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| exposedVolume := "/foo" |
| |
| req := defaultDispatchReq(b, exposedVolume) |
| err := volume(req) |
| require.NoError(t, err) |
| |
| require.NotNil(t, req.state.runConfig.Volumes) |
| assert.Len(t, req.state.runConfig.Volumes, 1) |
| assert.Contains(t, req.state.runConfig.Volumes, exposedVolume) |
| } |
| |
| func TestStopSignal(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| signal := "SIGKILL" |
| |
| req := defaultDispatchReq(b, signal) |
| err := stopSignal(req) |
| require.NoError(t, err) |
| assert.Equal(t, signal, req.state.runConfig.StopSignal) |
| } |
| |
| func TestArg(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| argName := "foo" |
| argVal := "bar" |
| argDef := fmt.Sprintf("%s=%s", argName, argVal) |
| |
| err := arg(defaultDispatchReq(b, argDef)) |
| require.NoError(t, err) |
| |
| expected := map[string]string{argName: argVal} |
| assert.Equal(t, expected, b.buildArgs.GetAllAllowed()) |
| } |
| |
| func TestShell(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| |
| shellCmd := "powershell" |
| req := defaultDispatchReq(b, shellCmd) |
| req.attributes = map[string]bool{"json": true} |
| |
| err := shell(req) |
| require.NoError(t, err) |
| |
| expectedShell := strslice.StrSlice([]string{shellCmd}) |
| assert.Equal(t, expectedShell, req.state.runConfig.Shell) |
| } |
| |
| func TestParseOptInterval(t *testing.T) { |
| flInterval := &Flag{ |
| name: "interval", |
| flagType: stringType, |
| Value: "50ns", |
| } |
| _, err := parseOptInterval(flInterval) |
| testutil.ErrorContains(t, err, "cannot be less than 1ms") |
| |
| flInterval.Value = "1ms" |
| _, err = parseOptInterval(flInterval) |
| require.NoError(t, err) |
| } |
| |
| func TestPrependEnvOnCmd(t *testing.T) { |
| buildArgs := newBuildArgs(nil) |
| buildArgs.AddArg("NO_PROXY", nil) |
| |
| args := []string{"sorted=nope", "args=not", "http_proxy=foo", "NO_PROXY=YA"} |
| cmd := []string{"foo", "bar"} |
| cmdWithEnv := prependEnvOnCmd(buildArgs, args, cmd) |
| expected := strslice.StrSlice([]string{ |
| "|3", "NO_PROXY=YA", "args=not", "sorted=nope", "foo", "bar"}) |
| assert.Equal(t, expected, cmdWithEnv) |
| } |
| |
| func TestRunWithBuildArgs(t *testing.T) { |
| b := newBuilderWithMockBackend() |
| b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO") |
| b.disableCommit = false |
| |
| runConfig := &container.Config{} |
| origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) |
| cmdWithShell := strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo")) |
| envVars := []string{"|1", "one=two"} |
| cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...)) |
| |
| imageCache := &mockImageCache{ |
| getCacheFunc: func(parentID string, cfg *container.Config) (string, error) { |
| // Check the runConfig.Cmd sent to probeCache() |
| assert.Equal(t, cachedCmd, cfg.Cmd) |
| assert.Equal(t, strslice.StrSlice(nil), cfg.Entrypoint) |
| return "", nil |
| }, |
| } |
| |
| mockBackend := b.docker.(*MockBackend) |
| mockBackend.makeImageCacheFunc = func(_ []string, _ string) builder.ImageCache { |
| return imageCache |
| } |
| b.imageProber = newImageProber(mockBackend, nil, runtime.GOOS, false) |
| mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ReleaseableLayer, error) { |
| return &mockImage{ |
| id: "abcdef", |
| config: &container.Config{Cmd: origCmd}, |
| }, nil, nil |
| } |
| mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) { |
| // Check the runConfig.Cmd sent to create() |
| assert.Equal(t, cmdWithShell, config.Config.Cmd) |
| assert.Contains(t, config.Config.Env, "one=two") |
| assert.Equal(t, strslice.StrSlice{""}, config.Config.Entrypoint) |
| return container.ContainerCreateCreatedBody{ID: "12345"}, nil |
| } |
| mockBackend.commitFunc = func(cID string, cfg *backend.ContainerCommitConfig) (string, error) { |
| // Check the runConfig.Cmd sent to commit() |
| assert.Equal(t, origCmd, cfg.Config.Cmd) |
| assert.Equal(t, cachedCmd, cfg.ContainerConfig.Cmd) |
| assert.Equal(t, strslice.StrSlice(nil), cfg.Config.Entrypoint) |
| return "", nil |
| } |
| |
| req := defaultDispatchReq(b, "abcdef") |
| require.NoError(t, from(req)) |
| b.buildArgs.AddArg("one", strPtr("two")) |
| |
| req.args = []string{"echo foo"} |
| require.NoError(t, run(req)) |
| |
| // Check that runConfig.Cmd has not been modified by run |
| assert.Equal(t, origCmd, req.state.runConfig.Cmd) |
| } |