Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 111 additions & 5 deletions pkg/build/builder/docker.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package builder

import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"

dockercmd "github.com/docker/docker/builder/command"
"github.com/docker/docker/builder/parser"
"github.com/fsouza/go-dockerclient"

"github.com/openshift/origin/pkg/build/api"
"github.com/openshift/source-to-image/pkg/git"
"github.com/openshift/source-to-image/pkg/tar"
Expand All @@ -22,9 +27,6 @@ import (
// not proceed further and stop
const urlCheckTimeout = 16 * time.Second

// imageRegex is used to substitute image names in buildconfigs with immutable image ids at build time.
var imageRegex = regexp.MustCompile(`(?mi)^\s*FROM\s+\w+.+`)

// DockerBuilder builds Docker images given a git repository URL
type DockerBuilder struct {
dockerClient DockerClient
Expand Down Expand Up @@ -154,7 +156,10 @@ func (d *DockerBuilder) addBuildParameters(dir string) error {

var newFileData string
if d.build.Parameters.Strategy.DockerStrategy.BaseImage != "" {
newFileData = imageRegex.ReplaceAllLiteralString(string(fileData), fmt.Sprintf("FROM %s", d.build.Parameters.Strategy.DockerStrategy.BaseImage))
newFileData, err = replaceValidCmd(dockercmd.From, d.build.Parameters.Strategy.DockerStrategy.BaseImage, fileData)
if err != nil {
return err
}
} else {
newFileData = newFileData + string(fileData)
}
Expand All @@ -171,6 +176,107 @@ func (d *DockerBuilder) addBuildParameters(dir string) error {
return nil
}

// invalidCmdErr repesents an error returned from replaceValidCmd
// when an invalid Dockerfile command has been passed to
// replaceValidCmd
var invalidCmdErr = errors.New("invalid Dockerfile command")

// replaceValidCmd replaces the valid occurrence of cmd
// 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")
}

var pos int
switch cmd {
case dockercmd.From, dockercmd.Entrypoint, dockercmd.Cmd:
pos = traverseAST(cmd, node)
if pos == 0 {
fallthrough
}
default:
return string(fileData), nil
}

// 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(line, cmd) || strings.HasPrefix(line, strings.ToUpper(cmd)) {
index++

// The current line finishes on a backslash
// All we need to do is to replace the next
// line with our specified replaceArgs
if line[len(line)-1:] == "\\" && index == pos {
replaceNextLn = true
newFileData += line + "\n"
continue
}

// Normal ending line
if index == pos {
line = fmt.Sprintf("%s %s\n", strings.ToUpper(cmd), replaceArgs)
}
}

// Previous line ended on a backslash
// This line contains command arguments
if replaceNextLn {
replaceNextLn = false
line = replaceArgs + "\n"
}

newFileData += line
if err == io.EOF {
break
}
}
return newFileData, nil
}

// traverseAST traverses the Abstract Syntax Tree output
// from the Docker parser and returns the valid position
// of the command it was requested to look for.
// Note that this function is intended to be used with
// Dockerfile commands that should be specified only once
// in a Dockerfile (FROM, CMD, ENTRYPOINT)
func traverseAST(cmd string, node *parser.Node) int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this will always return the last occurrence of the command? should probably document that behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you are right. I wanted to add a sanity check inside that function but since it's recursive it would probably affect performance a bit. So I agree with you about documenting it and I will address it.

index := 0
if node.Value == cmd {
index++
}
for _, n := range node.Children {
index += traverseAST(cmd, n)
}
if node.Next != nil {
for n := node.Next; n != nil; n = n.Next {
if len(n.Children) > 0 {
index += traverseAST(cmd, n)
} else if n.Value == cmd {
index++
}
}
}
return index
}

// dockerBuild performs a docker build on the source that has been retrieved
func (d *DockerBuilder) dockerBuild(dir string) error {
var noCache bool
Expand Down
87 changes: 87 additions & 0 deletions pkg/build/builder/docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package builder

import (
"testing"
)

func TestReplaceValidCmd(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a test to verify the output is also parsable, meaning valid Dockerfile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be useful but it's out of scope of the current changes ie. command replacement vs Dockerfile validation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Au contraire the resulting file MUST be valid Dockerfile as it's being the source of the docker build. So I insist on having such test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add a test where we'll parse both the original and the new Dockerfile and compare their ASTs. Validation of the Dockerfile though is something different than what this change is supposed to do. @bparees what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should suffice IMHO, just running Parse on the new file should be a good start without any erros is OK.

tests := []struct {
name string
cmd string
replaceStr string
fileData []byte
expectedOutput string
expectedErr error
}{
{
name: "from-replacement",
cmd: "from",
replaceStr: "FROM other/image",
fileData: []byte(dockerFile),
expectedOutput: expectedFROM,
expectedErr: nil,
},
{
name: "run-replacement",
cmd: "run",
replaceStr: "This test kind-of-fails before string replacement so this string won't be used",
fileData: []byte(dockerFile),
expectedOutput: dockerFile,
expectedErr: nil,
},
{
name: "invalid-dockerfile-cmd",
cmd: "blabla",
replaceStr: "This test fails at start so this string won't be used",
fileData: []byte(dockerFile),
expectedOutput: "",
expectedErr: invalidCmdErr,
},
{
name: "no-cmd-in-dockerfile",
cmd: "cmd",
replaceStr: "CMD runme.sh",
fileData: []byte(dockerFile),
expectedOutput: dockerFile,
expectedErr: nil,
},
}

for _, test := range tests {
out, err := replaceValidCmd(test.cmd, test.replaceStr, 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: Expected %s, got %s", test.name, test.expectedOutput, out)
}
}
}

const dockerFile = `
FROM openshift/origin-base
FROM candidate

RUN mkdir -p /var/lib/openshift

ADD bin/openshift /usr/bin/openshift
RUN ln -s /usr/bin/openshift /usr/bin/osc && \

ENV HOME /root
WORKDIR /var/lib/openshift
ENTRYPOINT ["/usr/bin/openshift"]
`

const expectedFROM = `
FROM openshift/origin-base
FROM other/image

RUN mkdir -p /var/lib/openshift

ADD bin/openshift /usr/bin/openshift
RUN ln -s /usr/bin/openshift /usr/bin/osc && \

ENV HOME /root
WORKDIR /var/lib/openshift
ENTRYPOINT ["/usr/bin/openshift"]
`