diff --git a/pkg/build/api/types.go b/pkg/build/api/types.go index 6b2ebb913c7a..2909e95e4a59 100644 --- a/pkg/build/api/types.go +++ b/pkg/build/api/types.go @@ -44,23 +44,23 @@ type BuildStatus string // Valid values for BuildStatus. const ( - // BuildNew is automatically assigned to a newly created build. + // BuildStatusNew is automatically assigned to a newly created build. BuildStatusNew BuildStatus = "New" - // BuildPending indicates that a pod name has been assigned and a build is + // BuildStatusPending indicates that a pod name has been assigned and a build is // about to start running. BuildStatusPending BuildStatus = "Pending" - // BuildRunning indicates that a pod has been created and a build is running. + // BuildStatusRunning indicates that a pod has been created and a build is running. BuildStatusRunning BuildStatus = "Running" - // BuildComplete indicates that a build has been successful. + // BuildStatusComplete indicates that a build has been successful. BuildStatusComplete BuildStatus = "Complete" - // BuildFailed indicates that a build has executed and failed. + // BuildStatusFailed indicates that a build has executed and failed. BuildStatusFailed BuildStatus = "Failed" - // BuildError indicates that an error prevented the build from executing. + // BuildStatusError indicates that an error prevented the build from executing. BuildStatusError BuildStatus = "Error" // BuildStatusCancelled indicates that a running/pending build was stopped from executing. @@ -72,7 +72,7 @@ type BuildSourceType string // Valid values for BuildSourceType. const ( - //BuildGitSource is a Git SCM + //BuildSourceGit is a Git SCM BuildSourceGit BuildSourceType = "Git" ) @@ -150,6 +150,15 @@ const ( CustomBuildStrategyType BuildStrategyType = "Custom" ) +const ( + // CustomBuildStrategyBaseImageKey is the environment variable that indicates the base image to be used when + // performing a custom build, if needed. + CustomBuildStrategyBaseImageKey = "OPENSHIFT_CUSTOM_BUILD_BASE_IMAGE" + + // DefaultImageTag is used when an image tag is needed and the configuration + DefaultImageTag string = "latest" +) + // CustomBuildStrategy defines input parameters specific to Custom build. type CustomBuildStrategy struct { // Image is the image required to execute the build. If not specified @@ -175,6 +184,11 @@ type DockerBuildStrategy struct { // NoCache if set to true indicates that the docker build must be executed with the // --no-cache=true flag NoCache bool `json:"noCache,omitempty" yaml:"noCache,omitempty"` + + // BaseImage is optional and indicates the image that the dockerfile for this + // build should "FROM". If present, the build process will substitute this value + // into the FROM line of the dockerfile. + BaseImage string `json:"baseImage,omitempty" yaml:"baseImage,omitempty"` } // STIBuildStrategy defines input parameters specific to an STI build. @@ -222,6 +236,17 @@ type WebHookTrigger struct { Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` } +// ImageChangeTrigger allows builds to be triggered when an ImageRepository changes +type ImageChangeTrigger struct { + // Image is used to specify the value in the BuildConfig to replace with the + // immutable image id supplied by the ImageRepository when this trigger fires. + Image string `json:"image" yaml:"image"` + // ImageRepositoryRef a reference to a Docker image repository to watch for changes. + ImageRepositoryRef *kapi.ObjectReference `json:"imageRepositoryRef" yaml:"imageRepositoryRef"` + // Tag is the name of an image repository tag to watch for changes. + Tag string `json:"tag,omitempty" yaml:"tag,omitempty"` +} + // BuildTriggerPolicy describes a policy for a single trigger that results in a new Build. type BuildTriggerPolicy struct { // Type is the type of build trigger @@ -232,6 +257,9 @@ type BuildTriggerPolicy struct { // GenericWebHook contains the parameters for a Generic webhook type of trigger GenericWebHook *WebHookTrigger `json:"generic,omitempty" yaml:"generic,omitempty"` + + // ImageChange contains parameters for an ImageChange type of trigger + ImageChange *ImageChangeTrigger `json:"imageChange,omitempty" yaml:"imageChange,omitempty"` } // BuildTriggerType refers to a specific BuildTriggerPolicy implementation. @@ -240,11 +268,15 @@ type BuildTriggerType string const ( // GithubWebHookType represents a trigger that launches builds on // Github webhook invocations - GithubWebHookType BuildTriggerType = "github" + GithubWebHookBuildTriggerType BuildTriggerType = "github" // GenericWebHookType represents a trigger that launches builds on // generic webhook invocations - GenericWebHookType BuildTriggerType = "generic" + GenericWebHookBuildTriggerType BuildTriggerType = "generic" + + // ImageChangeType represents a trigger that launches builds on + // availability of a new version of an image + ImageChangeBuildTriggerType BuildTriggerType = "imageChange" ) // BuildList is a collection of Builds. diff --git a/pkg/build/api/v1beta1/types.go b/pkg/build/api/v1beta1/types.go index 3e7a3b36bde1..8259f2d08870 100644 --- a/pkg/build/api/v1beta1/types.go +++ b/pkg/build/api/v1beta1/types.go @@ -44,23 +44,23 @@ type BuildStatus string // Valid values for BuildStatus. const ( - // BuildNew is automatically assigned to a newly created build. + // BuildStatusNew is automatically assigned to a newly created build. BuildStatusNew BuildStatus = "New" - // BuildPending indicates that a pod name has been assigned and a build is + // BuildStatusPending indicates that a pod name has been assigned and a build is // about to start running. BuildStatusPending BuildStatus = "Pending" - // BuildRunning indicates that a pod has been created and a build is running. + // BuildStatusRunning indicates that a pod has been created and a build is running. BuildStatusRunning BuildStatus = "Running" - // BuildComplete indicates that a build has been successful. BuildStatusComplete BuildStatus = "Complete" + // BuildStatusComplete indicates that a build has been successful. - // BuildFailed indicates that a build has executed and failed. + // BuildStatusFailed indicates that a build has executed and failed. BuildStatusFailed BuildStatus = "Failed" - // BuildError indicates that an error prevented the build from executing. + // BuildStatusError indicates that an error prevented the build from executing. BuildStatusError BuildStatus = "Error" // BuildStatusCancelled indicates that a running/pending build was stopped from executing. @@ -72,7 +72,7 @@ type BuildSourceType string // Valid values for BuildSourceType. const ( - //BuildGitSource is a Git SCM + //BuildSourceGit is a Git SCM BuildSourceGit BuildSourceType = "Git" ) @@ -175,6 +175,11 @@ type DockerBuildStrategy struct { // NoCache if set to true indicates that the docker build must be executed with the // --no-cache=true flag NoCache bool `json:"noCache,omitempty" yaml:"noCache,omitempty"` + + // BaseImage is optional and indicates the image that the dockerfile for this + // build should "FROM". If present, the build process will substitute this value + // into the FROM line of the dockerfile. + BaseImage string `json:"baseImage,omitempty" yaml:"baseImage,omitempty"` } // STIBuildStrategy defines input parameters specific to an STI build. @@ -226,6 +231,17 @@ type WebHookTrigger struct { Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` } +// ImageChangeTrigger allows builds to be triggered when an ImageRepository changes +type ImageChangeTrigger struct { + // Image is used to specify the value in the BuildConfig to replace with the + // immutable image id supplied by the ImageRepository when this trigger fires. + Image string `json:"image" yaml:"image"` + // ImageRepositoryRef a reference to a Docker image repository to watch for changes. + ImageRepositoryRef *kapi.ObjectReference `json:"imageRepositoryRef" yaml:"imageRepositoryRef"` + // Tag is the name of an image repository tag to watch for changes. + Tag string `json:"tag,omitempty" yaml:"tag,omitempty"` +} + // BuildTriggerPolicy describes a policy for a single trigger that results in a new Build. type BuildTriggerPolicy struct { // Type is the type of build trigger @@ -236,6 +252,9 @@ type BuildTriggerPolicy struct { // GenericWebHook contains the parameters for a Generic webhook type of trigger GenericWebHook *WebHookTrigger `json:"generic,omitempty" yaml:"generic,omitempty"` + + // ImageChange contains parameters for an ImageChange type of trigger + ImageChange *ImageChangeTrigger `json:"imageChange,omitempty" yaml:"imageChange,omitempty"` } // BuildTriggerType refers to a specific BuildTriggerPolicy implementation. diff --git a/pkg/build/api/validation/validation.go b/pkg/build/api/validation/validation.go index d58cbd933de3..f2aab7e6f9d6 100644 --- a/pkg/build/api/validation/validation.go +++ b/pkg/build/api/validation/validation.go @@ -138,25 +138,31 @@ func validateTrigger(trigger *buildapi.BuildTriggerPolicy) errs.ValidationErrorL // Ensure that only parameters for the trigger's type are present triggerPresence := map[buildapi.BuildTriggerType]bool{ - buildapi.GithubWebHookType: trigger.GithubWebHook != nil, - buildapi.GenericWebHookType: trigger.GenericWebHook != nil, + buildapi.GithubWebHookBuildTriggerType: trigger.GithubWebHook != nil, + buildapi.GenericWebHookBuildTriggerType: trigger.GenericWebHook != nil, } allErrs = append(allErrs, validateTriggerPresence(triggerPresence, trigger.Type)...) // Validate each trigger type switch trigger.Type { - case buildapi.GithubWebHookType: + case buildapi.GithubWebHookBuildTriggerType: if trigger.GithubWebHook == nil { allErrs = append(allErrs, errs.NewFieldRequired("github", nil)) } else { allErrs = append(allErrs, validateWebHook(trigger.GithubWebHook).Prefix("github")...) } - case buildapi.GenericWebHookType: + case buildapi.GenericWebHookBuildTriggerType: if trigger.GenericWebHook == nil { allErrs = append(allErrs, errs.NewFieldRequired("generic", nil)) } else { allErrs = append(allErrs, validateWebHook(trigger.GenericWebHook).Prefix("generic")...) } + case buildapi.ImageChangeBuildTriggerType: + if trigger.ImageChange == nil { + allErrs = append(allErrs, errs.NewFieldRequired("imageChange", nil)) + } else { + allErrs = append(allErrs, validateImageChange(trigger.ImageChange).Prefix("imageChange")...) + } } return allErrs } @@ -171,6 +177,21 @@ func validateTriggerPresence(params map[buildapi.BuildTriggerType]bool, t builda return allErrs } +func validateImageChange(imageChange *buildapi.ImageChangeTrigger) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(imageChange.Image) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("image", "")) + } + if imageChange.ImageRepositoryRef == nil { + allErrs = append(allErrs, errs.NewFieldRequired("imageRepositoryRef", "")) + } else if len(imageChange.ImageRepositoryRef.Name) == 0 { + nestedErrs := errs.ValidationErrorList{errs.NewFieldRequired("name", "")} + nestedErrs.Prefix("imageRepositoryRef") + allErrs = append(allErrs, nestedErrs...) + } + return allErrs +} + func validateWebHook(webHook *buildapi.WebHookTrigger) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if len(webHook.Secret) == 0 { diff --git a/pkg/build/api/validation/validation_test.go b/pkg/build/api/validation/validation_test.go index 0b2101b76b12..7bc991685d29 100644 --- a/pkg/build/api/validation/validation_test.go +++ b/pkg/build/api/validation/validation_test.go @@ -204,19 +204,19 @@ func TestValidateTrigger(t *testing.T) { expected: []*errs.ValidationError{errs.NewFieldRequired("type", "")}, }, "github type with no github webhook": { - trigger: buildapi.BuildTriggerPolicy{Type: buildapi.GithubWebHookType}, + trigger: buildapi.BuildTriggerPolicy{Type: buildapi.GithubWebHookBuildTriggerType}, expected: []*errs.ValidationError{errs.NewFieldRequired("github", "")}, }, "github trigger with no secret": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{}, }, expected: []*errs.ValidationError{errs.NewFieldRequired("github.secret", "")}, }, "github trigger with generic webhook": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GenericWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, @@ -224,19 +224,19 @@ func TestValidateTrigger(t *testing.T) { expected: []*errs.ValidationError{errs.NewFieldInvalid("generic", "", "long description")}, }, "generic trigger with no generic webhook": { - trigger: buildapi.BuildTriggerPolicy{Type: buildapi.GenericWebHookType}, + trigger: buildapi.BuildTriggerPolicy{Type: buildapi.GenericWebHookBuildTriggerType}, expected: []*errs.ValidationError{errs.NewFieldRequired("generic", "")}, }, "generic trigger with no secret": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GenericWebHookType, + Type: buildapi.GenericWebHookBuildTriggerType, GenericWebHook: &buildapi.WebHookTrigger{}, }, expected: []*errs.ValidationError{errs.NewFieldRequired("generic.secret", "")}, }, "generic trigger with github webhook": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GenericWebHookType, + Type: buildapi.GenericWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, @@ -245,7 +245,7 @@ func TestValidateTrigger(t *testing.T) { }, "valid github trigger": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, @@ -253,7 +253,7 @@ func TestValidateTrigger(t *testing.T) { }, "valid generic trigger": { trigger: buildapi.BuildTriggerPolicy{ - Type: buildapi.GenericWebHookType, + Type: buildapi.GenericWebHookBuildTriggerType, GenericWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, diff --git a/pkg/build/builder/docker.go b/pkg/build/builder/docker.go index 6da46b2af903..22f5bdd91899 100644 --- a/pkg/build/builder/docker.go +++ b/pkg/build/builder/docker.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "time" @@ -21,6 +22,9 @@ 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(`^FROM\s+\w+.+`) + // DockerBuilder builds Docker images given a git repository URL type DockerBuilder struct { dockerClient DockerClient @@ -54,10 +58,10 @@ func (d *DockerBuilder) Build() error { if err = d.fetchSource(buildDir); err != nil { return err } - if err = d.dockerBuild(buildDir); err != nil { + if err = d.addBuildParameters(buildDir); err != nil { return err } - if err = d.addImageVars(); err != nil { + if err = d.dockerBuild(buildDir); err != nil { return err } if d.build.Parameters.Output.Registry != "" || d.authPresent { @@ -125,40 +129,52 @@ func (d *DockerBuilder) fetchSource(dir string) error { return d.git.Checkout(dir, d.build.Parameters.Source.Git.Ref) } -// dockerBuild performs a docker build on the source that has been retrieved -func (d *DockerBuilder) dockerBuild(dir string) error { - var noCache bool - if d.build.Parameters.Strategy.DockerStrategy != nil { - if d.build.Parameters.Strategy.DockerStrategy.ContextDir != "" { - dir = filepath.Join(dir, d.build.Parameters.Strategy.DockerStrategy.ContextDir) - } - noCache = d.build.Parameters.Strategy.DockerStrategy.NoCache - } - return buildImage(d.dockerClient, dir, noCache, imageTag(d.build), d.tar) -} +// addBuildParameters checks if a BaseImage 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 in the Dockerfile. +func (d *DockerBuilder) addBuildParameters(dir string) error { + dockerfilePath := filepath.Join(dir, d.build.Parameters.Strategy.DockerStrategy.ContextDir, "Dockerfile") -// addImageVars creates a new Dockerfile which adds certain environment -// variables to the previously tagged image -func (d *DockerBuilder) addImageVars() error { - var noCache bool - envVars := getBuildEnvVars(d.build) - tempDir, err := ioutil.TempDir("", "overlay") + fileStat, err := os.Lstat(dockerfilePath) + filePerm := fileStat.Mode() + + fileData, err := ioutil.ReadFile(dockerfilePath) if err != nil { return err } - overlay, err := os.Create(filepath.Join(tempDir, "Dockerfile")) - if err != nil { - return err + + 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)) + } else { + newFileData = newFileData + string(fileData) } - overlay.WriteString(fmt.Sprintf("FROM %s\n", imageTag(d.build))) + + envVars := getBuildEnvVars(d.build) for k, v := range envVars { - overlay.WriteString(fmt.Sprintf("ENV %s %s\n", k, v)) + newFileData = newFileData + fmt.Sprintf("ENV %s %s\n", k, v) } - if err = overlay.Close(); err != nil { + + err = ioutil.WriteFile(dockerfilePath, []byte(newFileData), filePerm) + if err != nil { return err } + + var noCache bool if d.build.Parameters.Strategy.DockerStrategy != nil { noCache = d.build.Parameters.Strategy.DockerStrategy.NoCache } - return buildImage(d.dockerClient, tempDir, noCache, imageTag(d.build), d.tar) + return buildImage(d.dockerClient, dir, noCache, imageTag(d.build), d.tar) +} + +// dockerBuild performs a docker build on the source that has been retrieved +func (d *DockerBuilder) dockerBuild(dir string) error { + var noCache bool + if d.build.Parameters.Strategy.DockerStrategy != nil { + if d.build.Parameters.Strategy.DockerStrategy.ContextDir != "" { + dir = filepath.Join(dir, d.build.Parameters.Strategy.DockerStrategy.ContextDir) + } + noCache = d.build.Parameters.Strategy.DockerStrategy.NoCache + } + return buildImage(d.dockerClient, dir, noCache, imageTag(d.build), d.tar) } diff --git a/pkg/build/controller/factory/factory.go b/pkg/build/controller/factory/factory.go index b0c9d5785e88..276c245354d9 100644 --- a/pkg/build/controller/factory/factory.go +++ b/pkg/build/controller/factory/factory.go @@ -16,7 +16,9 @@ import ( buildapi "github.com/openshift/origin/pkg/build/api" controller "github.com/openshift/origin/pkg/build/controller" strategy "github.com/openshift/origin/pkg/build/controller/strategy" + buildutil "github.com/openshift/origin/pkg/build/util" osclient "github.com/openshift/origin/pkg/client" + imageapi "github.com/openshift/origin/pkg/image/api" ) type BuildControllerFactory struct { @@ -65,6 +67,35 @@ func (factory *BuildControllerFactory) Create() *controller.BuildController { } } +// ImageChangeControllerFactory can create an ImageChangeController which obtains ImageRepositories +// from a queue populated from a watch of all ImageRepositories. +type ImageChangeControllerFactory struct { + Client *osclient.Client + // Stop may be set to allow controllers created by this factory to be terminated. + Stop <-chan struct{} +} + +// Create creates a new ImageChangeController which is used to trigger builds when a new +// image is available +func (factory *ImageChangeControllerFactory) Create() *controller.ImageChangeController { + queue := cache.NewFIFO() + cache.NewReflector(&imageRepositoryLW{factory.Client}, &imageapi.ImageRepository{}, queue).Run() + + store := cache.NewStore() + cache.NewReflector(&buildConfigLW{client: factory.Client}, &buildapi.BuildConfig{}, store).Run() + + return &controller.ImageChangeController{ + BuildConfigStore: store, + BuildCreator: &ClientBuildCreator{factory.Client}, + NextImageRepository: func() *imageapi.ImageRepository { + repo := queue.Pop().(*imageapi.ImageRepository) + panicIfStopped(factory.Stop, "build image change controller stopped") + return repo + }, + Stop: factory.Stop, + } +} + // pollPods lists pods for all builds in the buildStore which are pending or running and // returns an enumerator for cache.Poller. The poll scope is narrowed for efficiency. func (factory *BuildControllerFactory) pollPods() (cache.Enumerator, error) { @@ -124,6 +155,15 @@ func (f *typeBasedFactoryStrategy) CreateBuildPod(build *buildapi.Build) (*kapi. } } +// panicIfStopped panics with the provided object if the channel is closed +func panicIfStopped(ch <-chan struct{}, message interface{}) { + select { + case <-ch: + panic(message) + default: + } +} + // buildLW is a ListWatcher implementation for Builds. type buildLW struct { client osclient.Interface @@ -139,6 +179,36 @@ func (lw *buildLW) Watch(resourceVersion string) (watch.Interface, error) { return lw.client.Builds(kapi.NamespaceAll).Watch(labels.Everything(), labels.Everything(), "0") } +// buildConfigLW is a ListWatcher implementation for BuildConfigs. +type buildConfigLW struct { + client osclient.Interface +} + +// List lists all BuildConfigs. +func (lw *buildConfigLW) List() (runtime.Object, error) { + return lw.client.BuildConfigs(kapi.NamespaceAll).List(labels.Everything(), labels.Everything()) +} + +// Watch watches all BuildConfigs. +func (lw *buildConfigLW) Watch(resourceVersion string) (watch.Interface, error) { + return lw.client.BuildConfigs(kapi.NamespaceAll).Watch(labels.Everything(), labels.Everything(), "0") +} + +// imageRepositoryLW is a ListWatcher for ImageRepositories. +type imageRepositoryLW struct { + client osclient.Interface +} + +// List lists all ImageRepositories. +func (lw *imageRepositoryLW) List() (runtime.Object, error) { + return lw.client.ImageRepositories(kapi.NamespaceAll).List(labels.Everything(), labels.Everything()) +} + +// Watch watches all ImageRepositories. +func (lw *imageRepositoryLW) Watch(resourceVersion string) (watch.Interface, error) { + return lw.client.ImageRepositories(kapi.NamespaceAll).Watch(labels.Everything(), labels.Everything(), "0") +} + // ClientPodManager is a PodManager which delegates to the Kubernetes client interface. type ClientPodManager struct { KubeClient kclient.Interface @@ -159,6 +229,24 @@ func (c ClientBuildUpdater) UpdateBuild(namespace string, build *buildapi.Build) return c.Client.Builds(namespace).Update(build) } +// ClientBuildCreator is a buildCreator which delegates to the OpenShift client interfaces +type ClientBuildCreator struct { + Client osclient.Interface +} + +// UpdateBuild updates build using the OpenShift client. +func (c *ClientBuildCreator) CreateBuild(config *buildapi.BuildConfig, imageSubstitutions map[string]string) error { + build := buildutil.GenerateBuildFromConfig(config, nil) + for originalImage, newImage := range imageSubstitutions { + buildutil.SubstituteImageReferences(build, originalImage, newImage) + } + if _, err := c.Client.Builds(config.Namespace).Create(build); err != nil { + glog.V(2).Infof("Error creating build for buildConfig %v: %v", config.Name, err) + return err + } + return nil +} + // DeletePod destroys a pod using the Kubernetes client. func (c ClientPodManager) DeletePod(namespace string, pod *kapi.Pod) error { return c.KubeClient.Pods(namespace).Delete(pod.Name) diff --git a/pkg/build/controller/image_change_controller.go b/pkg/build/controller/image_change_controller.go new file mode 100644 index 000000000000..b87996ba0f29 --- /dev/null +++ b/pkg/build/controller/image_change_controller.go @@ -0,0 +1,75 @@ +package controller + +import ( + "github.com/golang/glog" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + buildapi "github.com/openshift/origin/pkg/build/api" + imageapi "github.com/openshift/origin/pkg/image/api" +) + +// ImageChangeController watches for changes to ImageRepositories and triggers +// builds when a new version of a tag referenced by a BuildConfig +// is available. +type ImageChangeController struct { + NextImageRepository func() *imageapi.ImageRepository + BuildConfigStore cache.Store + BuildCreator buildCreator + // Stop is an optional channel that controls when the controller exits + Stop <-chan struct{} +} + +type buildCreator interface { + CreateBuild(build *buildapi.BuildConfig, imageSubstitutions map[string]string) error +} + +// Run processes ImageRepository events one by one. +func (c *ImageChangeController) Run() { + go util.Until(c.HandleImageRepo, 0, c.Stop) +} + +// HandleImageRepo processes the next ImageRepository event. +func (c *ImageChangeController) HandleImageRepo() { + glog.V(4).Infof("Waiting for imagerepo change") + imageRepo := c.NextImageRepository() + glog.V(4).Infof("Build image change controller detected imagerepo change %s", imageRepo.DockerImageRepository) + imageSubstitutions := make(map[string]string) + + for _, bc := range c.BuildConfigStore.List() { + config := bc.(*buildapi.BuildConfig) + glog.V(4).Infof("Detecting changed images for buildConfig %s", config.Name) + + // Extract relevant triggers for this imageRepo for this config + var triggerForConfig *buildapi.ImageChangeTrigger + for _, trigger := range config.Triggers { + // for every ImageChange trigger, record the image it substitutes for and get the latest + // image id from the imagerepository. We will substitute all images in the buildconfig + // with the latest values from the imagerepositories. + if trigger.Type == buildapi.ImageChangeBuildTriggerType { + // TODO: we don't really want to create a build for a buildconfig based the "test" tag if the "prod" tag is what just got + // updated, but ImageRepository doesn't give us that granularity today, so the only way to avoid these spurious builds is + // to check if the new imageid is different from the last time we built this buildcfg. Need to add this check. + // Will be effectively identical the logic needed on startup to spin new builds only if we missed a new image event. + var tag string + if tag = trigger.ImageChange.Tag; len(tag) == 0 { + tag = buildapi.DefaultImageTag + } + if repoImageID, repoHasTag := imageRepo.Tags[tag]; repoHasTag { + imageSubstitutions[trigger.ImageChange.Image] = imageRepo.DockerImageRepository + ":" + repoImageID + } + if trigger.ImageChange.ImageRepositoryRef.Name == imageRepo.Name { + triggerForConfig = trigger.ImageChange + } + } + } + + if triggerForConfig != nil { + glog.V(4).Infof("Running build for buildConfig %s", config.Name) + if err := c.BuildCreator.CreateBuild(config, imageSubstitutions); err != nil { + glog.V(2).Infof("Error starting build for buildConfig %v: %v", config.Name, err) + } + } + } +} diff --git a/pkg/build/controller/image_change_controller_test.go b/pkg/build/controller/image_change_controller_test.go new file mode 100644 index 000000000000..fe8ce7541704 --- /dev/null +++ b/pkg/build/controller/image_change_controller_test.go @@ -0,0 +1,121 @@ +package controller + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + buildapi "github.com/openshift/origin/pkg/build/api" + buildtest "github.com/openshift/origin/pkg/build/controller/test" + imageapi "github.com/openshift/origin/pkg/image/api" +) + +type mockBuildCreator struct { + buildcfg *buildapi.BuildConfig + imageSubstitutions map[string]string +} + +func (m *mockBuildCreator) CreateBuild(buildcfg *buildapi.BuildConfig, imageSubstitutions map[string]string) error { + m.buildcfg = buildcfg + m.imageSubstitutions = imageSubstitutions + return nil +} + +func mockBuildConfig(baseImage string, triggerImage string, repoName string, repoTag string) (buildcfg *buildapi.BuildConfig) { + buildcfg = &buildapi.BuildConfig{ + ObjectMeta: kapi.ObjectMeta{ + Name: "testBuildCfg", + }, + Parameters: buildapi.BuildParameters{ + Strategy: buildapi.BuildStrategy{ + Type: buildapi.DockerBuildStrategyType, + DockerStrategy: &buildapi.DockerBuildStrategy{ + ContextDir: "contextimage", + BaseImage: baseImage, + }, + }, + }, + Triggers: []buildapi.BuildTriggerPolicy{ + { + Type: buildapi.ImageChangeBuildTriggerType, + ImageChange: &buildapi.ImageChangeTrigger{ + Image: triggerImage, + ImageRepositoryRef: &kapi.ObjectReference{ + Name: repoName, + }, + Tag: repoTag, + }, + }, + }, + } + return +} + +func mockImageChangeController(buildcfg *buildapi.BuildConfig, repoName string, dockerImageRepo string, tags map[string]string) (controller *ImageChangeController) { + + imageRepo := imageapi.ImageRepository{ + ObjectMeta: kapi.ObjectMeta{ + Name: repoName, + }, + DockerImageRepository: dockerImageRepo, + Tags: tags, + } + + controller = &ImageChangeController{ + NextImageRepository: func() *imageapi.ImageRepository { return &imageRepo }, + BuildConfigStore: buildtest.NewFakeBuildConfigStore(buildcfg), + BuildCreator: &mockBuildCreator{}, + } + return +} + +func TestHandleImageRepo(t *testing.T) { + + // valid configuration, new build should be triggered. + buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "test") + controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"test": "newImageId123"}) + controller.HandleImageRepo() + buildCreator := controller.BuildCreator.(*mockBuildCreator) + if buildCreator.buildcfg == nil { + t.Errorf("New build not created when new image was created") + } + if buildCreator.imageSubstitutions["registry.com/namespace/imagename"] != "registry.com/namespace/imagename:newImageId123" { + t.Errorf("Image substitutions not properly setup for new build: %s |", buildCreator.imageSubstitutions["registry.com/namespace/imagename"]) + } + + // valid configuration using default latest tag, new build should be triggered. + buildcfg = mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "") + controller = mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"latest": "newImageId123"}) + controller.HandleImageRepo() + buildCreator = controller.BuildCreator.(*mockBuildCreator) + if buildCreator.buildcfg == nil { + t.Errorf("New build not created when new image was created") + } + if buildCreator.imageSubstitutions["registry.com/namespace/imagename"] != "registry.com/namespace/imagename:newImageId123" { + t.Errorf("Image substitutions not properly setup for new build using default latest tag: %s |", buildCreator.imageSubstitutions["registry.com/namespace/imagename"]) + } + + // this buildconfig references a non-existent imagerepo, so an update to the real imagerepo should not + // trigger a build here. + buildcfg = mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo2", "test") + controller = mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"test": "newImageId123"}) + controller.HandleImageRepo() + buildCreator = controller.BuildCreator.(*mockBuildCreator) + if buildCreator.buildcfg != nil { + t.Errorf("New build created when a different repository was updated") + } + + // this buildconfig references a different tag than the one that will be updated, this will (for now) result + // in a build being triggered, but there should be no image name substitution since the imagerepo does not contain + // a valid imageid for the "test2" tag, so we should use the existing image name in the buildconfig. + buildcfg = mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "test2") + controller = mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"test": "newImageId123"}) + controller.HandleImageRepo() + buildCreator = controller.BuildCreator.(*mockBuildCreator) + if buildCreator.buildcfg == nil { + t.Errorf("New build not created when a different repository tag was updated") + } + if len(buildCreator.imageSubstitutions) != 0 { + t.Errorf("Should not have had any image substitutions since tag does not exist in imagerepo") + } +} diff --git a/pkg/build/controller/test/fake_build_config_store.go b/pkg/build/controller/test/fake_build_config_store.go new file mode 100644 index 000000000000..d2e4a7a26507 --- /dev/null +++ b/pkg/build/controller/test/fake_build_config_store.go @@ -0,0 +1,41 @@ +package test + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + buildapi "github.com/openshift/origin/pkg/build/api" +) + +type FakeBuildConfigStore struct { + BuildConfig *buildapi.BuildConfig +} + +func NewFakeBuildConfigStore(buildcfg *buildapi.BuildConfig) FakeBuildConfigStore { + return FakeBuildConfigStore{buildcfg} +} + +func (s FakeBuildConfigStore) Add(id string, obj interface{}) { +} + +func (s FakeBuildConfigStore) Update(id string, obj interface{}) { +} + +func (s FakeBuildConfigStore) Delete(id string) { +} + +func (s FakeBuildConfigStore) List() []interface{} { + return []interface{}{s.BuildConfig} +} + +func (s FakeBuildConfigStore) ContainedIDs() util.StringSet { + return util.NewStringSet() +} + +func (s FakeBuildConfigStore) Get(id string) (item interface{}, exists bool) { + if s.BuildConfig == nil { + return nil, false + } + + return s.BuildConfig, true +} + +func (s FakeBuildConfigStore) Replace(idToObj map[string]interface{}) {} diff --git a/pkg/build/util/generate.go b/pkg/build/util/generate.go index c0eb9108f642..07697f7ca8c2 100644 --- a/pkg/build/util/generate.go +++ b/pkg/build/util/generate.go @@ -1,6 +1,8 @@ package util import ( + "github.com/golang/glog" + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/openshift/origin/pkg/build/api" ) @@ -30,3 +32,44 @@ func GenerateBuildFromBuild(build *api.Build) *api.Build { }, } } + +// SubstituteImageReferences replaces references to an image with a new value +func SubstituteImageReferences(build *api.Build, oldImage string, newImage string) { + switch { + case build.Parameters.Strategy.Type == api.DockerBuildStrategyType && + build.Parameters.Strategy.DockerStrategy != nil && + build.Parameters.Strategy.DockerStrategy.BaseImage == oldImage: + build.Parameters.Strategy.DockerStrategy.BaseImage = newImage + case build.Parameters.Strategy.Type == api.STIBuildStrategyType && + build.Parameters.Strategy.STIStrategy != nil && + build.Parameters.Strategy.STIStrategy.Image == oldImage: + build.Parameters.Strategy.STIStrategy.Image = newImage + + case build.Parameters.Strategy.Type == api.CustomBuildStrategyType: + // update env variable references to the old image with the new image + if build.Parameters.Strategy.CustomStrategy.Env == nil { + build.Parameters.Strategy.CustomStrategy.Env = make([]kapi.EnvVar, 1) + build.Parameters.Strategy.CustomStrategy.Env[0] = kapi.EnvVar{Name: api.CustomBuildStrategyBaseImageKey, Value: newImage} + } else { + found := false + for i := range build.Parameters.Strategy.CustomStrategy.Env { + glog.V(4).Infof("Checking env variable %s %s", build.Parameters.Strategy.CustomStrategy.Env[i].Name, build.Parameters.Strategy.CustomStrategy.Env[i].Value) + if build.Parameters.Strategy.CustomStrategy.Env[i].Name == api.CustomBuildStrategyBaseImageKey { + found = true + if build.Parameters.Strategy.CustomStrategy.Env[i].Value == oldImage { + build.Parameters.Strategy.CustomStrategy.Env[i].Value = newImage + glog.V(4).Infof("Updated env variable %s %s", build.Parameters.Strategy.CustomStrategy.Env[i].Name, build.Parameters.Strategy.CustomStrategy.Env[i].Value) + break + } + } + } + if !found { + build.Parameters.Strategy.CustomStrategy.Env = append(build.Parameters.Strategy.CustomStrategy.Env, kapi.EnvVar{Name: api.CustomBuildStrategyBaseImageKey, Value: newImage}) + } + } + // update the actual custom build image with the new image, if applicable + if build.Parameters.Strategy.CustomStrategy.Image == oldImage { + build.Parameters.Strategy.CustomStrategy.Image = newImage + } + } +} diff --git a/pkg/build/util/generate_test.go b/pkg/build/util/generate_test.go index 74a8540a3751..8669c5fee58c 100644 --- a/pkg/build/util/generate_test.go +++ b/pkg/build/util/generate_test.go @@ -8,9 +8,11 @@ import ( "github.com/openshift/origin/pkg/build/api" ) +const originalImage = "originalImage" + func TestGenerateBuildFromConfig(t *testing.T) { source := mockSource() - strategy := mockStrategy() + strategy := mockDockerStrategy() output := mockOutput() bc := &api.BuildConfig{ @@ -52,7 +54,7 @@ func TestGenerateBuildFromConfig(t *testing.T) { func TestGenerateBuildFromBuild(t *testing.T) { source := mockSource() - strategy := mockStrategy() + strategy := mockDockerStrategy() output := mockOutput() build := &api.Build{ @@ -80,6 +82,152 @@ func TestGenerateBuildFromBuild(t *testing.T) { } } +func TestSubstituteImage(t *testing.T) { + source := mockSource() + strategy := mockDockerStrategy() + output := mockOutput() + const newImage = "newImage" + + bc := &api.BuildConfig{ + ObjectMeta: kapi.ObjectMeta{ + Name: "test-build-config", + }, + Parameters: api.BuildParameters{ + Source: source, + Revision: &api.SourceRevision{ + Type: api.BuildSourceGit, + Git: &api.GitSourceRevision{ + Commit: "1234", + }, + }, + Strategy: strategy, + Output: output, + }, + } + + // Docker build with nil base image + // base image should still be nil + build := GenerateBuildFromConfig(bc, nil) + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.DockerStrategy.BaseImage != "" { + t.Errorf("Base image name was improperly substituted in docker strategy") + } + // Docker build with a matched base image + // base image should be replaced. + build.Parameters.Strategy.DockerStrategy.BaseImage = originalImage + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.DockerStrategy.BaseImage != newImage { + t.Errorf("Base image name was not substituted in docker strategy") + } + // Docker build with an unmatched base image + // base image should not be replaced. + SubstituteImageReferences(build, "unmatched", "dummy") + if build.Parameters.Strategy.DockerStrategy.BaseImage == "dummy2" { + t.Errorf("Base image name was improperly substituted in docker strategy") + } + + // STI build with a matched base image + // base image should be replaced + build = GenerateBuildFromConfig(bc, nil) + build.Parameters.Strategy = mockSTIStrategy() + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.STIStrategy.Image != newImage { + t.Errorf("Base image name was not substituted in sti strategy") + } + // STI build with an unmatched base image + // base image should not be replaced + SubstituteImageReferences(build, "unmatched", "dummy") + if build.Parameters.Strategy.STIStrategy.Image == "dummy" { + t.Errorf("Base image name was improperly substituted in STI strategy") + } + + // Full custom build with a BaseImage and a well defined environment variable image value, + // both should be replaced. Additional environment variables should not be touched. + build = GenerateBuildFromConfig(bc, nil) + build.Parameters.Strategy = mockCustomStrategy() + build.Parameters.Strategy.CustomStrategy.Env = make([]kapi.EnvVar, 2) + build.Parameters.Strategy.CustomStrategy.Env[0] = kapi.EnvVar{Name: "someImage", Value: originalImage} + build.Parameters.Strategy.CustomStrategy.Env[1] = kapi.EnvVar{Name: api.CustomBuildStrategyBaseImageKey, Value: originalImage} + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.CustomStrategy.Image != newImage { + t.Errorf("Base image name was not substituted in custom strategy") + } + if build.Parameters.Strategy.CustomStrategy.Env[0].Value != originalImage { + t.Errorf("Random env variable %s was improperly substituted in custom strategy", build.Parameters.Strategy.CustomStrategy.Env[0].Name) + } + if build.Parameters.Strategy.CustomStrategy.Env[1].Value != newImage { + t.Errorf("Image env variable was not properly substituted in custom strategy") + } + if c := len(build.Parameters.Strategy.CustomStrategy.Env); c != 2 { + t.Errorf("Expected %d, found %d environment variables", 2, c) + } + + // Full custom build with base image that is not matched + // Base image name should be unchanged + SubstituteImageReferences(build, "dummy", "dummy") + if build.Parameters.Strategy.CustomStrategy.Image != newImage { + t.Errorf("Base image name was improperly substituted in custom strategy") + } + + // Full custom build with a BaseImage and a well defined environment variable image value that does not match the new image + // Only bsase image should be replaced. Environment variables should not be touched. + build = GenerateBuildFromConfig(bc, nil) + build.Parameters.Strategy = mockCustomStrategy() + build.Parameters.Strategy.CustomStrategy.Env = make([]kapi.EnvVar, 2) + build.Parameters.Strategy.CustomStrategy.Env[0] = kapi.EnvVar{Name: "someImage", Value: originalImage} + build.Parameters.Strategy.CustomStrategy.Env[1] = kapi.EnvVar{Name: api.CustomBuildStrategyBaseImageKey, Value: "dummy"} + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.CustomStrategy.Image != newImage { + t.Errorf("Base image name was not substituted in custom strategy") + } + if build.Parameters.Strategy.CustomStrategy.Env[0].Value != originalImage { + t.Errorf("Random env variable %s was improperly substituted in custom strategy", build.Parameters.Strategy.CustomStrategy.Env[0].Name) + } + if build.Parameters.Strategy.CustomStrategy.Env[1].Value != "dummy" { + t.Errorf("Image env variable was improperly substituted in custom strategy") + } + if c := len(build.Parameters.Strategy.CustomStrategy.Env); c != 2 { + t.Errorf("Expected %d, found %d environment variables", 2, c) + } + + // Custom build with a base Image but no image environment variable. + // base image should be replaced, new image environment variable should be added, + // existing environment variable should be untouched + build = GenerateBuildFromConfig(bc, nil) + build.Parameters.Strategy = mockCustomStrategy() + build.Parameters.Strategy.CustomStrategy.Env = make([]kapi.EnvVar, 1) + build.Parameters.Strategy.CustomStrategy.Env[0] = kapi.EnvVar{Name: "someImage", Value: originalImage} + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.CustomStrategy.Image != newImage { + t.Errorf("Base image name was not substituted in custom strategy") + } + if build.Parameters.Strategy.CustomStrategy.Env[0].Value != originalImage { + t.Errorf("Random env variable was improperly substituted in custom strategy") + } + if build.Parameters.Strategy.CustomStrategy.Env[1].Name != api.CustomBuildStrategyBaseImageKey || build.Parameters.Strategy.CustomStrategy.Env[1].Value != newImage { + t.Errorf("Image env variable was not added in custom strategy %s %s |", build.Parameters.Strategy.CustomStrategy.Env[1].Name, build.Parameters.Strategy.CustomStrategy.Env[1].Value) + } + if c := len(build.Parameters.Strategy.CustomStrategy.Env); c != 2 { + t.Errorf("Expected %d, found %d environment variables", 2, c) + } + + // Custom build with a base Image but no environment variables + // base image should be replaced, new image environment variable should be added + build = GenerateBuildFromConfig(bc, nil) + build.Parameters.Strategy = mockCustomStrategy() + SubstituteImageReferences(build, originalImage, newImage) + if build.Parameters.Strategy.CustomStrategy.Image != newImage { + t.Errorf("Base image name was not substituted in custom strategy") + } + if build.Parameters.Strategy.CustomStrategy.Env[0].Name != api.CustomBuildStrategyBaseImageKey || build.Parameters.Strategy.CustomStrategy.Env[0].Value != newImage { + t.Errorf("New image name variable was not added to environment list in custom strategy") + } + if c := len(build.Parameters.Strategy.CustomStrategy.Env); c != 1 { + t.Errorf("Expected %d, found %d environment variables", 1, c) + } + +} + func mockSource() api.BuildSource { return api.BuildSource{ Type: api.BuildSourceGit, @@ -90,7 +238,7 @@ func mockSource() api.BuildSource { } } -func mockStrategy() api.BuildStrategy { +func mockDockerStrategy() api.BuildStrategy { return api.BuildStrategy{ Type: api.DockerBuildStrategyType, DockerStrategy: &api.DockerBuildStrategy{ @@ -100,6 +248,24 @@ func mockStrategy() api.BuildStrategy { } } +func mockSTIStrategy() api.BuildStrategy { + return api.BuildStrategy{ + Type: api.STIBuildStrategyType, + STIStrategy: &api.STIBuildStrategy{ + Image: originalImage, + }, + } +} + +func mockCustomStrategy() api.BuildStrategy { + return api.BuildStrategy{ + Type: api.CustomBuildStrategyType, + CustomStrategy: &api.CustomBuildStrategy{ + Image: originalImage, + }, + } +} + func mockOutput() api.BuildOutput { return api.BuildOutput{ Registry: "http://localhost:5000", diff --git a/pkg/build/webhook/controller.go b/pkg/build/webhook/controller.go index 5fed738311ef..2bbdfa5f7e4e 100644 --- a/pkg/build/webhook/controller.go +++ b/pkg/build/webhook/controller.go @@ -10,7 +10,7 @@ import ( "github.com/openshift/origin/pkg/build/util" ) -// Webhook verification is dependent on the sending side, it can be +// Plugin for Webhook verification is dependent on the sending side, it can be // eg. github, bitbucket or else, so there must be a separate Plugin // instance for each webhook provider. type Plugin interface { diff --git a/pkg/build/webhook/controller_test.go b/pkg/build/webhook/controller_test.go index 217cce6894d3..55d90e1174d4 100644 --- a/pkg/build/webhook/controller_test.go +++ b/pkg/build/webhook/controller_test.go @@ -17,7 +17,7 @@ func (*okClient) GetBuildConfig(namespace, name string) (*api.BuildConfig, error return &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GithubWebHookType, + Type: api.GithubWebHookBuildTriggerType, GithubWebHook: &api.WebHookTrigger{ Secret: "secret101", }, diff --git a/pkg/build/webhook/generic/generic.go b/pkg/build/webhook/generic/generic.go index f07f572ca368..7a3d69a4a680 100644 --- a/pkg/build/webhook/generic/generic.go +++ b/pkg/build/webhook/generic/generic.go @@ -32,7 +32,7 @@ type gitInfo struct { // Extract services generic webhooks. func (p *WebHookPlugin) Extract(buildCfg *api.BuildConfig, secret, path string, req *http.Request) (revision *api.SourceRevision, proceed bool, err error) { - trigger, ok := webhook.FindTriggerPolicy(api.GenericWebHookType, buildCfg) + trigger, ok := webhook.FindTriggerPolicy(api.GenericWebHookBuildTriggerType, buildCfg) if !ok { err = fmt.Errorf("BuildConfig %s does not support the Generic webhook trigger type", buildCfg.Name) return diff --git a/pkg/build/webhook/generic/generic_test.go b/pkg/build/webhook/generic/generic_test.go index 8aaa6a516623..06ba2471c2ae 100644 --- a/pkg/build/webhook/generic/generic_test.go +++ b/pkg/build/webhook/generic/generic_test.go @@ -92,7 +92,7 @@ func TestExtractWithEmptyPayload(t *testing.T) { buildConfig := &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GenericWebHookType, + Type: api.GenericWebHookBuildTriggerType, GenericWebHook: &api.WebHookTrigger{ Secret: "secret100", }, @@ -126,7 +126,7 @@ func TestExtractWithUnmatchedRefGitPayload(t *testing.T) { buildConfig := &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GenericWebHookType, + Type: api.GenericWebHookBuildTriggerType, GenericWebHook: &api.WebHookTrigger{ Secret: "secret100", }, @@ -161,7 +161,7 @@ func TestExtractWithGitPayload(t *testing.T) { buildConfig := &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GenericWebHookType, + Type: api.GenericWebHookBuildTriggerType, GenericWebHook: &api.WebHookTrigger{ Secret: "secret100", }, diff --git a/pkg/build/webhook/github/github.go b/pkg/build/webhook/github/github.go index 44118b7a39e8..317bb62c9613 100644 --- a/pkg/build/webhook/github/github.go +++ b/pkg/build/webhook/github/github.go @@ -36,7 +36,7 @@ type pushEvent struct { // Extract services webhooks from github.com func (p *WebHook) Extract(buildCfg *api.BuildConfig, secret, path string, req *http.Request) (revision *api.SourceRevision, proceed bool, err error) { - trigger, ok := webhook.FindTriggerPolicy(api.GithubWebHookType, buildCfg) + trigger, ok := webhook.FindTriggerPolicy(api.GithubWebHookBuildTriggerType, buildCfg) if !ok { err = fmt.Errorf("BuildConfig %s does not support the Github webhook trigger type", buildCfg.Name) return diff --git a/pkg/build/webhook/github/github_test.go b/pkg/build/webhook/github/github_test.go index e256aea598c8..5d0627f7eb8a 100644 --- a/pkg/build/webhook/github/github_test.go +++ b/pkg/build/webhook/github/github_test.go @@ -18,7 +18,7 @@ func (c *okClient) GetBuildConfig(namespace, name string) (*api.BuildConfig, err return &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GithubWebHookType, + Type: api.GithubWebHookBuildTriggerType, GithubWebHook: &api.WebHookTrigger{ Secret: "secret101", }, @@ -185,7 +185,7 @@ func setup(t *testing.T, filename, eventType string) *testContext { buildCfg: &api.BuildConfig{ Triggers: []api.BuildTriggerPolicy{ { - Type: api.GithubWebHookType, + Type: api.GithubWebHookBuildTriggerType, GithubWebHook: &api.WebHookTrigger{ Secret: "secret101", }, diff --git a/pkg/cmd/cli/describe/describer.go b/pkg/cmd/cli/describe/describer.go index 5d1c233645d4..402ab916b804 100644 --- a/pkg/cmd/cli/describe/describer.go +++ b/pkg/cmd/cli/describe/describer.go @@ -62,7 +62,9 @@ func (d *BuildDescriber) DescribeParameters(p buildapi.BuildParameters, out *tab if p.Strategy.DockerStrategy != nil && p.Strategy.DockerStrategy.NoCache { formatString(out, "No Cache", "yes") } - + if p.Strategy.DockerStrategy != nil { + formatString(out, "BaseImage", p.Strategy.DockerStrategy.BaseImage) + } case buildapi.STIBuildStrategyType: formatString(out, "Builder Image", p.Strategy.STIStrategy.Image) if p.Strategy.STIStrategy.Clean { diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 8f5f713acc0f..2a3a422051e4 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -349,6 +349,12 @@ func (c *MasterConfig) RunBuildController() { controller.Run() } +// RunDeploymentController starts the build image change trigger controller process. +func (c *MasterConfig) RunBuildImageChangeTriggerController() { + factory := buildcontrollerfactory.ImageChangeControllerFactory{Client: c.OSClient} + factory.Create().Run() +} + // RunDeploymentController starts the deployment controller process. func (c *MasterConfig) RunDeploymentController() { factory := deploycontrollerfactory.DeploymentControllerFactory{ diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 066b61a2afdf..32c5b8afae45 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -263,6 +263,7 @@ func start(cfg *config, args []string) error { osmaster.RunAssetServer() osmaster.RunBuildController() + osmaster.RunBuildImageChangeTriggerController() osmaster.RunDeploymentController() osmaster.RunDeploymentConfigController() osmaster.RunDeploymentConfigChangeController() diff --git a/test/integration/buildcfgclient_test.go b/test/integration/buildcfgclient_test.go index 49efcd1359f7..c44da1fa97ba 100644 --- a/test/integration/buildcfgclient_test.go +++ b/test/integration/buildcfgclient_test.go @@ -115,7 +115,7 @@ func mockBuildConfig() *buildapi.BuildConfig { }, Triggers: []buildapi.BuildTriggerPolicy{ { - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, diff --git a/test/integration/deploy_trigger_test.go b/test/integration/deploy_trigger_test.go index cb4da1b7db31..47e98dad323e 100644 --- a/test/integration/deploy_trigger_test.go +++ b/test/integration/deploy_trigger_test.go @@ -20,6 +20,11 @@ import ( "github.com/openshift/origin/pkg/api/latest" "github.com/openshift/origin/pkg/api/v1beta1" + buildcontrollerfactory "github.com/openshift/origin/pkg/build/controller/factory" + buildstrategy "github.com/openshift/origin/pkg/build/controller/strategy" + buildregistry "github.com/openshift/origin/pkg/build/registry/build" + buildconfigregistry "github.com/openshift/origin/pkg/build/registry/buildconfig" + buildetcd "github.com/openshift/origin/pkg/build/registry/etcd" osclient "github.com/openshift/origin/pkg/client" deployapi "github.com/openshift/origin/pkg/deploy/api" deploycontrollerfactory "github.com/openshift/origin/pkg/deploy/controller/factory" @@ -262,6 +267,8 @@ func NewTestOpenshift(t *testing.T) *testOpenshift { ImageRepositoryInterface: imageEtcd, } + buildEtcd := buildetcd.New(etcdHelper) + storage := map[string]apiserver.RESTStorage{ "images": image.NewREST(imageEtcd), "imageRepositories": imagerepository.NewREST(imageEtcd, ""), @@ -269,6 +276,8 @@ func NewTestOpenshift(t *testing.T) *testOpenshift { "deployments": deployregistry.NewREST(deployEtcd), "deploymentConfigs": deployconfigregistry.NewREST(deployEtcd), "generateDeploymentConfigs": deployconfiggenerator.NewREST(deployConfigGenerator, v1beta1.Codec), + "builds": buildregistry.NewREST(buildEtcd), + "buildConfigs": buildconfigregistry.NewREST(buildEtcd), } handlerContainer := master.NewHandlerContainer(osMux) @@ -296,6 +305,29 @@ func NewTestOpenshift(t *testing.T) *testOpenshift { iccFactory.Create().Run() + biccFactory := buildcontrollerfactory.ImageChangeControllerFactory{ + Client: osClient, + Stop: openshift.stop, + } + + biccFactory.Create().Run() + + bcFactory := buildcontrollerfactory.BuildControllerFactory{ + Client: osClient, + KubeClient: kubeClient, + DockerBuildStrategy: &buildstrategy.DockerBuildStrategy{ + Image: "test-docker-builder", + UseLocalImages: false, + }, + STIBuildStrategy: &buildstrategy.STIBuildStrategy{ + Image: "test-sti-builder", + TempDirectoryCreator: buildstrategy.STITempDirectoryCreator, + UseLocalImages: false, + }, + } + + bcFactory.Create().Run() + return openshift } diff --git a/test/integration/imagechange_buildtrigger_test.go b/test/integration/imagechange_buildtrigger_test.go new file mode 100644 index 000000000000..7e815663d746 --- /dev/null +++ b/test/integration/imagechange_buildtrigger_test.go @@ -0,0 +1,108 @@ +// +build integration,!no-etcd + +package integration + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + watchapi "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + buildapi "github.com/openshift/origin/pkg/build/api" + imageapi "github.com/openshift/origin/pkg/image/api" +) + +func init() { + requireEtcd() +} + +func TestSimpleImageChangeBuildTrigger(t *testing.T) { + t.Log("starting run") + deleteAllEtcdKeys() + //openshift := NewTestOpenshift(t) + openshift := NewTestOpenshift(t) + defer openshift.Close() + + imageRepo := &imageapi.ImageRepository{ + ObjectMeta: kapi.ObjectMeta{Name: "test-image-repo"}, + DockerImageRepository: "registry:8080/openshift/test-image", + Tags: map[string]string{ + "latest": "ref-1", + }, + } + + config := imageChangeBuildConfig() + var err error + + watch, err := openshift.Client.Builds(testNamespace).Watch(labels.Everything(), labels.Everything(), "0") + if err != nil { + t.Fatalf("Couldn't subscribe to Builds %v", err) + } + + if imageRepo, err = openshift.Client.ImageRepositories(testNamespace).Create(imageRepo); err != nil { + t.Fatalf("Couldn't create ImageRepository: %v", err) + } + + if _, err := openshift.Client.BuildConfigs(testNamespace).Create(config); err != nil { + t.Fatalf("Couldn't create BuildConfig: %v", err) + } + + t.Log("done creating buildconfig") + + imageRepo.Tags["latest"] = "ref-2" + + if _, err = openshift.Client.ImageRepositories(testNamespace).Update(imageRepo); err != nil { + t.Fatalf("Error updating imageRepo: %v", err) + } + + event := <-watch.ResultChan() + if e, a := watchapi.Added, event.Type; e != a { + t.Fatalf("expected watch event type %s, got %s", e, a) + } + newBuild := event.Object.(*buildapi.Build) + + if newBuild.Parameters.Strategy.DockerStrategy.BaseImage != "registry:8080/openshift/test-image:ref-2" { + t.Fatalf("Expected build with base image %s, got %s", "registry:8080/openshift/test-image:ref-2", newBuild.Parameters.Strategy.DockerStrategy.BaseImage) + } + +} + +func imageChangeBuildConfig() *buildapi.BuildConfig { + buildcfg := &buildapi.BuildConfig{ + ObjectMeta: kapi.ObjectMeta{ + Name: "testBuildCfg", + }, + Parameters: buildapi.BuildParameters{ + Source: buildapi.BuildSource{ + Type: "Git", + Git: &buildapi.GitBuildSource{ + URI: "git://github.com/openshift/ruby-hello-world.git", + }, + }, + Strategy: buildapi.BuildStrategy{ + Type: buildapi.DockerBuildStrategyType, + DockerStrategy: &buildapi.DockerBuildStrategy{ + ContextDir: "contextimage", + BaseImage: "registry:8080/openshift/test-image", + }, + }, + Output: buildapi.BuildOutput{ + ImageTag: "tag", + }, + }, + Triggers: []buildapi.BuildTriggerPolicy{ + { + Type: buildapi.ImageChangeBuildTriggerType, + ImageChange: &buildapi.ImageChangeTrigger{ + Image: "registry:8080/openshift/test-image", + ImageRepositoryRef: &kapi.ObjectReference{ + Name: "test-image-repo", + }, + Tag: "latest", + }, + }, + }, + } + return buildcfg +} diff --git a/test/integration/webhookgithub_test.go b/test/integration/webhookgithub_test.go index 40bcc93b2cda..d0bba3e8c638 100644 --- a/test/integration/webhookgithub_test.go +++ b/test/integration/webhookgithub_test.go @@ -30,7 +30,7 @@ func TestWebhookGithubPush(t *testing.T) { }, Triggers: []buildapi.BuildTriggerPolicy{ { - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", }, @@ -86,7 +86,7 @@ func TestWebhookGithubPing(t *testing.T) { }, Triggers: []buildapi.BuildTriggerPolicy{ { - Type: buildapi.GithubWebHookType, + Type: buildapi.GithubWebHookBuildTriggerType, GithubWebHook: &buildapi.WebHookTrigger{ Secret: "secret101", },