package controller
import (
"fmt"
"github.com/golang/glog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
buildapi "github.com/openshift/origin/pkg/build/api"
buildclient "github.com/openshift/origin/pkg/build/client"
buildutil "github.com/openshift/origin/pkg/build/util"
imageapi "github.com/openshift/origin/pkg/image/api"
)
type ImageChangeControllerFatalError struct {
Reason string
Err error
}
func (e ImageChangeControllerFatalError) Error() string {
return "fatal error handling ImageRepository change: " + e.Reason
}
// 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 {
BuildConfigStore cache.Store
BuildCreator buildclient.BuildCreator
BuildConfigUpdater buildclient.BuildConfigUpdater
// Stop is an optional channel that controls when the controller exits
Stop <-chan struct{}
}
// HandleImageRepo processes the next ImageRepository event.
func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageRepository) error {
glog.V(4).Infof("Build image change controller detected imagerepo change %s", repo.Status.DockerImageRepository)
imageSubs := make(map[string]string)
repoSubs := make(map[kapi.ObjectReference]string)
// TODO: this is inefficient
for _, bc := range c.BuildConfigStore.List() {
config := bc.(*buildapi.BuildConfig)
shouldBuild := false
// For every ImageChange trigger find the latest tagged image from the image repository and replace that value
// throughout the build strategies. A new build is triggered only if the latest tagged image id or pull spec
// differs from the last triggered build recorded on the build config.
for _, trigger := range config.Triggers {
if trigger.Type != buildapi.ImageChangeBuildTriggerType {
continue
}
change := trigger.ImageChange
// only trigger a build if this image repo matches the name and namespace of the ref in the build trigger
// also do not trigger if the imagerepo does not have a valid DockerImageRepository value for us to pull
// the image from
if repo.Status.DockerImageRepository == "" || change.From.Name != repo.Name || (len(change.From.Namespace) != 0 && change.From.Namespace != repo.Namespace) {
continue
}
latest, err := imageapi.LatestTaggedImage(repo, change.Tag)
if err != nil {
util.HandleError(fmt.Errorf("unable to find tagged image: %v", err))
continue
}
// (must be different) to trigger a build
last := change.LastTriggeredImageID
next := latest.Image
if len(next) == 0 {
// tags without images should still trigger builds (when going from a pure tag to an image
// based tag, we should rebuild)
next = latest.DockerImageReference
}
if len(last) == 0 || next != last {
imageSubs[change.Image] = latest.DockerImageReference
repoSubs[change.From] = latest.DockerImageReference
change.LastTriggeredImageID = next
shouldBuild = true
}
}
if shouldBuild {
glog.V(4).Infof("Running build for buildConfig %s in namespace %s", config.Name, config.Namespace)
b := buildutil.GenerateBuildFromConfig(config, nil, imageSubs, repoSubs)
if err := c.BuildCreator.Create(config.Namespace, b); err != nil {
return fmt.Errorf("error starting build for buildConfig %s: %v", config.Name, err)
}
if err := c.BuildConfigUpdater.Update(config); err != nil {
// This is not a retryable error because the build has been created. The worst case
// outcome of not updating the buildconfig is that we might rerun a build for the
// same "new" imageid change in the future, which is better than running the build
// 2+ times by retrying it here.
return ImageChangeControllerFatalError{Reason: fmt.Sprintf("Error updating buildConfig %s with new LastTriggeredImageID", config.Name), Err: err}
}
}
}
return nil
}