func TestReplaceLastFrom(t *testing.T) { tests := []struct { original string image string want string }{ { original: `# no FROM instruction`, image: "centos", want: ``, }, { original: `FROM scratch # FROM busybox RUN echo "hello world" `, image: "centos", want: `FROM centos RUN echo "hello world" `, }, { original: `FROM scratch FROM busybox RUN echo "hello world" `, image: "centos", want: `FROM scratch FROM centos RUN echo "hello world" `, }, } for i, test := range tests { got, err := parser.Parse(strings.NewReader(test.original)) if err != nil { t.Errorf("test[%d]: %v", i, err) continue } want, err := parser.Parse(strings.NewReader(test.want)) if err != nil { t.Errorf("test[%d]: %v", i, err) continue } replaceLastFrom(got, test.image) if !reflect.DeepEqual(got, want) { t.Errorf("test[%d]: replaceLastFrom(node, %+v) = %+v; want %+v", i, test.image, got, want) t.Logf("resulting Dockerfile:\n%s", dockerfile.ParseTreeToDockerfile(got)) } } }
// Parse parses an input Dockerfile func (_ *parser) Parse(input io.Reader) (Dockerfile, error) { buf := bufio.NewReader(input) bts, err := buf.Peek(buf.Buffered()) if err != nil { return nil, err } parsedByDocker := bytes.NewBuffer(bts) // Add one more level of validation by using the Docker parser if _, err := dparser.Parse(parsedByDocker); err != nil { return nil, fmt.Errorf("cannot parse Dockerfile: %v", err) } d := dockerfile{} scanner := bufio.NewScanner(input) for { line, ok := nextLine(scanner, true) if !ok { break } parts, err := parseLine(line) if err != nil { return nil, err } d = append(d, parts) } return d, nil }
// Parse parses an input Dockerfile func (_ *parser) Parse(input io.Reader) (Dockerfile, error) { b, err := ioutil.ReadAll(input) if err != nil { return nil, err } r := bytes.NewReader(b) // Add one more level of validation by using the Docker parser if _, err := dparser.Parse(r); err != nil { return nil, fmt.Errorf("cannot parse Dockerfile: %v", err) } if _, err = r.Seek(0, 0); err != nil { return nil, err } d := dockerfile{} scanner := bufio.NewScanner(r) for { line, ok := nextLine(scanner, true) if !ok { break } parts, err := parseLine(line) if err != nil { return nil, err } d = append(d, parts) } return d, nil }
// FromImage updates the builder to use the provided image (resetting RunConfig // and recording the image environment), and updates the node with any ONBUILD // statements extracted from the parent image. func (b *Builder) FromImage(image *docker.Image, node *parser.Node) error { SplitChildren(node, command.From) b.RunConfig = *image.Config b.Env = b.RunConfig.Env b.RunConfig.Env = nil // Check to see if we have a default PATH, note that windows won't // have one as its set by HCS if runtime.GOOS != "windows" && !hasEnvName(b.Env, "PATH") { b.RunConfig.Env = append(b.RunConfig.Env, "PATH="+defaultPathEnv) } // Join the image onbuild statements into node if image.Config == nil || len(image.Config.OnBuild) == 0 { return nil } extra, err := parser.Parse(bytes.NewBufferString(strings.Join(image.Config.OnBuild, "\n"))) if err != nil { return err } for _, child := range extra.Children { switch strings.ToUpper(child.Value) { case "ONBUILD": return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") case "MAINTAINER", "FROM": return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", child.Value) } } node.Children = append(extra.Children, node.Children...) // Since we've processed the OnBuild statements, clear them from the runconfig state. b.RunConfig.OnBuild = nil return nil }
// TestNextValuesOnbuild tests calling nextValues with ONBUILD instructions as // input. func TestNextValuesOnbuild(t *testing.T) { testCases := map[string][]string{ `ONBUILD ADD . /app/src`: {".", "/app/src"}, `ONBUILD RUN echo "Hello universe!"`: {`echo "Hello universe!"`}, } for original, want := range testCases { node, err := parser.Parse(strings.NewReader(original)) if err != nil { t.Fatalf("parse error: %s: %v", original, err) } if len(node.Children) != 1 { t.Fatalf("unexpected number of children in test case: %s", original) } // The Docker parser always wrap instructions in a root node. // Look at the node representing the instruction following // ONBUILD, the one and only one in each test case. node = node.Children[0].Next if node == nil || len(node.Children) != 1 { t.Fatalf("unexpected number of children in ONBUILD instruction of test case: %s", original) } node = node.Children[0] if got := nextValues(node); !reflect.DeepEqual(got, want) { t.Errorf("nextValues(%+v) = %#v; want %#v", node, got, want) } } }
// TestNextValues tests calling nextValues with multiple valid combinations of // input. func TestNextValues(t *testing.T) { testCases := map[string][]string{ `FROM busybox:latest`: {"busybox:latest"}, `MAINTAINER [email protected]`: {"*****@*****.**"}, `LABEL version=1.0`: {"version", "1.0"}, `EXPOSE 8080`: {"8080"}, `VOLUME /var/run/www`: {"/var/run/www"}, `ENV PATH=/bin`: {"PATH", "/bin"}, `ADD file /home/`: {"file", "/home/"}, `COPY dir/ /tmp/`: {"dir/", "/tmp/"}, `RUN echo "Hello world!"`: {`echo "Hello world!"`}, `ENTRYPOINT /bin/sh`: {"/bin/sh"}, `CMD ["-c", "env"]`: {"-c", "env"}, `USER 1001`: {"1001"}, `WORKDIR /home`: {"/home"}, } for original, want := range testCases { node, err := parser.Parse(strings.NewReader(original)) if err != nil { t.Fatalf("parse error: %s: %v", original, err) } if len(node.Children) != 1 { t.Fatalf("unexpected number of children in test case: %s", original) } // The Docker parser always wrap instructions in a root node. // Look at the node representing the first instruction, the one // and only one in each test case. node = node.Children[0] if got := nextValues(node); !reflect.DeepEqual(got, want) { t.Errorf("nextValues(%+v) = %#v; want %#v", node, got, want) } } }
// BuildFromConfig will do build directly from parameter 'changes', which comes // from Dockerfile entries, it will: // // - call parse.Parse() to get AST root from Dockerfile entries // - do build by calling builder.dispatch() to call all entries' handling routines func BuildFromConfig(d *daemon.Daemon, c *runconfig.Config, changes []string) (*runconfig.Config, error) { ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) if err != nil { return nil, err } // ensure that the commands are valid for _, n := range ast.Children { if !validCommitCommands[n.Value] { return nil, fmt.Errorf("%s is not a valid change command", n.Value) } } builder := &builder{ Daemon: d, Config: c, OutStream: ioutil.Discard, ErrStream: ioutil.Discard, disableCommit: true, } for i, n := range ast.Children { if err := builder.dispatch(i, n); err != nil { return nil, err } } return builder.Config, nil }
func TestRun(t *testing.T) { f, err := os.Open("../../../../../images/dockerregistry/Dockerfile") if err != nil { t.Fatal(err) } node, err := parser.Parse(f) if err != nil { t.Fatal(err) } b := NewBuilder() from, err := b.From(node) if err != nil { t.Fatal(err) } if from != "openshift/origin-base" { t.Fatalf("unexpected from: %s", from) } for _, child := range node.Children { step := b.Step() if err := step.Resolve(child); err != nil { t.Fatal(err) } if err := b.Run(step, LogExecutor); err != nil { t.Fatal(err) } } t.Logf("config: %#v", b.Config()) t.Logf(node.Dump()) }
// Run the builder with the context. This is the lynchpin of this package. This // will (barring errors): // // * call readContext() which will set up the temporary directory and unpack // the context into it. // * read the dockerfile // * parse the dockerfile // * walk the parse tree and execute it by dispatching to handlers. If Remove // or ForceRemove is set, additional cleanup around containers happens after // processing. // * Print a happy message and return the image ID. // func (b *Builder) Run(context io.Reader) (string, error) { if err := b.readContext(context); err != nil { return "", err } defer func() { if err := os.RemoveAll(b.contextPath); err != nil { log.Debugf("[BUILDER] failed to remove temporary context: %s", err) } }() filename := path.Join(b.contextPath, "Dockerfile") fi, err := os.Stat(filename) if os.IsNotExist(err) { return "", fmt.Errorf("Cannot build a directory without a Dockerfile") } if fi.Size() == 0 { return "", ErrDockerfileEmpty } f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() ast, err := parser.Parse(f) if err != nil { return "", err } b.dockerfile = ast // some initializations that would not have been supplied by the caller. b.Config = &runconfig.Config{} b.TmpContainers = map[string]struct{}{} for i, n := range b.dockerfile.Children { if err := b.dispatch(i, n); err != nil { if b.ForceRemove { b.clearTmp() } return "", err } fmt.Fprintf(b.OutStream, " ---> %s\n", utils.TruncateID(b.image)) if b.Remove { b.clearTmp() } } if b.image == "" { return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?\n") } fmt.Fprintf(b.OutStream, "Successfully built %s\n", utils.TruncateID(b.image)) return b.image, nil }
// addBuildParameters checks if a Image is set to replace the default base image. // If that's the case then change the Dockerfile to make the build with the given image. // Also append the environment variables and labels in the Dockerfile. func (d *DockerBuilder) addBuildParameters(dir string) error { dockerfilePath := filepath.Join(dir, "Dockerfile") if d.build.Spec.Strategy.DockerStrategy != nil && len(d.build.Spec.Source.ContextDir) > 0 { dockerfilePath = filepath.Join(dir, d.build.Spec.Source.ContextDir, "Dockerfile") } f, err := os.Open(dockerfilePath) if err != nil { return err } // Parse the Dockerfile. node, err := parser.Parse(f) if err != nil { return err } // Update base image if build strategy specifies the From field. if d.build.Spec.Strategy.DockerStrategy.From != nil && d.build.Spec.Strategy.DockerStrategy.From.Kind == "DockerImage" { // Reduce the name to a minimal canonical form for the daemon name := d.build.Spec.Strategy.DockerStrategy.From.Name if ref, err := imageapi.ParseDockerImageReference(name); err == nil { name = ref.DaemonMinimal().String() } err := replaceLastFrom(node, name) if err != nil { return err } } // Append build info as environment variables. err = appendEnv(node, d.buildInfo()) if err != nil { return err } // Append build labels. err = appendLabel(node, d.buildLabels(dir)) if err != nil { return err } // Insert environment variables defined in the build strategy. err = insertEnvAfterFrom(node, d.build.Spec.Strategy.DockerStrategy.Env) if err != nil { return err } instructions := dockerfile.ParseTreeToDockerfile(node) // Overwrite the Dockerfile. fi, err := f.Stat() if err != nil { return err } return ioutil.WriteFile(dockerfilePath, instructions, fi.Mode()) }
func NewDockerfile(contents string) (Dockerfile, error) { if len(contents) == 0 { return nil, fmt.Errorf("Dockerfile is empty") } node, err := parser.Parse(strings.NewReader(contents)) if err != nil { return nil, err } return dockerfileContents{node, contents}, nil }
func TestTraverseAST(t *testing.T) { tests := []struct { name string cmd string fileData []byte expected int }{ { name: "dockerFile", cmd: dockercmd.Entrypoint, fileData: []byte(dockerFile), expected: 1, }, { name: "dockerFile no newline", cmd: dockercmd.Entrypoint, fileData: []byte(dockerFileNoNewline), expected: 1, }, { name: "expectedFROM", cmd: dockercmd.From, fileData: []byte(expectedFROM), expected: 2, }, { name: "trSlashFile", cmd: dockercmd.Entrypoint, fileData: []byte(trSlashFile), expected: 0, }, { name: "expectedtrSlashFile", cmd: dockercmd.Cmd, fileData: []byte(expectedtrSlashFile), expected: 1, }, } var buf *bytes.Buffer for _, test := range tests { buf = bytes.NewBuffer([]byte(test.fileData)) node, err := parser.Parse(buf) if err != nil { log.Println(err) } howMany := traverseAST(test.cmd, node) if howMany != test.expected { t.Errorf("Wrong result, expected %d, got %d", test.expected, howMany) } } }
// TestExposedPorts tests calling exposedPorts with multiple valid combinations // of input. func TestExposedPorts(t *testing.T) { testCases := map[string]struct { in string want [][]string }{ "empty Dockerfile": { in: ``, want: nil, }, "EXPOSE missing argument": { in: `EXPOSE`, want: nil, }, "EXPOSE no FROM": { in: `EXPOSE 8080`, want: nil, }, "single EXPOSE after FROM": { in: `FROM centos:7 EXPOSE 8080`, want: [][]string{{"8080"}}, }, "multiple EXPOSE and FROM": { in: `# EXPOSE before FROM should be ignore EXPOSE 777 FROM busybox EXPOSE 8080 COPY . /boot FROM rhel # no EXPOSE instruction FROM centos:7 EXPOSE 8000 EXPOSE 9090 9091 `, want: [][]string{{"8080"}, nil, {"8000", "9090", "9091"}}, }, } for name, tc := range testCases { node, err := parser.Parse(strings.NewReader(tc.in)) if err != nil { t.Errorf("%s: parse error: %v", name, err) continue } got := exposedPorts(node) if !reflect.DeepEqual(got, tc.want) { t.Errorf("exposedPorts: %s: got %#v; want %#v", name, got, tc.want) } } }
// TestInsertInstructionsPosOutOfRange tests calling InsertInstructions with // invalid values for the pos argument. func TestInsertInstructionsPosOutOfRange(t *testing.T) { original := `FROM busybox ENV PATH=/bin ` node, err := parser.Parse(strings.NewReader(original)) if err != nil { t.Fatalf("parse error: %v", err) } for _, pos := range []int{-1, 3, 4} { err := InsertInstructions(node, pos, "") if err == nil { t.Errorf("InsertInstructions(node, %d, \"\"): got nil; want error", pos) } } }
// InsertInstructions inserts instructions starting from the pos-th child of // node, moving other children as necessary. The instructions should be valid // Dockerfile instructions. InsertInstructions mutates node in-place, and the // final state of node is equivalent to what parser.Parse would return if the // original Dockerfile represented by node contained the instructions at the // specified position pos. If the returned error is non-nil, node is guaranteed // to be unchanged. func InsertInstructions(node *parser.Node, pos int, instructions string) error { if node == nil { return fmt.Errorf("cannot insert instructions in a nil node") } if pos < 0 || pos > len(node.Children) { return fmt.Errorf("pos %d out of range [0, %d]", pos, len(node.Children)-1) } newChild, err := parser.Parse(strings.NewReader(instructions)) if err != nil { return err } // InsertVector pattern (https://github.com/golang/go/wiki/SliceTricks) node.Children = append(node.Children[:pos], append(newChild.Children, node.Children[pos:]...)...) return nil }
// FromDockerfile generates an ImageRef from a given name, directory, and context path. // The directory and context path will be joined and the resulting path should be a // Dockerfile from where the image's ports will be extracted. func (g *imageRefGenerator) FromDockerfile(name string, dir string, context string) (*ImageRef, error) { // Look for Dockerfile in repository file, err := os.Open(filepath.Join(dir, context, "Dockerfile")) if err != nil { return nil, err } node, err := parser.Parse(file) if err != nil { return nil, err } ports := dockerfile.LastExposedPorts(node) return g.FromNameAndPorts(name, ports) }
func (b *builder) processImageFrom(img *image.Image) error { b.image = img.ID if img.Config != nil { b.Config = img.Config } // The default path will be blank on Windows (set by HCS) if len(b.Config.Env) == 0 && daemon.DefaultPathEnv != "" { b.Config.Env = append(b.Config.Env, "PATH="+daemon.DefaultPathEnv) } // Process ONBUILD triggers if they exist if nTriggers := len(b.Config.OnBuild); nTriggers != 0 { word := "trigger" if nTriggers > 1 { word = "triggers" } fmt.Fprintf(b.ErrStream, "# Executing %d build %s...\n", nTriggers, word) } // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. onBuildTriggers := b.Config.OnBuild b.Config.OnBuild = []string{} // parse the ONBUILD triggers by invoking the parser for _, step := range onBuildTriggers { ast, err := parser.Parse(strings.NewReader(step)) if err != nil { return err } for i, n := range ast.Children { switch strings.ToUpper(n.Value) { case "ONBUILD": return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") case "MAINTAINER", "FROM": return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value) } if err := b.dispatch(i, n); err != nil { return err } } } return nil }
// Reads a Dockerfile from the current context. It assumes that the // 'filename' is a relative path from the root of the context func (b *Builder) readDockerfile(origFile string) error { filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile) } fi, err := os.Lstat(filename) if os.IsNotExist(err) { return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile) } if fi.Size() == 0 { return ErrDockerfileEmpty } f, err := os.Open(filename) if err != nil { return err } b.dockerfile, err = parser.Parse(f) f.Close() if err != nil { return err } // After the Dockerfile has been parsed, we need to check the .dockerignore // file for either "Dockerfile" or ".dockerignore", and if either are // present then erase them from the build context. These files should never // have been sent from the client but we did send them to make sure that // we had the Dockerfile to actually parse, and then we also need the // .dockerignore file to know whether either file should be removed. // Note that this assumes the Dockerfile has been read into memory and // is now safe to be removed. excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore")) if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true { os.Remove(filepath.Join(b.contextPath, ".dockerignore")) b.context.(tarsum.BuilderContext).Remove(".dockerignore") } if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true { os.Remove(filepath.Join(b.contextPath, b.dockerfileName)) b.context.(tarsum.BuilderContext).Remove(b.dockerfileName) } return nil }
func checkDockerfile(fs *test.FakeFileSystem, t *testing.T) { if fs.WriteFileError != nil { t.Errorf("%v", fs.WriteFileError) } if fs.WriteFileName != "upload/src/Dockerfile" { t.Errorf("Expected Dockerfile in 'upload/src/Dockerfile', got %v", fs.WriteFileName) } if !strings.Contains(fs.WriteFileContent, `ENTRYPOINT ["./run"]`) { t.Errorf("The Dockerfile does not set correct entrypoint:\n %s\n", fs.WriteFileContent) } buf := bytes.NewBuffer([]byte(fs.WriteFileContent)) if _, err := parser.Parse(buf); err != nil { t.Errorf("cannot parse new Dockerfile: " + err.Error()) } }
func (b *BuilderJob) CmdBuildConfig(job *engine.Job) engine.Status { if len(job.Args) != 0 { return job.Errorf("Usage: %s\n", job.Name) } var ( changes = job.GetenvList("changes") newConfig runconfig.Config ) if err := job.GetenvJson("config", &newConfig); err != nil { return job.Error(err) } ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) if err != nil { return job.Error(err) } // ensure that the commands are valid for _, n := range ast.Children { if !validCommitCommands[n.Value] { return job.Errorf("%s is not a valid change command", n.Value) } } builder := &Builder{ Daemon: b.Daemon, Engine: b.Engine, Config: &newConfig, OutStream: ioutil.Discard, ErrStream: ioutil.Discard, disableCommit: true, } for i, n := range ast.Children { if err := builder.dispatch(i, n); err != nil { return job.Error(err) } } if err := json.NewEncoder(job.Stdout).Encode(builder.Config); err != nil { return job.Error(err) } return engine.StatusOK }
// TestInsertInstructionsUnparseable tests calling InsertInstructions with // instructions that the Docker parser cannot handle. func TestInsertInstructionsUnparseable(t *testing.T) { original := `FROM busybox ENV PATH=/bin ` node, err := parser.Parse(strings.NewReader(original)) if err != nil { t.Fatalf("parse error: %v", err) } for name, instructions := range map[string]string{ "env without value": `ENV PATH`, "nested json": `CMD [ "echo", [ "nested json" ] ]`, } { err = InsertInstructions(node, 1, instructions) if err == nil { t.Errorf("InsertInstructions: %s: got nil; want error", name) } } }
// replaceLastFrom changes the last FROM instruction of node to point to the // base image image. func replaceLastFrom(node *parser.Node, image string) error { if node == nil { return nil } for i := len(node.Children) - 1; i >= 0; i-- { child := node.Children[i] if child != nil && child.Value == dockercmd.From { from, err := dockerfile.From(image) if err != nil { return err } fromTree, err := parser.Parse(strings.NewReader(from)) if err != nil { return err } node.Children[i] = fromTree.Children[0] return nil } } return nil }
func main() { var f *os.File var err error if len(os.Args) < 2 { fmt.Println("please supply filename(s)") os.Exit(1) } for _, fn := range os.Args[1:] { f, err = os.Open(fn) if err != nil { panic(err) } ast, err := parser.Parse(f) if err != nil { panic(err) } else { fmt.Println(ast.Dump()) } } }
// TestFindAll tests calling FindAll with multiple values of cmd. func TestFindAll(t *testing.T) { instructions := `FROM scratch LABEL version=1.0 FROM busybox ENV PATH=/bin ` node, err := parser.Parse(strings.NewReader(instructions)) if err != nil { t.Fatalf("parse error: %v", err) } for cmd, want := range map[string][]int{ command.From: {0, 2}, command.Label: {1}, command.Env: {3}, command.Maintainer: nil, "UnknownCommand": nil, } { got := FindAll(node, cmd) if !reflect.DeepEqual(got, want) { t.Errorf("FindAll(node, %q) = %#v; want %#v", cmd, got, want) } } }
// TestBaseImages tests calling baseImages with multiple valid combinations of // input. func TestBaseImages(t *testing.T) { testCases := map[string]struct { in string want []string }{ "empty Dockerfile": { in: ``, want: nil, }, "FROM missing argument": { in: `FROM`, want: nil, }, "single FROM": { in: `FROM centos:7`, want: []string{"centos:7"}, }, "multiple FROM": { in: `FROM scratch COPY . /boot FROM centos:7`, want: []string{"scratch", "centos:7"}, }, } for name, tc := range testCases { node, err := parser.Parse(strings.NewReader(tc.in)) if err != nil { t.Errorf("%s: parse error: %v", name, err) continue } got := baseImages(node) if !reflect.DeepEqual(got, tc.want) { t.Errorf("baseImages: %s: got %#v; want %#v", name, got, tc.want) } } }
func TestBuilder(t *testing.T) { testCases := []struct { Dockerfile string From string Copies []Copy Runs []Run Config docker.Config ErrFn func(err error) bool }{ { Dockerfile: "testdata/dir/Dockerfile", From: "busybox", Copies: []Copy{ {Src: ".", Dest: []string{"/"}, Download: false}, {Src: ".", Dest: []string{"/dir"}}, {Src: "subdir/", Dest: []string{"/test/"}, Download: false}, }, Config: docker.Config{ Image: "busybox", }, }, { Dockerfile: "testdata/ignore/Dockerfile", From: "busybox", Copies: []Copy{ {Src: ".", Dest: []string{"/"}}, }, Config: docker.Config{ Image: "busybox", }, }, { Dockerfile: "testdata/Dockerfile.env", From: "busybox", Config: docker.Config{ Env: []string{"name=value", "name2=value2a value2b", "name1=value1", "name3=value3a\\n\"value3b\"", "name4=value4a\\\\nvalue4b"}, Image: "busybox", }, }, { Dockerfile: "testdata/Dockerfile.edgecases", From: "busybox", Copies: []Copy{ {Src: ".", Dest: []string{"/"}, Download: true}, {Src: ".", Dest: []string{"/test/copy"}}, }, Runs: []Run{ {Shell: false, Args: []string{"ls", "-la"}}, {Shell: false, Args: []string{"echo", "'1234'"}}, {Shell: true, Args: []string{"echo \"1234\""}}, {Shell: true, Args: []string{"echo 1234"}}, {Shell: true, Args: []string{"echo '1234' && echo \"456\" && echo 789"}}, {Shell: true, Args: []string{"sh -c 'echo root:testpass > /tmp/passwd'"}}, {Shell: true, Args: []string{"mkdir -p /test /test2 /test3/test"}}, }, Config: docker.Config{ User: "******", ExposedPorts: map[docker.Port]struct{}{"6000/tcp": {}, "3000/tcp": {}, "9000/tcp": {}, "5000/tcp": {}}, Env: []string{"SCUBA=1 DUBA 3"}, Cmd: []string{"/bin/sh", "-c", "echo 'test' | wc -"}, Image: "busybox", Volumes: map[string]struct{}{"/test2": {}, "/test3": {}, "/test": {}}, WorkingDir: "/test", OnBuild: []string{"RUN [\"echo\", \"test\"]", "RUN echo test", "COPY . /"}, }, }, { Dockerfile: "testdata/Dockerfile.exposedefault", From: "busybox", Config: docker.Config{ ExposedPorts: map[docker.Port]struct{}{"3469/tcp": {}}, Image: "busybox", }, }, { Dockerfile: "testdata/Dockerfile.add", From: "busybox", Copies: []Copy{ {Src: "https://github.com/openshift/origin/raw/master/README.md", Dest: []string{"/README.md"}, Download: true}, {Src: "https://github.com/openshift/origin/raw/master/LICENSE", Dest: []string{"/"}, Download: true}, {Src: "https://github.com/openshift/origin/raw/master/LICENSE", Dest: []string{"/A"}, Download: true}, {Src: "https://github.com/openshift/origin/raw/master/LICENSE", Dest: []string{"/a"}, Download: true}, {Src: "https://github.com/openshift/origin/raw/master/LICENSE", Dest: []string{"/b/a"}, Download: true}, {Src: "https://github.com/openshift/origin/raw/master/LICENSE", Dest: []string{"/b/"}, Download: true}, {Src: "https://github.com/openshift/ruby-hello-world/archive/master.zip", Dest: []string{"/tmp/"}, Download: true}, }, Runs: []Run{ {Shell: true, Args: []string{"mkdir ./b"}}, }, Config: docker.Config{ Image: "busybox", User: "******", }, }, } for i, test := range testCases { data, err := ioutil.ReadFile(test.Dockerfile) if err != nil { t.Errorf("%d: %v", i, err) continue } node, err := parser.Parse(bytes.NewBuffer(data)) if err != nil { t.Errorf("%d: %v", i, err) continue } b := NewBuilder() from, err := b.From(node) if err != nil { t.Errorf("%d: %v", i, err) continue } if from != test.From { t.Errorf("%d: unexpected FROM: %s", i, from) } e := &testExecutor{} var lastErr error for j, child := range node.Children { step := b.Step() if err := step.Resolve(child); err != nil { lastErr = fmt.Errorf("%d: %d: %s: resolve: %v", i, j, step.Original, err) break } if err := b.Run(step, e); err != nil { lastErr = fmt.Errorf("%d: %d: %s: run: %v", i, j, step.Original, err) break } } if lastErr != nil { if test.ErrFn == nil || !test.ErrFn(lastErr) { t.Errorf("%d: unexpected error: %v", i, lastErr) } continue } if !reflect.DeepEqual(test.Copies, e.Copies) { t.Errorf("%d: unexpected copies: %#v", i, e.Copies) } if !reflect.DeepEqual(test.Runs, e.Runs) { t.Errorf("%d: unexpected runs: %#v", i, e.Runs) } lastConfig := b.RunConfig if !reflect.DeepEqual(test.Config, lastConfig) { t.Errorf("%d: unexpected config: %#v", i, lastConfig) } } }
func TestReplaceValidCmd(t *testing.T) { tests := []struct { name string cmd string replaceArgs string fileData []byte expectedOutput string expectedDiffs int expectedErr error }{ { name: "from-replacement", cmd: dockercmd.From, replaceArgs: "other/image", fileData: []byte(dockerFile), expectedOutput: expectedFROM, expectedDiffs: 1, expectedErr: nil, }, { name: "run-replacement", cmd: dockercmd.Run, replaceArgs: "This test kind-of-fails before string replacement so this string won't be used", fileData: []byte(dockerFile), expectedOutput: "", expectedErr: replaceCmdErr, }, { name: "invalid-dockerfile-cmd", cmd: "blabla", replaceArgs: "This test fails at start so this string won't be used", fileData: []byte(dockerFile), expectedOutput: "", expectedErr: invalidCmdErr, }, { name: "no-cmd-in-dockerfile", cmd: dockercmd.Cmd, replaceArgs: "runme.sh", fileData: []byte(dockerFile), expectedOutput: "", expectedErr: replaceCmdErr, }, { name: "trailing-slash", cmd: dockercmd.From, replaceArgs: "rhel", fileData: []byte(trSlashFile), expectedOutput: expectedtrSlashFile, expectedDiffs: 1, expectedErr: nil, }, { name: "multiple trailing slashes plus plus", cmd: dockercmd.From, replaceArgs: "scratch", fileData: []byte(trickierFile), expectedOutput: expectedTrickierFile, expectedDiffs: 1, expectedErr: nil, }, } for _, test := range tests { out, err := replaceValidCmd(test.cmd, test.replaceArgs, test.fileData) if err != test.expectedErr { t.Errorf("%s: Unexpected error: Expected %v, got %v", test.name, test.expectedErr, err) } if out != test.expectedOutput { t.Errorf("%s: Unexpected output:\n\nExpected:\n%s\n(length: %d)\n\ngot:\n%s\n(length: %d)", test.name, test.expectedOutput, len(test.expectedOutput), out, len(out)) } } // Re-use the tests above var buf *bytes.Buffer for _, test := range tests { buf = bytes.NewBuffer([]byte(test.fileData)) original, err := parser.Parse(buf) if err != nil { log.Println(err) } repl, err := replaceValidCmd(test.cmd, test.replaceArgs, test.fileData) if err != nil { log.Println(err) } buf = bytes.NewBuffer([]byte(repl)) edited, err := parser.Parse(buf) if err != nil { log.Println(err) } diff := cmpASTs(original, edited) if diff != test.expectedDiffs { t.Errorf("%s: Edit mismatch, expected %d edit(s), got %d", test.name, test.expectedDiffs, diff) } } }
// Reads a Dockerfile from the current context. It assumes that the // 'filename' is a relative path from the root of the context func (b *builder) readDockerfile() error { // If no -f was specified then look for 'Dockerfile'. If we can't find // that then look for 'dockerfile'. If neither are found then default // back to 'Dockerfile' and use that in the error message. if b.dockerfileName == "" { b.dockerfileName = api.DefaultDockerfileName tmpFN := filepath.Join(b.contextPath, api.DefaultDockerfileName) if _, err := os.Lstat(tmpFN); err != nil { tmpFN = filepath.Join(b.contextPath, strings.ToLower(api.DefaultDockerfileName)) if _, err := os.Lstat(tmpFN); err == nil { b.dockerfileName = strings.ToLower(api.DefaultDockerfileName) } } } origFile := b.dockerfileName filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath) if err != nil { return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile) } fi, err := os.Lstat(filename) if os.IsNotExist(err) { return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile) } if fi.Size() == 0 { return fmt.Errorf("The Dockerfile (%s) cannot be empty", origFile) } f, err := os.Open(filename) if err != nil { return err } b.dockerfile, err = parser.Parse(f) f.Close() if err != nil { return err } // After the Dockerfile has been parsed, we need to check the .dockerignore // file for either "Dockerfile" or ".dockerignore", and if either are // present then erase them from the build context. These files should never // have been sent from the client but we did send them to make sure that // we had the Dockerfile to actually parse, and then we also need the // .dockerignore file to know whether either file should be removed. // Note that this assumes the Dockerfile has been read into memory and // is now safe to be removed. excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore")) if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true { os.Remove(filepath.Join(b.contextPath, ".dockerignore")) b.context.(tarsum.BuilderContext).Remove(".dockerignore") } if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true { os.Remove(filepath.Join(b.contextPath, b.dockerfileName)) b.context.(tarsum.BuilderContext).Remove(b.dockerfileName) } return nil }
// replaceValidCmd replaces the valid occurrence of a command // in a Dockerfile with the given replaceArgs func replaceValidCmd(cmd, replaceArgs string, fileData []byte) (string, error) { if _, ok := dockercmd.Commands[cmd]; !ok { return "", invalidCmdErr } buf := bytes.NewBuffer(fileData) // Parse with Docker parser node, err := parser.Parse(buf) if err != nil { return "", errors.New("cannot parse Dockerfile: " + err.Error()) } pos := traverseAST(cmd, node) if pos == 0 { return "", replaceCmdErr } // Re-initialize the buffer buf = bytes.NewBuffer(fileData) var newFileData string var index int var replaceNextLn bool for { line, err := buf.ReadString('\n') if err != nil && err != io.EOF { return "", err } line = strings.TrimSpace(line) // The current line starts with the specified command (cmd) if strings.HasPrefix(strings.ToUpper(line), strings.ToUpper(cmd)) { index++ // The current line finishes on a backslash. // All we need to do is replace the next line // with our specified replaceArgs if line[len(line)-1:] == "\\" && index == pos { replaceNextLn = true args := strings.Split(line, " ") if len(args) > 2 { // Keep just our Dockerfile command and the backslash newFileData += args[0] + " \\" + "\n" } else { newFileData += line + "\n" } continue } // Normal ending line if index == pos { line = fmt.Sprintf("%s %s", strings.ToUpper(cmd), replaceArgs) } } // Previous line ended on a backslash // This line contains command arguments if replaceNextLn { if line[len(line)-1:] == "\\" { // Ignore all successive lines terminating on a backslash // since they all are going to be replaced by replaceArgs continue } replaceNextLn = false line = replaceArgs } if err == io.EOF { // Otherwise, the new Dockerfile will have one newline // more in the end newFileData += line break } newFileData += line + "\n" } // Parse output for validation buf = bytes.NewBuffer([]byte(newFileData)) if _, err := parser.Parse(buf); err != nil { return "", errors.New("cannot parse new Dockerfile: " + err.Error()) } return newFileData, nil }
func TestInsertEnvAfterFrom(t *testing.T) { tests := map[string]struct { original string env []kapi.EnvVar want string }{ "no FROM instruction": { original: `RUN echo "invalid Dockerfile" `, env: []kapi.EnvVar{ {Name: "PATH", Value: "/bin"}, }, want: `RUN echo "invalid Dockerfile" `}, "empty env": { original: `FROM busybox `, env: []kapi.EnvVar{}, want: `FROM busybox `}, "single FROM instruction": { original: `FROM busybox RUN echo "hello world" `, env: []kapi.EnvVar{ {Name: "PATH", Value: "/bin"}, }, want: `FROM busybox ENV "PATH"="/bin" RUN echo "hello world" `}, "multiple FROM instructions": { original: `FROM scratch FROM busybox RUN echo "hello world" `, env: []kapi.EnvVar{ {Name: "PATH", Value: "/bin"}, {Name: "GOPATH", Value: "/go"}, {Name: "PATH", Value: "/go/bin:$PATH"}, }, want: `FROM scratch ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="/go/bin:$PATH" FROM busybox ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="/go/bin:$PATH" RUN echo "hello world" `}, } for name, test := range tests { got, err := parser.Parse(strings.NewReader(test.original)) if err != nil { t.Errorf("%s: %v", name, err) continue } want, err := parser.Parse(strings.NewReader(test.want)) if err != nil { t.Errorf("%s: %v", name, err) continue } insertEnvAfterFrom(got, test.env) if !reflect.DeepEqual(got, want) { t.Errorf("%s: insertEnvAfterFrom(node, %+v) = %+v; want %+v", name, test.env, got, want) t.Logf("resulting Dockerfile:\n%s", dockerfile.ParseTreeToDockerfile(got)) } } }