From a8b0fc933849bd113c95a1ecffc50cf5c9196d7e Mon Sep 17 00:00:00 2001 From: Rodolfo Carvalho Date: Thu, 14 Jan 2016 17:15:41 +0100 Subject: [PATCH] Run user-provided command as part of build flow This is meant to be used for running project unit tests as part of a build. --- api/swagger-spec/oapi-v1.json | 17 ++++++ pkg/api/deep_copy_generated.go | 9 +++ pkg/api/v1/conversion_generated.go | 32 +++++++++++ pkg/api/v1/deep_copy_generated.go | 9 +++ pkg/api/v1beta3/conversion_generated.go | 32 +++++++++++ pkg/api/v1beta3/deep_copy_generated.go | 9 +++ pkg/build/api/types.go | 11 ++++ pkg/build/api/v1/types.go | 11 ++++ pkg/build/api/v1beta3/types.go | 11 ++++ pkg/build/builder/common.go | 45 +++++++++++++++ pkg/build/builder/docker.go | 73 ++++++++++++++++++++++--- pkg/build/builder/dockerutil.go | 62 +++++++++++++++++++++ pkg/build/builder/sti.go | 55 ++++++++++++++++--- pkg/build/generator/generator.go | 1 + 14 files changed, 362 insertions(+), 15 deletions(-) diff --git a/api/swagger-spec/oapi-v1.json b/api/swagger-spec/oapi-v1.json index a3d675b93892..ac339da92530 100644 --- a/api/swagger-spec/oapi-v1.json +++ b/api/swagger-spec/oapi-v1.json @@ -14594,6 +14594,10 @@ "$ref": "v1.ResourceRequirements", "description": "the desired compute resources the build should have" }, + "postCommit": { + "$ref": "v1.BuildPostCommitSpec", + "description": "an action executed after the build output image is committed" + }, "completionDeadlineSeconds": { "type": "integer", "format": "int64", @@ -15075,6 +15079,15 @@ } } }, + "v1.BuildPostCommitSpec": { + "id": "v1.BuildPostCommitSpec", + "properties": { + "runBash": { + "type": "string", + "description": "bash script to be executed in a container running the build output image" + } + } + }, "v1.BuildConfigStatus": { "id": "v1.BuildConfigStatus", "required": [ @@ -15335,6 +15348,10 @@ "$ref": "v1.ResourceRequirements", "description": "the desired compute resources the build should have" }, + "postCommit": { + "$ref": "v1.BuildPostCommitSpec", + "description": "an action executed after the build output image is committed" + }, "completionDeadlineSeconds": { "type": "integer", "format": "int64", diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 901eaf046d5a..9196b9fcdd54 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -877,6 +877,11 @@ func deepCopy_api_BuildOutput(in buildapi.BuildOutput, out *buildapi.BuildOutput return nil } +func deepCopy_api_BuildPostCommitSpec(in buildapi.BuildPostCommitSpec, out *buildapi.BuildPostCommitSpec, c *conversion.Cloner) error { + out.RunBash = in.RunBash + return nil +} + func deepCopy_api_BuildRequest(in buildapi.BuildRequest, out *buildapi.BuildRequest, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { return err @@ -1011,6 +1016,9 @@ func deepCopy_api_BuildSpec(in buildapi.BuildSpec, out *buildapi.BuildSpec, c *c } else { out.Resources = newVal.(pkgapi.ResourceRequirements) } + if err := deepCopy_api_BuildPostCommitSpec(in.PostCommit, &out.PostCommit, c); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -2948,6 +2956,7 @@ func init() { deepCopy_api_BuildLog, deepCopy_api_BuildLogOptions, deepCopy_api_BuildOutput, + deepCopy_api_BuildPostCommitSpec, deepCopy_api_BuildRequest, deepCopy_api_BuildSource, deepCopy_api_BuildSpec, diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index edeedbd933c9..66d4e62cdf3d 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -1328,6 +1328,18 @@ func autoconvert_api_BuildOutput_To_v1_BuildOutput(in *buildapi.BuildOutput, out return nil } +func autoconvert_api_BuildPostCommitSpec_To_v1_BuildPostCommitSpec(in *buildapi.BuildPostCommitSpec, out *apiv1.BuildPostCommitSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*buildapi.BuildPostCommitSpec))(in) + } + out.RunBash = in.RunBash + return nil +} + +func convert_api_BuildPostCommitSpec_To_v1_BuildPostCommitSpec(in *buildapi.BuildPostCommitSpec, out *apiv1.BuildPostCommitSpec, s conversion.Scope) error { + return autoconvert_api_BuildPostCommitSpec_To_v1_BuildPostCommitSpec(in, out, s) +} + func autoconvert_api_BuildRequest_To_v1_BuildRequest(in *buildapi.BuildRequest, out *apiv1.BuildRequest, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*buildapi.BuildRequest))(in) @@ -1462,6 +1474,9 @@ func autoconvert_api_BuildSpec_To_v1_BuildSpec(in *buildapi.BuildSpec, out *apiv if err := convert_api_ResourceRequirements_To_v1_ResourceRequirements(&in.Resources, &out.Resources, s); err != nil { return err } + if err := convert_api_BuildPostCommitSpec_To_v1_BuildPostCommitSpec(&in.PostCommit, &out.PostCommit, s); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -2094,6 +2109,18 @@ func autoconvert_v1_BuildOutput_To_api_BuildOutput(in *apiv1.BuildOutput, out *b return nil } +func autoconvert_v1_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in *apiv1.BuildPostCommitSpec, out *buildapi.BuildPostCommitSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*apiv1.BuildPostCommitSpec))(in) + } + out.RunBash = in.RunBash + return nil +} + +func convert_v1_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in *apiv1.BuildPostCommitSpec, out *buildapi.BuildPostCommitSpec, s conversion.Scope) error { + return autoconvert_v1_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in, out, s) +} + func autoconvert_v1_BuildRequest_To_api_BuildRequest(in *apiv1.BuildRequest, out *buildapi.BuildRequest, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*apiv1.BuildRequest))(in) @@ -2229,6 +2256,9 @@ func autoconvert_v1_BuildSpec_To_api_BuildSpec(in *apiv1.BuildSpec, out *buildap if err := convert_v1_ResourceRequirements_To_api_ResourceRequirements(&in.Resources, &out.Resources, s); err != nil { return err } + if err := convert_v1_BuildPostCommitSpec_To_api_BuildPostCommitSpec(&in.PostCommit, &out.PostCommit, s); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -7867,6 +7897,7 @@ func init() { autoconvert_api_BuildLogOptions_To_v1_BuildLogOptions, autoconvert_api_BuildLog_To_v1_BuildLog, autoconvert_api_BuildOutput_To_v1_BuildOutput, + autoconvert_api_BuildPostCommitSpec_To_v1_BuildPostCommitSpec, autoconvert_api_BuildRequest_To_v1_BuildRequest, autoconvert_api_BuildSource_To_v1_BuildSource, autoconvert_api_BuildSpec_To_v1_BuildSpec, @@ -8024,6 +8055,7 @@ func init() { autoconvert_v1_BuildLogOptions_To_api_BuildLogOptions, autoconvert_v1_BuildLog_To_api_BuildLog, autoconvert_v1_BuildOutput_To_api_BuildOutput, + autoconvert_v1_BuildPostCommitSpec_To_api_BuildPostCommitSpec, autoconvert_v1_BuildRequest_To_api_BuildRequest, autoconvert_v1_BuildSource_To_api_BuildSource, autoconvert_v1_BuildSpec_To_api_BuildSpec, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 15f910527342..3dd1ea1ee194 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -901,6 +901,11 @@ func deepCopy_v1_BuildOutput(in apiv1.BuildOutput, out *apiv1.BuildOutput, c *co return nil } +func deepCopy_v1_BuildPostCommitSpec(in apiv1.BuildPostCommitSpec, out *apiv1.BuildPostCommitSpec, c *conversion.Cloner) error { + out.RunBash = in.RunBash + return nil +} + func deepCopy_v1_BuildRequest(in apiv1.BuildRequest, out *apiv1.BuildRequest, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { return err @@ -1036,6 +1041,9 @@ func deepCopy_v1_BuildSpec(in apiv1.BuildSpec, out *apiv1.BuildSpec, c *conversi } else { out.Resources = newVal.(pkgapiv1.ResourceRequirements) } + if err := deepCopy_v1_BuildPostCommitSpec(in.PostCommit, &out.PostCommit, c); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -2842,6 +2850,7 @@ func init() { deepCopy_v1_BuildLog, deepCopy_v1_BuildLogOptions, deepCopy_v1_BuildOutput, + deepCopy_v1_BuildPostCommitSpec, deepCopy_v1_BuildRequest, deepCopy_v1_BuildSource, deepCopy_v1_BuildSpec, diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index 6c5227d2aa67..58f2f5259501 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -1337,6 +1337,18 @@ func autoconvert_api_BuildOutput_To_v1beta3_BuildOutput(in *buildapi.BuildOutput return nil } +func autoconvert_api_BuildPostCommitSpec_To_v1beta3_BuildPostCommitSpec(in *buildapi.BuildPostCommitSpec, out *apiv1beta3.BuildPostCommitSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*buildapi.BuildPostCommitSpec))(in) + } + out.RunBash = in.RunBash + return nil +} + +func convert_api_BuildPostCommitSpec_To_v1beta3_BuildPostCommitSpec(in *buildapi.BuildPostCommitSpec, out *apiv1beta3.BuildPostCommitSpec, s conversion.Scope) error { + return autoconvert_api_BuildPostCommitSpec_To_v1beta3_BuildPostCommitSpec(in, out, s) +} + func autoconvert_api_BuildRequest_To_v1beta3_BuildRequest(in *buildapi.BuildRequest, out *apiv1beta3.BuildRequest, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*buildapi.BuildRequest))(in) @@ -1471,6 +1483,9 @@ func autoconvert_api_BuildSpec_To_v1beta3_BuildSpec(in *buildapi.BuildSpec, out if err := convert_api_ResourceRequirements_To_v1beta3_ResourceRequirements(&in.Resources, &out.Resources, s); err != nil { return err } + if err := convert_api_BuildPostCommitSpec_To_v1beta3_BuildPostCommitSpec(&in.PostCommit, &out.PostCommit, s); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -2103,6 +2118,18 @@ func autoconvert_v1beta3_BuildOutput_To_api_BuildOutput(in *apiv1beta3.BuildOutp return nil } +func autoconvert_v1beta3_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in *apiv1beta3.BuildPostCommitSpec, out *buildapi.BuildPostCommitSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*apiv1beta3.BuildPostCommitSpec))(in) + } + out.RunBash = in.RunBash + return nil +} + +func convert_v1beta3_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in *apiv1beta3.BuildPostCommitSpec, out *buildapi.BuildPostCommitSpec, s conversion.Scope) error { + return autoconvert_v1beta3_BuildPostCommitSpec_To_api_BuildPostCommitSpec(in, out, s) +} + func autoconvert_v1beta3_BuildRequest_To_api_BuildRequest(in *apiv1beta3.BuildRequest, out *buildapi.BuildRequest, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*apiv1beta3.BuildRequest))(in) @@ -2238,6 +2265,9 @@ func autoconvert_v1beta3_BuildSpec_To_api_BuildSpec(in *apiv1beta3.BuildSpec, ou if err := convert_v1beta3_ResourceRequirements_To_api_ResourceRequirements(&in.Resources, &out.Resources, s); err != nil { return err } + if err := convert_v1beta3_BuildPostCommitSpec_To_api_BuildPostCommitSpec(&in.PostCommit, &out.PostCommit, s); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -7834,6 +7864,7 @@ func init() { autoconvert_api_BuildLogOptions_To_v1beta3_BuildLogOptions, autoconvert_api_BuildLog_To_v1beta3_BuildLog, autoconvert_api_BuildOutput_To_v1beta3_BuildOutput, + autoconvert_api_BuildPostCommitSpec_To_v1beta3_BuildPostCommitSpec, autoconvert_api_BuildRequest_To_v1beta3_BuildRequest, autoconvert_api_BuildSource_To_v1beta3_BuildSource, autoconvert_api_BuildSpec_To_v1beta3_BuildSpec, @@ -7991,6 +8022,7 @@ func init() { autoconvert_v1beta3_BuildLogOptions_To_api_BuildLogOptions, autoconvert_v1beta3_BuildLog_To_api_BuildLog, autoconvert_v1beta3_BuildOutput_To_api_BuildOutput, + autoconvert_v1beta3_BuildPostCommitSpec_To_api_BuildPostCommitSpec, autoconvert_v1beta3_BuildRequest_To_api_BuildRequest, autoconvert_v1beta3_BuildSource_To_api_BuildSource, autoconvert_v1beta3_BuildSpec_To_api_BuildSpec, diff --git a/pkg/api/v1beta3/deep_copy_generated.go b/pkg/api/v1beta3/deep_copy_generated.go index 63f1b0fd82b7..acf05b6454ff 100644 --- a/pkg/api/v1beta3/deep_copy_generated.go +++ b/pkg/api/v1beta3/deep_copy_generated.go @@ -909,6 +909,11 @@ func deepCopy_v1beta3_BuildOutput(in apiv1beta3.BuildOutput, out *apiv1beta3.Bui return nil } +func deepCopy_v1beta3_BuildPostCommitSpec(in apiv1beta3.BuildPostCommitSpec, out *apiv1beta3.BuildPostCommitSpec, c *conversion.Cloner) error { + out.RunBash = in.RunBash + return nil +} + func deepCopy_v1beta3_BuildRequest(in apiv1beta3.BuildRequest, out *apiv1beta3.BuildRequest, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { return err @@ -1044,6 +1049,9 @@ func deepCopy_v1beta3_BuildSpec(in apiv1beta3.BuildSpec, out *apiv1beta3.BuildSp } else { out.Resources = newVal.(pkgapiv1beta3.ResourceRequirements) } + if err := deepCopy_v1beta3_BuildPostCommitSpec(in.PostCommit, &out.PostCommit, c); err != nil { + return err + } if in.CompletionDeadlineSeconds != nil { out.CompletionDeadlineSeconds = new(int64) *out.CompletionDeadlineSeconds = *in.CompletionDeadlineSeconds @@ -2832,6 +2840,7 @@ func init() { deepCopy_v1beta3_BuildLog, deepCopy_v1beta3_BuildLogOptions, deepCopy_v1beta3_BuildOutput, + deepCopy_v1beta3_BuildPostCommitSpec, deepCopy_v1beta3_BuildRequest, deepCopy_v1beta3_BuildSource, deepCopy_v1beta3_BuildSpec, diff --git a/pkg/build/api/types.go b/pkg/build/api/types.go index daede0847ddf..17c171fe6d23 100644 --- a/pkg/build/api/types.go +++ b/pkg/build/api/types.go @@ -59,6 +59,10 @@ type BuildSpec struct { // Compute resource requirements to execute the build Resources kapi.ResourceRequirements + // PostCommit is a build hook executed after the build output image is + // committed, before it is pushed to a registry. + PostCommit BuildPostCommitSpec + // Optional duration in seconds, counted from the time when a build pod gets // scheduled in the system, that the build may be active on a node before the // system actively tries to terminate the build; value must be positive integer @@ -388,6 +392,13 @@ type SourceBuildStrategy struct { ForcePull bool } +// BuildPostCommitSpec holds a build post commit hook specification. +type BuildPostCommitSpec struct { + // RunBash holds a bash script to be executed in a container running the + // build output image. + RunBash string +} + // BuildOutput is input to a build strategy and describes the Docker image that the strategy // should produce. type BuildOutput struct { diff --git a/pkg/build/api/v1/types.go b/pkg/build/api/v1/types.go index 924135a3be2d..c967d9d42fd1 100644 --- a/pkg/build/api/v1/types.go +++ b/pkg/build/api/v1/types.go @@ -43,6 +43,10 @@ type BuildSpec struct { // Compute resource requirements to execute the build Resources kapi.ResourceRequirements `json:"resources,omitempty" description:"the desired compute resources the build should have"` + // PostCommit is a build hook executed after the build output image is + // committed, before it is pushed to a registry. + PostCommit BuildPostCommitSpec `json:"postCommit,omitempty" description:"an action executed after the build output image is committed"` + // Optional duration in seconds, counted from the time when a build pod gets // scheduled in the system, that the build may be active on a node before the // system actively tries to terminate the build; value must be positive integer @@ -371,6 +375,13 @@ type SourceBuildStrategy struct { ForcePull bool `json:"forcePull,omitempty" description:"forces the source build to pull the image if true"` } +// BuildPostCommitSpec holds a build post commit hook specification. +type BuildPostCommitSpec struct { + // RunBash holds a bash script to be executed in a container running the + // build output image. + RunBash string `json:"runBash,omitempty" description:"bash script to be executed in a container running the build output image"` +} + // BuildOutput is input to a build strategy and describes the Docker image that the strategy // should produce. type BuildOutput struct { diff --git a/pkg/build/api/v1beta3/types.go b/pkg/build/api/v1beta3/types.go index 894caa9a40ac..bb8f04e28ac2 100644 --- a/pkg/build/api/v1beta3/types.go +++ b/pkg/build/api/v1beta3/types.go @@ -43,6 +43,10 @@ type BuildSpec struct { // Compute resource requirements to execute the build Resources kapi.ResourceRequirements `json:"resources,omitempty" description:"the desired compute resources the build should have"` + // PostCommit is a build hook executed after the build output image is + // committed, before it is pushed to a registry. + PostCommit BuildPostCommitSpec `json:"postCommit,omitempty" description:"an action executed after the build output image is committed"` + // Optional duration in seconds, counted from the time when a build pod gets // scheduled in the system, that the build may be active on a node before the // system actively tries to terminate the build; value must be positive integer @@ -361,6 +365,13 @@ type SourceBuildStrategy struct { ForcePull bool `json:"forcePull,omitempty" description:"forces the source build to pull the image if true"` } +// BuildPostCommitSpec holds a build post commit hook specification. +type BuildPostCommitSpec struct { + // RunBash holds a bash script to be executed in a container running the + // build output image. + RunBash string `json:"runBash,omitempty" description:"bash script to be executed in a container running the build output image"` +} + // BuildOutput is input to a build strategy and describes the Docker image that the strategy // should produce. type BuildOutput struct { diff --git a/pkg/build/builder/common.go b/pkg/build/builder/common.go index e5308d74f5de..4d355c03c746 100644 --- a/pkg/build/builder/common.go +++ b/pkg/build/builder/common.go @@ -1,6 +1,9 @@ package builder import ( + "os" + + "github.com/fsouza/go-dockerclient" "github.com/golang/glog" "github.com/openshift/origin/pkg/build/api" @@ -81,3 +84,45 @@ func updateBuildRevision(c client.BuildInterface, build *api.Build, sourceInfo * glog.Warningf("An error occurred saving build revision: %v", err) } } + +// runScriptInContainer runs a Bash script in an ephemeral Docker container +// started off imageID, and returns an error if the script cannot be run or +// returns a non-zero exit code. +func runScriptInContainer(client DockerClient, script, imageID string) error { + // Read CPU and memory limits from /sys/fs/cgroup. + // These limits are used to enforce limits on the container that runs + // the post commit hook. + r := intFromFileReader{} + cpuShares := r.read("/sys/fs/cgroup/cpu,cpuacct/cpu.shares") + cpuPeriod := r.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us") + cpuQuota := r.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us") + memory := r.read("/sys/fs/cgroup/memory/memory.limit_in_bytes") + if r.err != nil { + return r.err + } + + return dockerRun(client, docker.CreateContainerOptions{ + // TODO set Name to a reasonable value + // (source-to-image/pkg/docker/docker.go doesn't set the Name!) + Name: "", + Config: &docker.Config{ + Image: imageID, + Entrypoint: []string{"bash", "-c"}, + Cmd: []string{script}, + }, + HostConfig: &docker.HostConfig{ + // Limit container's resource allocation. + CPUShares: cpuShares, + CPUPeriod: cpuPeriod, + CPUQuota: cpuQuota, + Memory: memory, + }, + }, docker.LogsOptions{ + // Stream logs to stdout and stderr. + OutputStream: os.Stdout, + ErrorStream: os.Stderr, + Follow: true, + Stdout: true, + Stderr: true, + }) +} diff --git a/pkg/build/builder/docker.go b/pkg/build/builder/docker.go index 22f0f20eb197..5e917eca13d3 100644 --- a/pkg/build/builder/docker.go +++ b/pkg/build/builder/docker.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/docker/distribution/reference" dockercmd "github.com/docker/docker/builder/command" "github.com/docker/docker/builder/parser" docker "github.com/fsouza/go-dockerclient" @@ -23,6 +24,7 @@ import ( "github.com/openshift/origin/pkg/generate/git" imageapi "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/util/docker/dockerfile" + "github.com/openshift/origin/pkg/util/namer" ) // defaultDockerfilePath is the default path of the Dockerfile @@ -53,6 +55,7 @@ func NewDockerBuilder(dockerClient DockerClient, buildsClient client.BuildInterf // Build executes a Docker build func (d *DockerBuilder) Build() error { var push bool + pushTag := d.build.Status.OutputDockerImageReference buildDir, err := ioutil.TempDir("", "docker-build") if err != nil { @@ -77,23 +80,52 @@ func (d *DockerBuilder) Build() error { push = true } - if err := d.dockerBuild(buildDir); err != nil { + // TODO remove the code duplication in docker.go and sti.go. + + // TODO replace this with a truly random string + suffix := fmt.Sprintf("%x", time.Now().UnixNano()) + // buildTag is a random tag used for building the image in such a way + // that we can refer to the built image knowing that it was built here + // and not concurrently by a different goroutine or process. + buildTag := namer.GetName(pushTag, suffix, reference.NameTotalLengthMax) + + if err := d.dockerBuild(buildDir, buildTag); err != nil { return err } - defer removeImage(d.dockerClient, d.build.Status.OutputDockerImageReference) + defer removeImage(d.dockerClient, buildTag) + + if err := d.runPostCommitHook(buildTag); err != nil { + return err + } + + var repo, tag string + if i := strings.LastIndex(pushTag, ":"); i != -1 { + repo = pushTag[:i] + tag = pushTag[i+1:] + } else { + // TODO do not panic... + panic(fmt.Sprintf("build.Status.OutputDockerImageReference is malformed: %q", pushTag)) + } + if err := d.dockerClient.TagImage(buildTag, docker.TagImageOptions{ + Repo: repo, + Tag: tag, + }); err != nil { + return err + } + defer removeImage(d.dockerClient, pushTag) if push { // Get the Docker push authentication pushAuthConfig, authPresent := dockercfg.NewHelper().GetDockerAuth( - d.build.Status.OutputDockerImageReference, + pushTag, dockercfg.PushAuthType, ) if authPresent { glog.V(4).Infof("Authenticating Docker push with user %q", pushAuthConfig.Username) } - glog.Infof("Pushing image %s ...", d.build.Status.OutputDockerImageReference) - if err := pushImage(d.dockerClient, d.build.Status.OutputDockerImageReference, pushAuthConfig); err != nil { + glog.Infof("Pushing image %s ...", pushTag) + if err := pushImage(d.dockerClient, pushTag, pushAuthConfig); err != nil { return fmt.Errorf("Failed to push image: %v", err) } glog.Infof("Push successful") @@ -222,7 +254,7 @@ func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) { } // dockerBuild performs a docker build on the source that has been retrieved -func (d *DockerBuilder) dockerBuild(dir string) error { +func (d *DockerBuilder) dockerBuild(dir string, tag string) error { var noCache bool var forcePull bool dockerfilePath := defaultDockerfilePath @@ -240,7 +272,34 @@ func (d *DockerBuilder) dockerBuild(dir string) error { if err != nil { return err } - return buildImage(d.dockerClient, dir, dockerfilePath, noCache, d.build.Status.OutputDockerImageReference, d.tar, auth, forcePull) + return buildImage(d.dockerClient, dir, dockerfilePath, noCache, tag, d.tar, auth, forcePull) +} + +func (d *DockerBuilder) runPostCommitHook(imageID string) error { + glog.Infof("Running post commit hook with image %s ...", imageID) + return runScriptInContainer(d.dockerClient, d.build.Spec.PostCommit.RunBash, imageID) +} + +// intFromFileReader is an utility to read an integer from files. It records the +// first error encountered, that should be checked after done using it. +type intFromFileReader struct { + err error +} + +// read opens filename and reads an int64. If r.err is not nil, calling read is +// a no-op. +func (r *intFromFileReader) read(filename string) (v int64) { + if r.err != nil { + return + } + var f *os.File + f, r.err = os.Open(filename) + if r.err != nil { + return + } + defer f.Close() + _, r.err = fmt.Fscan(f, &v) + return } // replaceLastFrom changes the last FROM instruction of node to point to the diff --git a/pkg/build/builder/dockerutil.go b/pkg/build/builder/dockerutil.go index b461d1c0c2da..29b7b92a98c2 100644 --- a/pkg/build/builder/dockerutil.go +++ b/pkg/build/builder/dockerutil.go @@ -36,9 +36,14 @@ type DockerClient interface { PushImage(opts docker.PushImageOptions, auth docker.AuthConfiguration) error RemoveImage(name string) error CreateContainer(opts docker.CreateContainerOptions) (*docker.Container, error) + WaitContainer(id string) (int, error) DownloadFromContainer(id string, opts docker.DownloadFromContainerOptions) error PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error RemoveContainer(opts docker.RemoveContainerOptions) error + TagImage(name string, opts docker.TagImageOptions) error + AttachToContainer(opts docker.AttachToContainerOptions) error + StartContainer(id string, hostConfig *docker.HostConfig) error + Logs(opts docker.LogsOptions) error } // pushImage pushes a docker image to the registry specified in its tag. @@ -113,3 +118,60 @@ func buildImage(client DockerClient, dir string, dockerfilePath string, noCache } return client.BuildImage(opts) } + +// dockerRun mimics the 'docker run' command. It uses the Docker Remote API to +// create and start a container and stream its logs to stdout and stderr. The +// container is removed after it terminates. +func dockerRun(client DockerClient, createOpts docker.CreateContainerOptions, logsOpts docker.LogsOptions) error { + // Create a new container. + glog.V(4).Infof("Creating container ...") + c, err := client.CreateContainer(createOpts) + if err != nil { + return err + } + + // Container was created, so we defer its removal. + var rmErr error + defer func() { + glog.V(4).Infof("Removing container %q ...", c.ID) + rmErr = client.RemoveContainer(docker.RemoveContainerOptions{ + ID: c.ID, + }) + if rmErr == nil { + glog.V(4).Infof("Removed container %q", c.ID) + } else { + glog.V(4).Infof("Failed to remove container %q: %v", c.ID, rmErr) + } + }() + + // Start the container. + glog.V(4).Infof("Starting container %q ...", c.ID) + if err := client.StartContainer(c.ID, nil); err != nil { + return err + } + + // Stream container logs. + glog.V(4).Infof("Streaming logs of container %q ...", c.ID) + logsOpts.Container = c.ID + if err := client.Logs(logsOpts); err != nil { + return err + } + + // Return an error if the exit code of the container is non-zero. + glog.V(4).Infof("Waiting for container %q to stop ...", c.ID) + exitCode, err := client.WaitContainer(c.ID) + if err != nil { + return err + } + if exitCode != 0 { + return fmt.Errorf("container returned non-zero exit code: %d", exitCode) + } + + // TODO should we return rmErr or nil? Returning nil means that failing + // to remove the container does not report a failure wrt running the + // post commit hook (which at this point is guaranteed to have been run + // successfully), which might be desirable. The only way to recover from + // a failure to remove the container is retrying here or doing some kind + // of Docker cleanup in a separate process. + return rmErr +} diff --git a/pkg/build/builder/sti.go b/pkg/build/builder/sti.go index de7f0f48cd10..bf3924785913 100644 --- a/pkg/build/builder/sti.go +++ b/pkg/build/builder/sti.go @@ -9,8 +9,11 @@ import ( "net/url" "os" "path/filepath" + "strings" "time" + "github.com/docker/distribution/reference" + "github.com/fsouza/go-dockerclient" "github.com/golang/glog" s2iapi "github.com/openshift/source-to-image/pkg/api" @@ -22,6 +25,7 @@ import ( "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/build/builder/cmd/dockercfg" "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/util/namer" ) // builderFactory is the internal interface to decouple S2I-specific code from Origin builder code @@ -123,7 +127,7 @@ func (s *S2IBuilder) Build() error { } else { push = true } - tag := s.build.Status.OutputDockerImageReference + pushTag := s.build.Status.OutputDockerImageReference git := s.build.Spec.Source.Git var ref string @@ -140,6 +144,13 @@ func (s *S2IBuilder) Build() error { Fragment: ref, } + // TODO replace this with a truly random string + suffix := fmt.Sprintf("%x", time.Now().UnixNano()) + // buildTag is a random tag used for building the image in such a way + // that we can refer to the built image knowing that it was built here + // and not concurrently by a different goroutine or process. + buildTag := namer.GetName(pushTag, suffix, reference.NameTotalLengthMax) + config := &s2iapi.Config{ WorkingDir: buildDir, DockerConfig: &s2iapi.DockerConfig{Endpoint: s.dockerSocket}, @@ -155,7 +166,7 @@ func (s *S2IBuilder) Build() error { DockerNetworkMode: getDockerNetworkMode(), Source: sourceURI.String(), - Tag: tag, + Tag: buildTag, ContextDir: s.build.Spec.Source.ContextDir, } @@ -190,7 +201,7 @@ func (s *S2IBuilder) Build() error { // If DockerCfgPath is provided in api.Config, then attempt to read the the // dockercfg file and get the authentication for pulling the builder image. config.PullAuthentication, _ = dockercfg.NewHelper().GetDockerAuth(config.BuilderImage, dockercfg.PullAuthType) - config.IncrementalAuthentication, _ = dockercfg.NewHelper().GetDockerAuth(tag, dockercfg.PushAuthType) + config.IncrementalAuthentication, _ = dockercfg.NewHelper().GetDockerAuth(pushTag, dockercfg.PushAuthType) glog.V(2).Infof("Creating a new S2I builder with build config: %#v\n", describe.DescribeConfig(config)) builder, err := s.builder.Builder(config, s2ibuild.Overrides{Downloader: download}) @@ -204,19 +215,42 @@ func (s *S2IBuilder) Build() error { return err } + // TODO: why in docker builds we defer removing the image, while here we don't?! + // defer removeImage(s.dockerClient, buildTag) + + if err := s.runPostCommitHook(buildTag); err != nil { + return err + } + + var repo, tag string + if i := strings.LastIndex(pushTag, ":"); i != -1 { + repo = pushTag[:i] + tag = pushTag[i+1:] + } else { + // TODO do not panic... + panic(fmt.Sprintf("build.Status.OutputDockerImageReference is malformed: %q", pushTag)) + } + if err := s.dockerClient.TagImage(buildTag, docker.TagImageOptions{ + Repo: repo, + Tag: tag, + }); err != nil { + return err + } + defer removeImage(s.dockerClient, pushTag) + if push { // Get the Docker push authentication pushAuthConfig, authPresent := dockercfg.NewHelper().GetDockerAuth( - tag, + pushTag, dockercfg.PushAuthType, ) if authPresent { - glog.Infof("Using provided push secret for pushing %s image", tag) + glog.Infof("Using provided push secret for pushing %s image", pushTag) } else { glog.Infof("No push secret provided") } - glog.Infof("Pushing %s image ...", tag) - if err := pushImage(s.dockerClient, tag, pushAuthConfig); err != nil { + glog.Infof("Pushing %s image ...", pushTag) + if err := pushImage(s.dockerClient, pushTag, pushAuthConfig); err != nil { // write extended error message to assist in problem resolution msg := fmt.Sprintf("Failed to push image. Response from registry is: %v", err) if authPresent { @@ -231,12 +265,17 @@ func (s *S2IBuilder) Build() error { } return errors.New(msg) } - glog.Infof("Successfully pushed %s", tag) + glog.Infof("Successfully pushed %s", pushTag) glog.Flush() } return nil } +func (s *S2IBuilder) runPostCommitHook(imageID string) error { + glog.Infof("Running post commit hook with image %s ...", imageID) + return runScriptInContainer(s.dockerClient, s.build.Spec.PostCommit.RunBash, imageID) +} + type downloader struct { s *S2IBuilder in io.Reader diff --git a/pkg/build/generator/generator.go b/pkg/build/generator/generator.go index 5791bc2f8ae0..806fe8ee82be 100644 --- a/pkg/build/generator/generator.go +++ b/pkg/build/generator/generator.go @@ -376,6 +376,7 @@ func (g *BuildGenerator) generateBuildFromConfig(ctx kapi.Context, bc *buildapi. Output: bcCopy.Spec.Output, Revision: revision, Resources: bcCopy.Spec.Resources, + PostCommit: bcCopy.Spec.PostCommit, CompletionDeadlineSeconds: bcCopy.Spec.CompletionDeadlineSeconds, }, ObjectMeta: kapi.ObjectMeta{