8da7af8f |
package builder
import (
"fmt"
"io/ioutil"
"os" |
70672304 |
"os/exec" |
8da7af8f |
"path/filepath"
"strings"
"time"
|
09b57bf7 |
dockercmd "github.com/docker/docker/builder/command"
"github.com/docker/docker/builder/parser" |
8933ecc1 |
docker "github.com/fsouza/go-dockerclient" |
83c702b4 |
kapi "k8s.io/kubernetes/pkg/api" |
09b57bf7 |
|
05696c1e |
s2iapi "github.com/openshift/source-to-image/pkg/api" |
aa60c834 |
"github.com/openshift/source-to-image/pkg/tar" |
b427b4f2 |
"github.com/openshift/source-to-image/pkg/util" |
97eb0bfd |
"github.com/openshift/origin/pkg/build/api"
"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg" |
70672304 |
"github.com/openshift/origin/pkg/build/controller/strategy" |
e66f133f |
"github.com/openshift/origin/pkg/client" |
21e0dd92 |
"github.com/openshift/origin/pkg/generate/git" |
b10d7000 |
imageapi "github.com/openshift/origin/pkg/image/api" |
97eb0bfd |
"github.com/openshift/origin/pkg/util/docker/dockerfile" |
8da7af8f |
)
|
21f6fc01 |
// defaultDockerfilePath is the default path of the Dockerfile
const defaultDockerfilePath = "Dockerfile"
|
8da7af8f |
// DockerBuilder builds Docker images given a git repository URL
type DockerBuilder struct {
dockerClient DockerClient |
21e0dd92 |
gitClient GitClient |
8da7af8f |
tar tar.Tar
build *api.Build
urlTimeout time.Duration |
e66f133f |
client client.BuildInterface |
05696c1e |
cgLimits *s2iapi.CGroupLimits |
8da7af8f |
}
// NewDockerBuilder creates a new instance of DockerBuilder |
05696c1e |
func NewDockerBuilder(dockerClient DockerClient, buildsClient client.BuildInterface, build *api.Build, gitClient GitClient, cgLimits *s2iapi.CGroupLimits) *DockerBuilder { |
8da7af8f |
return &DockerBuilder{
dockerClient: dockerClient,
build: build, |
21e0dd92 |
gitClient: gitClient, |
aa60c834 |
tar: tar.New(), |
8da7af8f |
urlTimeout: urlCheckTimeout, |
e66f133f |
client: buildsClient, |
05696c1e |
cgLimits: cgLimits, |
8da7af8f |
}
}
// Build executes a Docker build
func (d *DockerBuilder) Build() error { |
9d089a21 |
if d.build.Spec.Source.Git == nil && d.build.Spec.Source.Binary == nil && d.build.Spec.Source.Dockerfile == nil && d.build.Spec.Source.Images == nil {
return fmt.Errorf("must provide a value for at least one of source, binary, images, or dockerfile")
} |
51c78d25 |
var push bool |
3e5a8591 |
pushTag := d.build.Status.OutputDockerImageReference |
51c78d25 |
|
8da7af8f |
buildDir, err := ioutil.TempDir("", "docker-build")
if err != nil {
return err
} |
bb7ce114 |
sourceInfo, err := fetchSource(d.dockerClient, buildDir, d.build, d.urlTimeout, os.Stdin, d.gitClient) |
e66f133f |
if err != nil { |
8da7af8f |
return err
} |
e66f133f |
if sourceInfo != nil {
updateBuildRevision(d.client, d.build, sourceInfo)
} |
b10d7000 |
if err := d.addBuildParameters(buildDir); err != nil { |
8da7af8f |
return err
} |
70672304 |
|
b10d7000 |
glog.V(4).Infof("Starting Docker build from build config %s ...", d.build.Name) |
8933ecc1 |
// if there is no output target, set one up so the docker build logic |
51c78d25 |
// (which requires a tag) will still work, but we won't push it at the end. |
8933ecc1 |
if d.build.Spec.Output.To == nil || len(d.build.Spec.Output.To.Name) == 0 { |
51c78d25 |
d.build.Status.OutputDockerImageReference = d.build.Name |
8933ecc1 |
} else {
push = true
} |
0cc72926 |
|
3e5a8591 |
buildTag := randomBuildTag(d.build.Namespace, d.build.Name)
if err := d.dockerBuild(buildDir, buildTag, d.build.Spec.Source.Secrets); err != nil {
return err
}
cname := containerName("docker", d.build.Name, d.build.Namespace, "post-commit")
if err := execPostCommitHook(d.dockerClient, d.build.Spec.PostCommit, buildTag, cname); err != nil { |
0cc72926 |
return err
}
|
16a3adc9 |
if push {
if err := tagImage(d.dockerClient, buildTag, pushTag); err != nil {
return err
} |
3e5a8591 |
} |
16a3adc9 |
|
3e5a8591 |
if err := removeImage(d.dockerClient, buildTag); err != nil { |
1b10493b |
glog.V(0).Infof("warning: Failed to remove temporary build tag %v: %v", buildTag, err) |
3e5a8591 |
}
|
8933ecc1 |
if push { |
13acec54 |
// Get the Docker push authentication
pushAuthConfig, authPresent := dockercfg.NewHelper().GetDockerAuth( |
3e5a8591 |
pushTag, |
13acec54 |
dockercfg.PushAuthType,
)
if authPresent { |
b10d7000 |
glog.V(4).Infof("Authenticating Docker push with user %q", pushAuthConfig.Username) |
13acec54 |
} |
1b10493b |
glog.V(1).Infof("Pushing image %s ...", pushTag) |
3e5a8591 |
if err := pushImage(d.dockerClient, pushTag, pushAuthConfig); err != nil { |
47e16125 |
return fmt.Errorf("Failed to push image: %v", err) |
f24be2f3 |
} |
1b10493b |
glog.V(1).Infof("Push successful") |
8da7af8f |
} |
1b9f255f |
return nil |
8da7af8f |
}
|
70672304 |
// copySecrets copies all files from the directory where the secret is
// mounted in the builder pod to a directory where the is the Dockerfile, so
// users can ADD or COPY the files inside their Dockerfile.
func (d *DockerBuilder) copySecrets(secrets []api.SecretBuildSource, buildDir string) error {
for _, s := range secrets {
dstDir := filepath.Join(buildDir, s.DestinationDir)
if err := os.MkdirAll(dstDir, 0777); err != nil {
return err
}
srcDir := filepath.Join(strategy.SecretBuildSourceBaseMountPath, s.Secret.Name) |
1b10493b |
glog.V(3).Infof("Copying files from the build secret %q to %q", s.Secret.Name, filepath.Clean(s.DestinationDir)) |
70672304 |
out, err := exec.Command("cp", "-vrf", srcDir+"/.", dstDir+"/").Output()
if err != nil { |
1b10493b |
glog.V(4).Infof("Secret %q failed to copy: %q", s.Secret.Name, string(out)) |
70672304 |
return err
}
// See what is copied where when debugging.
glog.V(5).Infof(string(out))
}
return nil
}
|
62c9fcbb |
// addBuildParameters checks if a Image is set to replace the default base image. |
17d00230 |
// If that's the case then change the Dockerfile to make the build with the given image. |
b427b4f2 |
// Also append the environment variables and labels in the Dockerfile. |
17d00230 |
func (d *DockerBuilder) addBuildParameters(dir string) error { |
21f6fc01 |
var contextDirPath string |
8933ecc1 |
if d.build.Spec.Strategy.DockerStrategy != nil && len(d.build.Spec.Source.ContextDir) > 0 { |
21f6fc01 |
contextDirPath = filepath.Join(dir, d.build.Spec.Source.ContextDir)
} else {
contextDirPath = dir
}
var dockerfilePath string
if d.build.Spec.Strategy.DockerStrategy != nil && len(d.build.Spec.Strategy.DockerStrategy.DockerfilePath) > 0 {
dockerfilePath = filepath.Join(contextDirPath, d.build.Spec.Strategy.DockerStrategy.DockerfilePath)
} else {
dockerfilePath = filepath.Join(contextDirPath, defaultDockerfilePath) |
bcba00e9 |
} |
8da7af8f |
|
97eb0bfd |
f, err := os.Open(dockerfilePath) |
84d46285 |
if err != nil {
return err
}
|
97eb0bfd |
// Parse the Dockerfile.
node, err := parser.Parse(f) |
8da7af8f |
if err != nil {
return err
} |
17d00230 |
|
97eb0bfd |
// Update base image if build strategy specifies the From field. |
8933ecc1 |
if d.build.Spec.Strategy.DockerStrategy.From != nil && d.build.Spec.Strategy.DockerStrategy.From.Kind == "DockerImage" { |
b10d7000 |
// 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 { |
a54b74fb |
name = ref.DaemonMinimal().Exact() |
b10d7000 |
}
err := replaceLastFrom(node, name) |
09b57bf7 |
if err != nil {
return err
} |
8da7af8f |
} |
17d00230 |
|
97eb0bfd |
// Append build info as environment variables.
err = appendEnv(node, d.buildInfo())
if err != nil {
return err |
b427b4f2 |
} |
d67b0734 |
|
97eb0bfd |
// Append build labels.
err = appendLabel(node, d.buildLabels(dir)) |
d67b0734 |
if err != nil { |
d8b46d02 |
return err
}
|
97eb0bfd |
// Insert environment variables defined in the build strategy. |
d67b0734 |
err = insertEnvAfterFrom(node, d.build.Spec.Strategy.DockerStrategy.Env)
if err != nil {
return err
}
instructions := dockerfile.ParseTreeToDockerfile(node)
|
97eb0bfd |
// Overwrite the Dockerfile.
fi, err := f.Stat()
if err != nil {
return err
}
return ioutil.WriteFile(dockerfilePath, instructions, fi.Mode()) |
d8b46d02 |
}
|
97eb0bfd |
// buildInfo converts the buildInfo output to a format that appendEnv can
// consume.
func (d *DockerBuilder) buildInfo() []dockerfile.KeyValue {
bi := buildInfo(d.build)
kv := make([]dockerfile.KeyValue, len(bi))
for i, item := range bi {
kv[i] = dockerfile.KeyValue{Key: item.Key, Value: item.Value} |
8da7af8f |
} |
97eb0bfd |
return kv |
17d00230 |
}
|
97eb0bfd |
// buildLabels returns a slice of KeyValue pairs in a format that appendEnv can
// consume.
func (d *DockerBuilder) buildLabels(dir string) []dockerfile.KeyValue {
labels := map[string]string{} |
604ac45d |
// TODO: allow source info to be overridden by build |
21e0dd92 |
sourceInfo := &git.SourceInfo{} |
9b43b0e3 |
if d.build.Spec.Source.Git != nil { |
21e0dd92 |
var errors []error
sourceInfo, errors = d.gitClient.GetInfo(dir)
if len(errors) > 0 {
for _, e := range errors { |
1b10493b |
glog.V(0).Infof("warning: Unable to retrieve Git info: %v", e.Error()) |
21e0dd92 |
}
} |
9b43b0e3 |
} |
97eb0bfd |
if len(d.build.Spec.Source.ContextDir) > 0 {
sourceInfo.ContextDir = d.build.Spec.Source.ContextDir |
09b57bf7 |
} |
21e0dd92 |
labels = util.GenerateLabelsFromSourceInfo(labels, &sourceInfo.SourceInfo, api.DefaultDockerLabelNamespace) |
9d1ac783 |
kv := make([]dockerfile.KeyValue, 0, len(labels)) |
97eb0bfd |
for k, v := range labels { |
9d1ac783 |
kv = append(kv, dockerfile.KeyValue{Key: k, Value: v}) |
97eb0bfd |
}
return kv
} |
09b57bf7 |
|
97eb0bfd |
// setupPullSecret provides a Docker authentication configuration when the
// PullSecret is specified.
func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) {
if len(os.Getenv(dockercfg.PullAuthType)) == 0 {
return nil, nil |
09b57bf7 |
} |
90f3434d |
glog.V(2).Infof("Checking for Docker config file for %s in path %s", dockercfg.PullAuthType, os.Getenv(dockercfg.PullAuthType))
dockercfgPath := dockercfg.GetDockercfgFile(os.Getenv(dockercfg.PullAuthType))
if len(dockercfgPath) == 0 {
return nil, fmt.Errorf("no docker config file found in '%s'", os.Getenv(dockercfg.PullAuthType))
}
glog.V(2).Infof("Using Docker config file %s", dockercfgPath)
r, err := os.Open(dockercfgPath) |
97eb0bfd |
if err != nil { |
90f3434d |
return nil, fmt.Errorf("'%s': %s", dockercfgPath, err) |
97eb0bfd |
}
return docker.NewAuthConfigurations(r) |
90f3434d |
|
97eb0bfd |
} |
09b57bf7 |
|
97eb0bfd |
// dockerBuild performs a docker build on the source that has been retrieved |
3e5a8591 |
func (d *DockerBuilder) dockerBuild(dir string, tag string, secrets []api.SecretBuildSource) error { |
97eb0bfd |
var noCache bool
var forcePull bool |
21f6fc01 |
dockerfilePath := defaultDockerfilePath |
97eb0bfd |
if d.build.Spec.Strategy.DockerStrategy != nil {
if d.build.Spec.Source.ContextDir != "" {
dir = filepath.Join(dir, d.build.Spec.Source.ContextDir) |
09b57bf7 |
} |
21f6fc01 |
if d.build.Spec.Strategy.DockerStrategy.DockerfilePath != "" {
dockerfilePath = d.build.Spec.Strategy.DockerStrategy.DockerfilePath
} |
97eb0bfd |
noCache = d.build.Spec.Strategy.DockerStrategy.NoCache
forcePull = d.build.Spec.Strategy.DockerStrategy.ForcePull
}
auth, err := d.setupPullSecret()
if err != nil {
return err
} |
70672304 |
if err := d.copySecrets(secrets, dir); err != nil {
return err
} |
3e5a8591 |
return buildImage(d.dockerClient, dir, dockerfilePath, noCache, tag, d.tar, auth, forcePull, d.cgLimits) |
97eb0bfd |
} |
09b57bf7 |
|
97eb0bfd |
// replaceLastFrom changes the last FROM instruction of node to point to the |
a54b74fb |
// base image. |
97eb0bfd |
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 |
09b57bf7 |
} |
97eb0bfd |
fromTree, err := parser.Parse(strings.NewReader(from))
if err != nil {
return err |
09b57bf7 |
} |
97eb0bfd |
node.Children[i] = fromTree.Children[0]
return nil |
09b57bf7 |
}
} |
97eb0bfd |
return nil
} |
09b57bf7 |
|
97eb0bfd |
// appendEnv appends an ENV Dockerfile instruction as the last child of node
// with keys and values from m.
func appendEnv(node *parser.Node, m []dockerfile.KeyValue) error {
return appendKeyValueInstruction(dockerfile.Env, node, m)
} |
09b57bf7 |
|
97eb0bfd |
// appendLabel appends a LABEL Dockerfile instruction as the last child of node
// with keys and values from m.
func appendLabel(node *parser.Node, m []dockerfile.KeyValue) error { |
9d1ac783 |
if len(m) == 0 {
return nil
} |
97eb0bfd |
return appendKeyValueInstruction(dockerfile.Label, node, m) |
09b57bf7 |
}
|
97eb0bfd |
// appendKeyValueInstruction is a primitive used to avoid code duplication.
// Callers should use a derivative of this such as appendEnv or appendLabel.
// appendKeyValueInstruction appends a Dockerfile instruction with key-value
// syntax created by f as the last child of node with keys and values from m.
func appendKeyValueInstruction(f func([]dockerfile.KeyValue) (string, error), node *parser.Node, m []dockerfile.KeyValue) error {
if node == nil {
return nil |
09b57bf7 |
} |
97eb0bfd |
instruction, err := f(m)
if err != nil {
return err
}
return dockerfile.InsertInstructions(node, len(node.Children), instruction) |
09b57bf7 |
}
|
d67b0734 |
// insertEnvAfterFrom inserts an ENV instruction with the environment variables
// from env after every FROM instruction in node.
func insertEnvAfterFrom(node *parser.Node, env []kapi.EnvVar) error {
if node == nil || len(env) == 0 {
return nil
}
// Build ENV instruction.
var m []dockerfile.KeyValue
for _, e := range env {
m = append(m, dockerfile.KeyValue{Key: e.Name, Value: e.Value})
}
buildEnv, err := dockerfile.Env(m)
if err != nil {
return err
}
// Insert the buildEnv after every FROM instruction.
// We iterate in reverse order, otherwise indices would have to be
// recomputed after each step, because we're changing node in-place.
indices := dockerfile.FindAll(node, dockercmd.From)
for i := len(indices) - 1; i >= 0; i-- {
err := dockerfile.InsertInstructions(node, indices[i]+1, buildEnv)
if err != nil {
return err
}
}
return nil
} |