package imagechange import ( "fmt" "github.com/golang/glog" "k8s.io/kubernetes/pkg/util/sets" "github.com/openshift/origin/pkg/client" 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 deployment config 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 { listDeploymentConfigs func() ([]*deployapi.DeploymentConfig, error) client client.Interface } // fatalError is an error which can't be retried. type fatalError string func (e fatalError) Error() string { return fmt.Sprintf("fatal error handling image stream: %s", string(e)) } // Handle processes image change triggers associated with imagestream. func (c *ImageChangeController) Handle(stream *imageapi.ImageStream) error { configs, err := c.listDeploymentConfigs() if err != nil { return fmt.Errorf("couldn't get list of deployment configs while handling image stream %q: %v", imageapi.LabelForStream(stream), err) } // Find any configs which should be updated based on the new image state configsToUpdate := []*deployapi.DeploymentConfig{} for _, config := range configs { glog.V(4).Infof("Detecting image changes for deployment config %q", deployutil.LabelForDeploymentConfig(config)) hasImageChange := false for _, trigger := range config.Spec.Triggers { params := trigger.ImageChangeParams // Only automatic image change triggers should fire if trigger.Type != deployapi.DeploymentTriggerOnImageChange { continue } // All initial deployments should have their images resolved in order to // be able to work and not try to pull non-existent images from DockerHub. // Deployments with automatic set to false that have been deployed at least // once shouldn't have their images updated. if (!params.Automatic || config.Spec.Paused) && len(params.LastTriggeredImage) > 0 { continue } // Check if the image stream matches the trigger if !triggerMatchesImage(config, params, stream) { continue } _, tag, ok := imageapi.SplitImageStreamTag(params.From.Name) if !ok { glog.Warningf("Invalid image stream tag %q in %q", params.From.Name, deployutil.LabelForDeploymentConfig(config)) continue } // Find the latest tag event for the trigger tag latestEvent := imageapi.LatestTaggedImage(stream, tag) if latestEvent == nil { glog.V(5).Infof("Couldn't find latest tag event for tag %q in image stream %q", tag, imageapi.LabelForStream(stream)) continue } // Ensure a change occurred if len(latestEvent.DockerImageReference) == 0 || latestEvent.DockerImageReference == params.LastTriggeredImage { glog.V(4).Infof("No image changes for deployment config %q were detected", deployutil.LabelForDeploymentConfig(config)) continue } names := sets.NewString(params.ContainerNames...) for i := range config.Spec.Template.Spec.Containers { container := &config.Spec.Template.Spec.Containers[i] if !names.Has(container.Name) { continue } // Update the image container.Image = latestEvent.DockerImageReference // Log the last triggered image ID params.LastTriggeredImage = latestEvent.DockerImageReference hasImageChange = true } } if hasImageChange { configsToUpdate = append(configsToUpdate, config) } } // Attempt to regenerate all configs which may contain image updates anyFailed := false for _, config := range configsToUpdate { instantiate(config) if _, err := c.client.DeploymentConfigs(config.Namespace).Update(config); err != nil { anyFailed = true glog.V(2).Infof("Couldn't update deployment config %q: %v", deployutil.LabelForDeploymentConfig(config), err) } } if anyFailed { return fatalError(fmt.Sprintf("couldn't update some deployment configs for trigger on image stream %q", imageapi.LabelForStream(stream))) } glog.V(5).Infof("Updated all deployment configs for trigger on image stream %q", imageapi.LabelForStream(stream)) return nil } // triggerMatchesImages decides whether a given trigger for config matches the provided image stream. func triggerMatchesImage(config *deployapi.DeploymentConfig, params *deployapi.DeploymentTriggerImageChangeParams, stream *imageapi.ImageStream) bool { namespace := params.From.Namespace if len(namespace) == 0 { namespace = config.Namespace } name, _, ok := imageapi.SplitImageStreamTag(params.From.Name) return stream.Namespace == namespace && stream.Name == name && ok } // instantiate the deployment config if it has no config change trigger and all of its // image change triggers are set to automatic. func instantiate(config *deployapi.DeploymentConfig) { if deployutil.HasChangeTrigger(config) { return } shouldInstantiate := true for _, trigger := range config.Spec.Triggers { if trigger.Type != deployapi.DeploymentTriggerOnImageChange { continue } if !trigger.ImageChangeParams.Automatic { shouldInstantiate = false } } if shouldInstantiate { deployutil.Instantiate(config) } }