package controller
import (
"fmt"
"github.com/golang/glog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
deployapi "github.com/openshift/origin/pkg/deploy/api"
imageapi "github.com/openshift/origin/pkg/image/api"
)
// ImageChangeController watches for changes to ImageRepositories and regenerates
// DeploymentConfigs when a new version of a tag referenced by a DeploymentConfig
// is available.
type ImageChangeController struct {
DeploymentConfigClient ImageChangeControllerDeploymentConfigClient
NextImageRepository func() *imageapi.ImageRepository
// Stop is an optional channel that controls when the controller exits
Stop <-chan struct{}
}
// Run processes ImageRepository events one by one.
func (c *ImageChangeController) Run() {
go util.Until(func() {
err := c.HandleImageRepo(c.NextImageRepository())
if err != nil {
glog.Errorf("%v", err)
}
}, 0, c.Stop)
}
// HandleImageRepo processes the next ImageRepository event.
func (c *ImageChangeController) HandleImageRepo(imageRepo *imageapi.ImageRepository) error {
configsToGenerate := []*deployapi.DeploymentConfig{}
firedTriggersForConfig := make(map[string][]deployapi.DeploymentTriggerImageChangeParams)
configs, err := c.DeploymentConfigClient.ListDeploymentConfigs()
if err != nil {
return fmt.Errorf("couldn't get list of deploymentConfigs while handling imageRepo %s: %v", labelForRepo(imageRepo), err)
}
for _, config := range configs {
glog.V(4).Infof("Detecting changed images for deploymentConfig %s", labelFor(config))
// Extract relevant triggers for this imageRepo for this config
triggersForConfig := []deployapi.DeploymentTriggerImageChangeParams{}
for _, trigger := range config.Triggers {
if trigger.Type != deployapi.DeploymentTriggerOnImageChange ||
!trigger.ImageChangeParams.Automatic {
continue
}
if triggerMatchesImage(config, trigger.ImageChangeParams, imageRepo) {
glog.V(4).Infof("Found matching %s trigger for deploymentConfig %s: %#v", trigger.Type, labelFor(config), trigger.ImageChangeParams)
triggersForConfig = append(triggersForConfig, *trigger.ImageChangeParams)
}
}
for _, params := range triggersForConfig {
glog.V(4).Infof("Processing image triggers for deploymentConfig %s", labelFor(config))
containerNames := util.NewStringSet(params.ContainerNames...)
for _, container := range config.Template.ControllerTemplate.Template.Spec.Containers {
if !containerNames.Has(container.Name) {
continue
}
// The container image's tag name is by convention the same as the image ID it references
_, _, _, containerImageID, err := imageapi.SplitDockerPullSpec(container.Image)
if err != nil {
glog.V(4).Infof("Skipping container %s for config %s; container's image is invalid: %v", container.Name, labelFor(config), err)
continue
}
if repoImageID, repoHasTag := imageRepo.Tags[params.Tag]; repoHasTag && repoImageID != containerImageID {
configsToGenerate = append(configsToGenerate, config)
firedTriggersForConfig[config.Name] = append(firedTriggersForConfig[config.Name], params)
}
}
}
}
anyFailed := false
for _, config := range configsToGenerate {
err := c.regenerate(imageRepo, config, firedTriggersForConfig[config.Name])
if err != nil {
anyFailed = true
continue
}
glog.V(4).Infof("Updated deploymentConfig %s in response to image change trigger", labelFor(config))
}
if anyFailed {
return fmt.Errorf("couldn't update some deploymentConfigs for trigger on imageRepo %s", labelForRepo(imageRepo))
}
glog.V(4).Infof("Updated all configs for trigger on imageRepo %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, trigger *deployapi.DeploymentTriggerImageChangeParams, repo *imageapi.ImageRepository) bool {
if len(trigger.From.Name) > 0 {
namespace := trigger.From.Namespace
if len(namespace) == 0 {
namespace = config.Namespace
}
return repo.Namespace == namespace && repo.Name == trigger.From.Name
}
// This is an invalid state (as one of From.Name or RepositoryName is required), but
// account for it anyway.
if len(trigger.RepositoryName) == 0 {
return false
}
// If the repo's repository information isn't yet available, we can't assume it'll match.
return len(repo.Status.DockerImageRepository) > 0 &&
trigger.RepositoryName == repo.Status.DockerImageRepository
}
func (c *ImageChangeController) regenerate(imageRepo *imageapi.ImageRepository, config *deployapi.DeploymentConfig, triggers []deployapi.DeploymentTriggerImageChangeParams) 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", labelFor(config), err)
}
// Update the deployment config with the trigger that resulted in the new config
causes := []*deployapi.DeploymentCause{}
for _, trigger := range triggers {
repoName := trigger.RepositoryName
if len(repoName) == 0 {
if len(imageRepo.Status.DockerImageRepository) == 0 {
// If the trigger relies on a image repo reference, and we don't know what docker repo
// it points at, we can't build a cause for the reference yet.
continue
}
id, ok := imageRepo.Tags[trigger.Tag]
if !ok {
// TODO: not really sure what to do here
}
repoName = fmt.Sprintf("%s:%s", imageRepo.Status.DockerImageRepository, id)
}
causes = append(causes,
&deployapi.DeploymentCause{
Type: deployapi.DeploymentTriggerOnImageChange,
ImageTrigger: &deployapi.DeploymentCauseImageTrigger{
RepositoryName: repoName,
Tag: trigger.Tag,
},
})
}
newConfig.Details = &deployapi.DeploymentDetails{
Causes: causes,
}
// Persist the new config
_, err = c.DeploymentConfigClient.UpdateDeploymentConfig(newConfig.Namespace, newConfig)
if err != nil {
return fmt.Errorf("couldn't update deploymentConfig %s: %v", labelFor(config), err)
}
return nil
}
func labelForRepo(imageRepo *imageapi.ImageRepository) string {
return fmt.Sprintf("%s/%s", imageRepo.Namespace, imageRepo.Name)
}
// ImageChangeControllerDeploymentConfigClient abstracts access to DeploymentConfigs.
type ImageChangeControllerDeploymentConfigClient interface {
ListDeploymentConfigs() ([]*deployapi.DeploymentConfig, error)
UpdateDeploymentConfig(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
GenerateDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error)
}
// ImageChangeControllerDeploymentConfigClientImpl is a pluggable ChangeStrategy.
type ImageChangeControllerDeploymentConfigClientImpl 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 *ImageChangeControllerDeploymentConfigClientImpl) ListDeploymentConfigs() ([]*deployapi.DeploymentConfig, error) {
return i.ListDeploymentConfigsFunc()
}
func (i *ImageChangeControllerDeploymentConfigClientImpl) GenerateDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error) {
return i.GenerateDeploymentConfigFunc(namespace, name)
}
func (i *ImageChangeControllerDeploymentConfigClientImpl) UpdateDeploymentConfig(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
return i.UpdateDeploymentConfigFunc(namespace, config)
}