package imagechange 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 increments the version of a DeploymentConfig which has an image // change trigger when a tag update to a triggered ImageRepository 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 "fatal error handling imageRepository: " + string(e) } // Handle processes image change triggers associated with imageRepo. func (c *ImageChangeController) Handle(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 } ref, err := imageapi.ParseDockerImageReference(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 } latest, err := imageapi.LatestTaggedImage(imageRepo, params.Tag) if err != nil { glog.V(4).Infof("Skipping container %s for config %s; %s", container.Name, labelFor(config), err) continue } containerImageID := ref.ID if len(containerImageID) == 0 { // For v1 images, the container image's tag name is by convention the same as the image ID it references containerImageID = ref.Tag } if latest.Image != containerImageID { glog.V(4).Infof("Container %s for config %s: image id changed from %q to %q; regenerating config", container.Name, labelFor(config), containerImageID, latest.Image) 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 fatalError(fmt.Sprintf("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 } latest, err := imageapi.LatestTaggedImage(imageRepo, trigger.Tag) if err != nil { return fmt.Errorf("error generating new version of deploymentConfig: %s: %s", labelFor(config), err) } repoName = latest.DockerImageReference } 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 err } return nil } func labelForRepo(imageRepo *imageapi.ImageRepository) string { return fmt.Sprintf("%s/%s", imageRepo.Namespace, imageRepo.Name) } // labelFor builds a string identifier for a DeploymentConfig. func labelFor(config *deployapi.DeploymentConfig) string { return fmt.Sprintf("%s/%s:%d", config.Namespace, config.Name, config.LatestVersion) } // ImageChangeControllerDeploymentConfigClient 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) } // ImageChangeControllerDeploymentConfigClientImpl is a pluggable ChangeStrategy. 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) }