package imagechange
import (
"fmt"
"github.com/golang/glog"
deployapi "github.com/openshift/origin/pkg/deploy/api"
deployutil "github.com/openshift/origin/pkg/deploy/util"
imageapi "github.com/openshift/origin/pkg/image/api"
)
// ImageChangeController increments the version of a DeploymentConfig which has an image
// change trigger when a tag update to a triggered ImageStream is detected.
//
// Use the ImageChangeControllerFactory to create this controller.
type ImageChangeController struct {
deploymentConfigClient deploymentConfigClient
}
// fatalError is an error which can't be retried.
type fatalError string
func (e fatalError) Error() string {
return fmt.Sprintf("fatal error handling ImageStream: %s", string(e))
}
// Handle processes image change triggers associated with imageRepo.
func (c *ImageChangeController) Handle(imageRepo *imageapi.ImageStream) error {
configs, err := c.deploymentConfigClient.listDeploymentConfigs()
if err != nil {
return fmt.Errorf("couldn't get list of DeploymentConfig while handling ImageStream %s: %v", labelForRepo(imageRepo), err)
}
// Find any configs which should be updated based on the new image state
configsToUpdate := map[string]*deployapi.DeploymentConfig{}
for _, config := range configs {
glog.V(4).Infof("Detecting changed images for DeploymentConfig %s", deployutil.LabelForDeploymentConfig(config))
for _, trigger := range config.Spec.Triggers {
params := trigger.ImageChangeParams
// Only automatic image change triggers should fire
if trigger.Type != deployapi.DeploymentTriggerOnImageChange || !params.Automatic {
continue
}
// Check if the image repo matches the trigger
if !triggerMatchesImage(config, params, imageRepo) {
continue
}
_, tag, ok := imageapi.SplitImageStreamTag(params.From.Name)
if !ok {
return fmt.Errorf("invalid ImageStreamTag: %s", params.From.Name)
}
// Find the latest tag event for the trigger tag
latestEvent := imageapi.LatestTaggedImage(imageRepo, tag)
if latestEvent == nil {
glog.V(5).Infof("Couldn't find latest tag event for tag %s in ImageStream %s", tag, labelForRepo(imageRepo))
continue
}
// Ensure a change occurred
if len(latestEvent.DockerImageReference) > 0 &&
latestEvent.DockerImageReference != params.LastTriggeredImage {
// Mark the config for regeneration
configsToUpdate[config.Name] = config
}
}
}
// Attempt to regenerate all configs which may contain image updates
anyFailed := false
for _, config := range configsToUpdate {
err := c.regenerate(config)
if err != nil {
anyFailed = true
glog.V(2).Infof("Couldn't regenerate DeploymentConfig %s: %s", deployutil.LabelForDeploymentConfig(config), err)
continue
}
}
if anyFailed {
return fatalError(fmt.Sprintf("couldn't update some DeploymentConfig for trigger on ImageStream %s", labelForRepo(imageRepo)))
}
glog.V(5).Infof("Updated all DeploymentConfigs for trigger on ImageStream %s", labelForRepo(imageRepo))
return nil
}
// triggerMatchesImages decides whether a given trigger for config matches the provided image repo.
// When matching:
// - The trigger From field is preferred over the deprecated RepositoryName field.
// - The namespace of the trigger is preferred over the config's namespace.
func triggerMatchesImage(config *deployapi.DeploymentConfig, params *deployapi.DeploymentTriggerImageChangeParams, repo *imageapi.ImageStream) bool {
if len(params.From.Name) > 0 {
namespace := params.From.Namespace
if len(namespace) == 0 {
namespace = config.Namespace
}
name, _, ok := imageapi.SplitImageStreamTag(params.From.Name)
return repo.Namespace == namespace && repo.Name == name && ok
}
return false
}
// regenerate calls the generator to get a new config. If the newly generated
// config's version is newer, update the old config to be the new config.
// Otherwise do nothing.
func (c *ImageChangeController) regenerate(config *deployapi.DeploymentConfig) error {
// Get a regenerated config which includes the new image repo references
newConfig, err := c.deploymentConfigClient.generateDeploymentConfig(config.Namespace, config.Name)
if err != nil {
return fmt.Errorf("error generating new version of DeploymentConfig %s: %v", deployutil.LabelForDeploymentConfig(config), err)
}
// No update occurred
if config.Status.LatestVersion == newConfig.Status.LatestVersion {
glog.V(5).Infof("No version difference for generated DeploymentConfig %s", deployutil.LabelForDeploymentConfig(config))
return nil
}
// Persist the new config
_, err = c.deploymentConfigClient.updateDeploymentConfig(newConfig.Namespace, newConfig)
if err != nil {
return err
}
glog.V(4).Infof("Regenerated DeploymentConfig %s for image updates", deployutil.LabelForDeploymentConfig(config))
return nil
}
func labelForRepo(imageRepo *imageapi.ImageStream) string {
return fmt.Sprintf("%s/%s", imageRepo.Namespace, imageRepo.Name)
}
// deploymentConfigClient abstracts access to DeploymentConfigs.
type deploymentConfigClient interface {
listDeploymentConfigs() ([]*deployapi.DeploymentConfig, error)
updateDeploymentConfig(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
generateDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error)
}
// deploymentConfigClientImpl is a pluggable deploymentConfigClient.
type deploymentConfigClientImpl struct {
listDeploymentConfigsFunc func() ([]*deployapi.DeploymentConfig, error)
generateDeploymentConfigFunc func(namespace, name string) (*deployapi.DeploymentConfig, error)
updateDeploymentConfigFunc func(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
}
func (i *deploymentConfigClientImpl) listDeploymentConfigs() ([]*deployapi.DeploymentConfig, error) {
return i.listDeploymentConfigsFunc()
}
func (i *deploymentConfigClientImpl) generateDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error) {
return i.generateDeploymentConfigFunc(namespace, name)
}
func (i *deploymentConfigClientImpl) updateDeploymentConfig(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
return i.updateDeploymentConfigFunc(namespace, config)
}