// 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.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 } // Find the latest tag event for the trigger tag latestEvent := imageapi.LatestTaggedImage(imageRepo, params.Tag) if latestEvent == nil { glog.V(2).Infof("Couldn't find latest tag event for tag %s in ImageStream %s", params.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 } glog.V(4).Infof("Regenerated DeploymentConfig %s in response to image change trigger", deployutil.LabelForDeploymentConfig(config)) } if anyFailed { return fatalError(fmt.Sprintf("couldn't update some DeploymentConfig for trigger on ImageStream %s", labelForRepo(imageRepo))) } glog.V(4).Infof("Updated all DeploymentConfigs for trigger on ImageStream %s", labelForRepo(imageRepo)) return nil }
func tagReferenceToTagEvent(stream *api.ImageStream, tagRef api.TagReference, tagOrID string) (*api.TagEvent, error) { switch tagRef.From.Kind { case "DockerImage": return &api.TagEvent{ Created: util.Now(), DockerImageReference: tagRef.From.Name, }, nil case "ImageStreamImage": ref, err := api.DockerImageReferenceForStream(stream) if err != nil { return nil, err } resolvedIDs := api.ResolveImageID(stream, tagOrID) switch len(resolvedIDs) { case 1: ref.ID = resolvedIDs.List()[0] return &api.TagEvent{ Created: util.Now(), DockerImageReference: ref.String(), Image: ref.ID, }, nil case 0: return nil, fmt.Errorf("no images match the prefix %q", tagOrID) default: return nil, fmt.Errorf("multiple images match the prefix %q: %s", tagOrID, strings.Join(resolvedIDs.List(), ", ")) } case "ImageStreamTag": return api.LatestTaggedImage(stream, tagOrID), nil default: return nil, fmt.Errorf("invalid from.kind %q: it must be ImageStreamImage or ImageStreamTag", tagRef.From.Kind) } }
// Resolve will attempt to find an imagestream with a name that matches the passed in value func (r ImageStreamResolver) Resolve(value string) (*ComponentMatch, error) { ref, err := imageapi.ParseDockerImageReference(value) if err != nil || len(ref.Registry) != 0 { return nil, fmt.Errorf("image repositories must be of the form [<namespace>/]<name>[:<tag>|@<digest>]") } namespaces := r.Namespaces if len(ref.Namespace) != 0 { namespaces = []string{ref.Namespace} } searchTag := ref.Tag if len(searchTag) == 0 { searchTag = imageapi.DefaultImageTag } for _, namespace := range namespaces { glog.V(4).Infof("checking ImageStream %s/%s with ref %q", namespace, ref.Name, searchTag) repo, err := r.Client.ImageStreams(namespace).Get(ref.Name) if err != nil { if errors.IsNotFound(err) || errors.IsForbidden(err) { continue } return nil, err } ref.Namespace = namespace latest := imageapi.LatestTaggedImage(repo, searchTag) if latest == nil { // continue searching in the next namespace glog.V(2).Infof("no image recorded for %s/%s:%s", repo.Namespace, repo.Name, searchTag) continue } imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(ref.Name, latest.Image) if err != nil { if errors.IsNotFound(err) { // continue searching in the next namespace glog.V(2).Infof("tag %q is set, but image %q has been removed", searchTag, latest.Image) continue } return nil, err } imageData := imageStreamImage.Image ref.Registry = "" return &ComponentMatch{ Value: ref.String(), Argument: fmt.Sprintf("--image=%q", ref.String()), Name: ref.Name, Description: fmt.Sprintf("Image repository %s (tag %q) in project %s, tracks %q", repo.Name, searchTag, repo.Namespace, repo.Status.DockerImageRepository), Builder: IsBuilderImage(&imageData.DockerImageMetadata), Score: 0, ImageStream: repo, Image: &imageData.DockerImageMetadata, ImageTag: searchTag, }, nil } return nil, ErrNoMatch{value: value} }
// Get retrieves an image that has been tagged by stream and tag. `id` is of the format // <stream name>:<tag>. func (r *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { name, tag, err := nameAndTag(id) if err != nil { return nil, err } stream, err := r.imageStreamRegistry.GetImageStream(ctx, name) if err != nil { return nil, err } event := api.LatestTaggedImage(stream, tag) if event == nil || len(event.Image) == 0 { return nil, errors.NewNotFound("imageStreamTag", id) } image, err := r.imageRegistry.GetImage(ctx, event.Image) if err != nil { return nil, err } // if the stream has Spec.Tags[tag].Annotations[k] = v, copy it to the image's annotations if stream.Spec.Tags != nil { if tagRef, ok := stream.Spec.Tags[tag]; ok { if image.Annotations == nil { image.Annotations = make(map[string]string) } for k, v := range tagRef.Annotations { image.Annotations[k] = v } } } imageWithMetadata, err := api.ImageWithMetadata(*image) if err != nil { return nil, err } ist := api.ImageStreamTag{ ObjectMeta: kapi.ObjectMeta{ Namespace: kapi.NamespaceValue(ctx), Name: id, }, Image: *imageWithMetadata, } return &ist, nil }
func (r *ImageStreamByAnnotationResolver) annotationMatches(stream *imageapi.ImageStream, value string) []*ComponentMatch { if stream.Spec.Tags == nil { glog.Infof("No tags found on image, returning nil") return nil } matches := []*ComponentMatch{} for tag, tagref := range stream.Spec.Tags { if tagref.Annotations == nil { continue } supports, ok := tagref.Annotations[supportsAnnotationKey] if !ok { continue } score, ok := matchSupportsAnnotation(value, supports) if !ok { continue } latest := imageapi.LatestTaggedImage(stream, tag) if latest == nil { continue } imageStream, err := r.ImageStreamImages.ImageStreamImages(stream.Namespace).Get(stream.Name, latest.Image) if err != nil { glog.V(2).Infof("Could not retrieve image stream image for stream %q, tag %q: %v", stream.Name, tag, err) continue } if imageStream == nil { continue } imageData := imageStream.Image match := &ComponentMatch{ Value: value, Name: stream.Name, Argument: fmt.Sprintf("--image=%q", value), Description: fmt.Sprintf("Image stream %s in project %s, tracks %q", stream.Name, stream.Namespace, stream.Status.DockerImageRepository), Builder: IsBuilderImage(&imageData.DockerImageMetadata), Score: score, ImageStream: stream, Image: &imageData.DockerImageMetadata, ImageTag: imageapi.DefaultImageTag, } matches = append(matches, match) } return matches }
// HandleImageRepo processes the next ImageStream event. func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageStream) error { glog.V(4).Infof("Build image change controller detected ImageStream change %s", repo.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 // TODO: this is inefficient for _, bc := range c.BuildConfigStore.List() { config := bc.(*buildapi.BuildConfig) from := buildutil.GetImageStreamForStrategy(config.Parameters.Strategy) if from == nil || from.Kind != "ImageStreamTag" { continue } shouldBuild := false triggeredImage := "" // For every ImageChange trigger find the latest tagged image from the image repository and replace that value // throughout the build strategies. 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 _, trigger := range config.Triggers { if trigger.Type != buildapi.ImageChangeBuildTriggerType { continue } fromStreamName := getImageStreamNameFromReference(from) fromNamespace := from.Namespace if len(fromNamespace) == 0 { fromNamespace = config.Namespace } // only trigger a build if this image repo matches the name and namespace of the 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(repo.Status.DockerImageRepository) == 0 || fromStreamName != repo.Name || fromNamespace != repo.Namespace { continue } // This split is safe because ImageStreamTag names always have the form // name:tag. tag := strings.Split(from.Name, ":")[1] latest := imageapi.LatestTaggedImage(repo, tag) if latest == nil { glog.V(4).Infof("unable to find tagged image: no image recorded for %s/%s:%s", repo.Namespace, repo.Name, tag) continue } glog.V(4).Infof("Found ImageStream %s/%s with tag %s", repo.Namespace, repo.Name, tag) // (must be different) to trigger a build last := trigger.ImageChange.LastTriggeredImageID next := latest.DockerImageReference 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) // instantiate new build request := &buildapi.BuildRequest{ ObjectMeta: kapi.ObjectMeta{ Name: config.Name, Namespace: config.Namespace, }, TriggeredByImage: &kapi.ObjectReference{ Kind: "DockerImage", Name: triggeredImage, }, } if _, err := c.BuildConfigInstantiator.Instantiate(config.Namespace, request); err != nil { if kerrors.IsConflict(err) { util.HandleError(fmt.Errorf("unable to instantiate Build for BuildConfig %s/%s due to a conflicting update: %v", config.Namespace, config.Name, err)) } else { util.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", repo.Status.DockerImageRepository) } return nil }
// Generate returns a potential future DeploymentConfig based on the DeploymentConfig specified // by namespace and name. Returns a RESTful error. func (g *DeploymentConfigGenerator) Generate(ctx kapi.Context, name string) (*deployapi.DeploymentConfig, error) { config, err := g.Client.GetDeploymentConfig(ctx, name) if err != nil { return nil, err } // Update the containers with new images based on defined triggers configChanged := false errs := fielderrors.ValidationErrorList{} causes := []*deployapi.DeploymentCause{} for i, trigger := range config.Triggers { params := trigger.ImageChangeParams // Only process image change triggers if trigger.Type != deployapi.DeploymentTriggerOnImageChange { continue } // Find the image repo referred to by the trigger params imageStream, err := g.findImageStream(config, params) if err != nil { f := fmt.Sprintf("triggers[%d].imageChange.from", i) v := params.From.Name if len(params.RepositoryName) > 0 { f = fmt.Sprintf("triggers[%d].imageChange.repositoryName", i) v = params.RepositoryName } errs = append(errs, fielderrors.NewFieldInvalid(f, v, err.Error())) continue } // Find the latest tag event for the trigger tag latestEvent := imageapi.LatestTaggedImage(imageStream, params.Tag) if latestEvent == nil { f := fmt.Sprintf("triggers[%d].imageChange.tag", i) errs = append(errs, fielderrors.NewFieldInvalid(f, params.Tag, fmt.Sprintf("no image recorded for %s/%s:%s", imageStream.Namespace, imageStream.Name, params.Tag))) continue } // Update containers template := config.Template.ControllerTemplate.Template names := util.NewStringSet(params.ContainerNames...) containerChanged := false for i := range template.Spec.Containers { container := &template.Spec.Containers[i] if !names.Has(container.Name) { continue } if len(latestEvent.DockerImageReference) > 0 && container.Image != latestEvent.DockerImageReference { // Update the image container.Image = latestEvent.DockerImageReference // Log the last triggered image ID params.LastTriggeredImage = latestEvent.DockerImageReference containerChanged = true } } // If any container was updated, create a cause for the change if containerChanged { configChanged = true causes = append(causes, &deployapi.DeploymentCause{ Type: deployapi.DeploymentTriggerOnImageChange, ImageTrigger: &deployapi.DeploymentCauseImageTrigger{ RepositoryName: latestEvent.DockerImageReference, Tag: params.Tag, }, }) } } if len(errs) > 0 { return nil, errors.NewInvalid("DeploymentConfig", config.Name, errs) } // Bump the version if we updated containers or if this is an initial // deployment if configChanged || config.LatestVersion == 0 { config.Details = &deployapi.DeploymentDetails{ Causes: causes, } config.LatestVersion++ } return config, nil }