package controller
import (
"fmt"
"strings"
"github.com/golang/glog"
kerrors "k8s.io/kubernetes/pkg/api/errors"
kapi "k8s.io/kubernetes/pkg/api"
utilruntime "k8s.io/kubernetes/pkg/util/runtime"
buildapi "github.com/openshift/origin/pkg/build/api"
buildclient "github.com/openshift/origin/pkg/build/client"
buildutil "github.com/openshift/origin/pkg/build/util"
oscache "github.com/openshift/origin/pkg/client/cache"
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 {
BuildConfigIndex oscache.StoreToBuildConfigLister
BuildConfigInstantiator buildclient.BuildConfigInstantiator
}
// getImageStreamNameFromReference strips off the :tag or @id suffix
// from an ImageStream[Tag,Image,''].Name
func getImageStreamNameFromReference(ref *kapi.ObjectReference) string {
name := strings.Split(ref.Name, ":")[0]
return strings.Split(name, "@")[0]
}
// HandleImageStream processes the next ImageStream event.
func (c *ImageChangeController) HandleImageStream(stream *imageapi.ImageStream) error {
glog.V(4).Infof("Build image change controller detected ImageStream change %s", stream.Status.DockerImageRepository)
// Loop through all build configurations and record if there was an error
// instead of breaking the loop. The error will be returned in the end, so the
// retry controller can retry. Any BuildConfigs that were processed successfully
// should have had their LastTriggeredImageID updated, so the retry should result
// in a no-op for them.
hasError := false
bcs, err := c.BuildConfigIndex.GetConfigsForImageStreamTrigger(stream.Namespace, stream.Name)
if err != nil {
return err
}
for _, config := range bcs {
var (
from *kapi.ObjectReference
shouldBuild = false
triggeredImage = ""
latestReference string
)
// For every ImageChange trigger find the latest tagged image from the image stream and
// invoke a build using that image id. 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 that trigger
for _, trigger := range config.Spec.Triggers {
if trigger.Type != buildapi.ImageChangeBuildTriggerType {
continue
}
if trigger.ImageChange.From != nil {
from = trigger.ImageChange.From
} else {
from = buildutil.GetInputReference(config.Spec.Strategy)
}
if from == nil || from.Kind != "ImageStreamTag" {
continue
}
fromStreamName, tag, ok := imageapi.SplitImageStreamTag(from.Name)
if !ok {
glog.Errorf("Invalid image stream tag: %s in build config %s/%s", from.Name, config.Name, config.Namespace)
continue
}
fromNamespace := from.Namespace
if len(fromNamespace) == 0 {
fromNamespace = config.Namespace
}
// only trigger a build if this image stream matches the name and namespace of the stream 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 len(stream.Status.DockerImageRepository) == 0 || fromStreamName != stream.Name || fromNamespace != stream.Namespace {
continue
}
// This split is safe because ImageStreamTag names always have the form
// name:tag.
latestReference, ok = imageapi.ResolveLatestTaggedImage(stream, tag)
if !ok {
glog.V(4).Infof("unable to find tagged image: no image recorded for %s/%s:%s", stream.Namespace, stream.Name, tag)
continue
}
glog.V(4).Infof("Found ImageStream %s/%s with tag %s", stream.Namespace, stream.Name, tag)
// (must be different) to trigger a build
last := trigger.ImageChange.LastTriggeredImageID
next := latestReference
if len(last) == 0 || (len(next) > 0 && next != last) {
triggeredImage = next
shouldBuild = true
// it doesn't really make sense to have multiple image change triggers any more,
// so just exit the loop now
break
}
}
if shouldBuild {
glog.V(4).Infof("Running build for BuildConfig %s/%s", config.Namespace, config.Name)
buildCauses := []buildapi.BuildTriggerCause{}
// instantiate new build
request := &buildapi.BuildRequest{
ObjectMeta: kapi.ObjectMeta{
Name: config.Name,
Namespace: config.Namespace,
},
TriggeredBy: append(buildCauses,
buildapi.BuildTriggerCause{
Message: buildapi.BuildTriggerCauseImageMsg,
ImageChangeBuild: &buildapi.ImageChangeCause{
ImageID: latestReference,
FromRef: from,
},
},
),
TriggeredByImage: &kapi.ObjectReference{
Kind: "DockerImage",
Name: triggeredImage,
},
From: from,
}
if _, err := c.BuildConfigInstantiator.Instantiate(config.Namespace, request); err != nil {
if kerrors.IsConflict(err) {
utilruntime.HandleError(fmt.Errorf("unable to instantiate Build for BuildConfig %s/%s due to a conflicting update: %v", config.Namespace, config.Name, err))
} else {
utilruntime.HandleError(fmt.Errorf("error instantiating Build from BuildConfig %s/%s: %v", config.Namespace, config.Name, err))
}
hasError = true
continue
}
}
}
if hasError {
return fmt.Errorf("an error occurred processing 1 or more build configurations; the image change trigger for image stream %s will be retried", stream.Status.DockerImageRepository)
}
return nil
}