func (t tagService) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { imageStream, err := t.repo.getImageStream() if err != nil { context.GetLogger(ctx).Errorf("error retrieving ImageStream %s/%s: %v", t.repo.namespace, t.repo.name, err) return distribution.Descriptor{}, distribution.ErrRepositoryUnknown{Name: t.repo.Named().Name()} } te := imageapi.LatestTaggedImage(imageStream, tag) if te == nil { return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} } dgst, err := digest.ParseDigest(te.Image) if err != nil { return distribution.Descriptor{}, err } if !t.repo.pullthrough { image, err := t.repo.getImage(dgst) if err != nil { return distribution.Descriptor{}, err } if !isImageManaged(image) { return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} } } return distribution.Descriptor{Digest: dgst}, nil }
func tagReferenceToTagEvent(stream *api.ImageStream, tagRef api.TagReference, tagOrID string) (*api.TagEvent, error) { switch tagRef.From.Kind { case "DockerImage": return &api.TagEvent{ Created: unversioned.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: unversioned.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) } }
func tagReferenceToTagEvent(stream *api.ImageStream, tagRef api.TagReference, tagOrID string) (*api.TagEvent, error) { var ( event *api.TagEvent err error ) switch tagRef.From.Kind { case "DockerImage": event = &api.TagEvent{ Created: unversioned.Now(), DockerImageReference: tagRef.From.Name, } case "ImageStreamImage": event, err = api.ResolveImageID(stream, tagOrID) case "ImageStreamTag": event, err = api.LatestTaggedImage(stream, tagOrID), nil default: err = fmt.Errorf("invalid from.kind %q: it must be DockerImage, ImageStreamImage or ImageStreamTag", tagRef.From.Kind) } if err != nil { return nil, err } if event != nil && tagRef.Generation != nil { event.Generation = *tagRef.Generation } return event, nil }
// FromStream generates an ImageRef from an OpenShift ImageStream func (g *imageRefGenerator) FromStream(stream *imageapi.ImageStream, tag string) (*ImageRef, error) { imageRef := &ImageRef{ Stream: stream, } if tagged := imageapi.LatestTaggedImage(stream, tag); tagged != nil { if ref, err := imageapi.ParseDockerImageReference(tagged.DockerImageReference); err == nil { imageRef.ResolvedReference = &ref imageRef.Reference = ref } } if pullSpec := stream.Status.DockerImageRepository; len(pullSpec) != 0 { ref, err := imageapi.ParseDockerImageReference(pullSpec) if err != nil { return nil, err } imageRef.Reference = ref } switch { case len(tag) > 0: imageRef.Reference.Tag = tag case len(tag) == 0 && len(imageRef.Reference.Tag) == 0: imageRef.Reference.Tag = imageapi.DefaultImageTag } return imageRef, nil }
func (t tagService) Untag(ctx context.Context, tag string) error { imageStream, err := t.repo.getImageStream() if err != nil { context.GetLogger(ctx).Errorf("error retrieving ImageStream %s/%s: %v", t.repo.namespace, t.repo.name, err) return distribution.ErrRepositoryUnknown{Name: t.repo.Named().Name()} } te := imageapi.LatestTaggedImage(imageStream, tag) if te == nil { return distribution.ErrTagUnknown{Tag: tag} } if !t.repo.pullthrough { dgst, err := digest.ParseDigest(te.Image) if err != nil { return err } image, err := t.repo.getImage(dgst) if err != nil { return err } if !isImageManaged(image) { return distribution.ErrTagUnknown{Tag: tag} } } return t.repo.registryOSClient.ImageStreamTags(imageStream.Namespace).Delete(imageStream.Name, tag) }
// 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 }
// imageFor retrieves the most recent image for a tag in a given imageStreem. func (r *REST) imageFor(ctx kapi.Context, tag string, imageStream *api.ImageStream) (*api.Image, error) { event := api.LatestTaggedImage(imageStream, tag) if event == nil || len(event.Image) == 0 { return nil, kapierrors.NewNotFound("imageStreamTag", api.JoinImageStreamTag(imageStream.Name, tag)) } return r.imageRegistry.GetImage(ctx, event.Image) }
// getImageReferenceForObjectReference returns corresponding docker image reference for the given object // reference representing either an image stream image or image stream tag or docker image. func (c *GenericImageStreamUsageComputer) getImageReferenceForObjectReference( namespace string, objRef *kapi.ObjectReference, ) (imageapi.DockerImageReference, error) { switch objRef.Kind { case "ImageStreamImage": nameParts := strings.Split(objRef.Name, "@") if len(nameParts) != 2 { return imageapi.DockerImageReference{}, fmt.Errorf("failed to parse name of imageStreamImage %q", objRef.Name) } res, err := imageapi.ParseDockerImageReference(objRef.Name) if err != nil { return imageapi.DockerImageReference{}, err } if res.Namespace == "" { res.Namespace = objRef.Namespace } if res.Namespace == "" { res.Namespace = namespace } return res, nil case "ImageStreamTag": // This is really fishy. An admission check can be easily worked around by setting a tag reference // to an ImageStreamTag with no or small image and then tagging a large image to the source tag. // TODO: Shall we refuse an ImageStreamTag set in the spec if the quota is set? nameParts := strings.Split(objRef.Name, ":") if len(nameParts) != 2 { return imageapi.DockerImageReference{}, fmt.Errorf("failed to parse name of imageStreamTag %q", objRef.Name) } ns := namespace if len(objRef.Namespace) > 0 { ns = objRef.Namespace } isName := nameParts[0] is, err := c.getImageStream(ns, isName) if err != nil { return imageapi.DockerImageReference{}, fmt.Errorf("failed to get imageStream for ImageStreamTag %s/%s: %v", ns, objRef.Name, err) } event := imageapi.LatestTaggedImage(is, nameParts[1]) if event == nil || len(event.DockerImageReference) == 0 { return imageapi.DockerImageReference{}, fmt.Errorf("%q is not currently pointing to an image, cannot use it as the source of a tag", objRef.Name) } return imageapi.ParseDockerImageReference(event.DockerImageReference) case "DockerImage": managedByOS, ref := imageReferenceBelongsToInternalRegistry(objRef.Name) if !managedByOS { return imageapi.DockerImageReference{}, fmt.Errorf("DockerImage %s does not belong to internal registry", objRef.Name) } return ref, nil } return imageapi.DockerImageReference{}, fmt.Errorf("unsupported object reference kind %s", objRef.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} }
func (r *ImageStreamByAnnotationSearcher) 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 } // indicate the server knows how to directly tag images var meta map[string]string if imageStream.Generation > 0 { meta = map[string]string{"direct-tag": "1"} } imageData := imageStream.Image matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) glog.V(5).Infof("ImageStreamAnnotationSearcher match found: %s for %s with score %f", matchName, value, score) match := &ComponentMatch{ Value: value, Name: fmt.Sprintf("%s", matchName), Argument: fmt.Sprintf("--image-stream=%q", matchName), Description: fmt.Sprintf("Image stream %s in project %s", stream.Name, stream.Namespace), Score: score, ImageStream: stream, Image: &imageData.DockerImageMetadata, ImageTag: tag, Meta: meta, } matches = append(matches, match) } return matches }
func newISTag(tag string, imageStream *api.ImageStream, image *api.Image) (*api.ImageStreamTag, error) { istagName := api.JoinImageStreamTag(imageStream.Name, tag) event := api.LatestTaggedImage(imageStream, tag) if event == nil || len(event.Image) == 0 { return nil, kapierrors.NewNotFound("imageStreamTag", istagName) } ist := &api.ImageStreamTag{ ObjectMeta: kapi.ObjectMeta{ Namespace: imageStream.Namespace, Name: istagName, CreationTimestamp: event.Created, Annotations: map[string]string{}, ResourceVersion: imageStream.ResourceVersion, }, } // if the imageStream has Spec.Tags[tag].Annotations[k] = v, copy it to the image's annotations // and add them to the istag's annotations if imageStream.Spec.Tags != nil { if tagRef, ok := imageStream.Spec.Tags[tag]; ok { if image != nil && image.Annotations == nil { image.Annotations = make(map[string]string) } for k, v := range tagRef.Annotations { ist.Annotations[k] = v if image != nil { image.Annotations[k] = v } } } } if image != nil { imageWithMetadata, err := api.ImageWithMetadata(*image) if err != nil { return nil, err } ist.Image = *imageWithMetadata } else { ist.Image = api.Image{} ist.Image.Name = event.Image } // Replace the DockerImageReference with the value from event, which contains // real value from status. This should fix the problem for v1 registries, // where mutliple tags point to a single id and only the first image's metadata // is saved. This in turn will always return the pull spec from the first // imported image, which might be different than the requested tag. ist.Image.DockerImageReference = event.DockerImageReference return ist, nil }
// 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, CreationTimestamp: event.Created, }, Image: *imageWithMetadata, } return &ist, nil }
func tagReferenceToTagEvent(stream *api.ImageStream, tagRef api.TagReference, tagOrID string) (*api.TagEvent, error) { switch tagRef.From.Kind { case "DockerImage": return &api.TagEvent{ Created: unversioned.Now(), DockerImageReference: tagRef.From.Name, }, nil case "ImageStreamImage": return api.ResolveImageID(stream, tagOrID) case "ImageStreamTag": return api.LatestTaggedImage(stream, tagOrID), nil default: return nil, fmt.Errorf("invalid from.kind %q: it must be DockerImage, ImageStreamImage or ImageStreamTag", tagRef.From.Kind) } }
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-stream=%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 }
// processTriggers will go over all deployment triggers that require processing and update // the deployment config accordingly. This contains the work that the image change controller // had been doing up to the point we got the /instantiate endpoint. func processTriggers(config *deployapi.DeploymentConfig, isn client.ImageStreamsNamespacer, force bool) error { errs := []error{} // Process any image change triggers. for _, trigger := range config.Spec.Triggers { if trigger.Type != deployapi.DeploymentTriggerOnImageChange { continue } params := trigger.ImageChangeParams // Forced deployments should always try to resolve the images in the template. // On the other hand, paused deployments or non-automatic triggers shouldn't. if !force && (config.Spec.Paused || !params.Automatic) { continue } // Tag references are already validated name, tag, _ := imageapi.SplitImageStreamTag(params.From.Name) stream, err := isn.ImageStreams(params.From.Namespace).Get(name) if err != nil { if !errors.IsNotFound(err) { errs = append(errs, err) } continue } // Find the latest tag event for the trigger reference. latestEvent := imageapi.LatestTaggedImage(stream, tag) if latestEvent == nil { continue } // Ensure a change occurred latestRef := latestEvent.DockerImageReference if len(latestRef) == 0 || latestRef == params.LastTriggeredImage { continue } // Update containers 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 } if container.Image != latestRef { // Update the image container.Image = latestRef // Log the last triggered image ID params.LastTriggeredImage = latestRef } } } if err := utilerrors.NewAggregate(errs); err != nil { return errors.NewInternalError(err) } 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 := field.ErrorList{} causes := []deployapi.DeploymentCause{} for i, trigger := range config.Spec.Triggers { params := trigger.ImageChangeParams // Only process image change triggers if trigger.Type != deployapi.DeploymentTriggerOnImageChange { continue } name, tag, ok := imageapi.SplitImageStreamTag(params.From.Name) if !ok { f := field.NewPath("triggers").Index(i).Child("imageChange", "from") errs = append(errs, field.Invalid(f, name, err.Error())) continue } // Find the image repo referred to by the trigger params imageStream, err := g.findImageStream(config, params) if err != nil { f := field.NewPath("triggers").Index(i).Child("imageChange", "from") errs = append(errs, field.Invalid(f, name, err.Error())) continue } // Find the latest tag event for the trigger tag latestEvent := imageapi.LatestTaggedImage(imageStream, tag) if latestEvent == nil { f := field.NewPath("triggers").Index(i).Child("imageChange", "tag") errs = append(errs, field.Invalid(f, tag, fmt.Sprintf("no image recorded for %s/%s:%s", imageStream.Namespace, imageStream.Name, tag))) continue } // Update containers template := config.Spec.Template names := sets.NewString(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{ From: kapi.ObjectReference{ Name: imageapi.JoinImageStreamTag(imageStream.Name, tag), Kind: "ImageStreamTag", }, }, }) } } if len(errs) > 0 { return nil, errors.NewInvalid(deployapi.Kind("DeploymentConfig"), config.Name, errs) } // Bump the version if we updated containers or if this is an initial // deployment if configChanged || config.Status.LatestVersion == 0 { config.Status.Details = &deployapi.DeploymentDetails{ Causes: causes, } config.Status.LatestVersion++ } return config, nil }
// Search will attempt to find imagestreams with names that matches the passed in value func (r ImageStreamSearcher) Search(terms ...string) (ComponentMatches, error) { componentMatches := ComponentMatches{} for _, term := range terms { ref, err := imageapi.ParseDockerImageReference(term) if err != nil || len(ref.Registry) != 0 { return nil, fmt.Errorf("image streams 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 ImageStreams %s/%s with ref %q", namespace, ref.Name, searchTag) streams, err := r.Client.ImageStreams(namespace).List(labels.Everything(), fields.Everything()) if err != nil { if errors.IsNotFound(err) || errors.IsForbidden(err) { continue } return nil, err } ref.Namespace = namespace for i := 0; i < len(streams.Items); i++ { stream := streams.Items[i] score, scored := imageStreamScorer(stream, ref.Name) if scored { imageref, _ := imageapi.ParseDockerImageReference(term) imageref.Name = stream.Name latest := imageapi.LatestTaggedImage(&stream, searchTag) if latest == nil { glog.V(2).Infof("no image recorded for %s/%s:%s", stream.Namespace, stream.Name, searchTag) componentMatches = append(componentMatches, &ComponentMatch{ Value: imageref.String(), Argument: fmt.Sprintf("--image-stream=%q", imageref.String()), Name: imageref.Name, Description: fmt.Sprintf("Image stream %s in project %s, tracks %q", stream.Name, stream.Namespace, stream.Status.DockerImageRepository), Score: 0.5 + score, ImageStream: &stream, ImageTag: searchTag, }) continue } imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(stream.Name, latest.Image) if err != nil { if errors.IsNotFound(err) { // continue searching glog.V(2).Infof("tag %q is set, but image %q has been removed", searchTag, latest.Image) continue } return nil, err } imageData := imageStreamImage.Image imageref.Registry = "" componentMatches = append(componentMatches, &ComponentMatch{ Value: imageref.String(), Argument: fmt.Sprintf("--image-stream=%q", imageref.String()), Name: imageref.Name, Description: fmt.Sprintf("Image stream %q (tag %q) in project %q, tracks %q", stream.Name, searchTag, stream.Namespace, stream.Status.DockerImageRepository), Builder: IsBuilderImage(&imageData.DockerImageMetadata), Score: score, ImageStream: &stream, Image: &imageData.DockerImageMetadata, ImageTag: searchTag, }) } } } } return componentMatches, nil }
func TestTrackingTags(t *testing.T) { client, server, storage := setup(t) defer server.Terminate(t) stream := &api.ImageStream{ ObjectMeta: kapi.ObjectMeta{ Namespace: "default", Name: "stream", }, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ "tracking": { From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: "2.0", }, }, "tracking2": { From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: "2.0", }, }, }, }, Status: api.ImageStreamStatus{ Tags: map[string]api.TagEventList{ "tracking": { Items: []api.TagEvent{ { DockerImageReference: "foo/bar@sha256:1234", Image: "1234", }, }, }, "nontracking": { Items: []api.TagEvent{ { DockerImageReference: "bar/baz@sha256:9999", Image: "9999", }, }, }, "2.0": { Items: []api.TagEvent{ { DockerImageReference: "foo/bar@sha256:1234", Image: "1234", }, }, }, }, }, } _, err := client.Create( context.TODO(), etcdtest.AddPrefix("/imagestreams/default/stream"), runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(v1.SchemeGroupVersion), stream), ) if err != nil { t.Fatalf("Unexpected error: %v", err) } image := &api.Image{ ObjectMeta: kapi.ObjectMeta{ Name: "5678", }, DockerImageReference: "foo/bar@sha256:5678", } mapping := api.ImageStreamMapping{ ObjectMeta: kapi.ObjectMeta{ Namespace: "default", Name: "stream", }, Image: *image, Tag: "2.0", } _, err = storage.Create(kapi.NewDefaultContext(), &mapping) if err != nil { t.Fatalf("Unexpected error creating mapping: %v", err) } stream, err = storage.imageStreamRegistry.GetImageStream(kapi.NewDefaultContext(), "stream") if err != nil { t.Fatalf("error extracting updated stream: %v", err) } for _, trackingTag := range []string{"tracking", "tracking2"} { tracking := api.LatestTaggedImage(stream, trackingTag) if tracking == nil { t.Fatalf("unexpected nil %s TagEvent", trackingTag) } if e, a := image.DockerImageReference, tracking.DockerImageReference; e != a { t.Errorf("dockerImageReference: expected %s, got %s", e, a) } if e, a := image.Name, tracking.Image; e != a { t.Errorf("image: expected %s, got %s", e, a) } } nonTracking := api.LatestTaggedImage(stream, "nontracking") if nonTracking == nil { t.Fatal("unexpected nil nontracking TagEvent") } if e, a := "bar/baz@sha256:9999", nonTracking.DockerImageReference; e != a { t.Errorf("dockerImageReference: expected %s, got %s", e, a) } if e, a := "9999", nonTracking.Image; e != a { t.Errorf("image: expected %s, got %s", e, a) } }
// Complete completes all the required options for the tag command. func (o *TagOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error { if len(args) < 2 && (len(args) < 1 && !o.deleteTag) { return cmdutil.UsageError(cmd, "you must specify a source and at least one destination or one or more tags to delete") } // Setup writer. o.out = out // Setup client. var err error o.osClient, _, err = f.Clients() if err != nil { return err } // Setup namespace. defaultNamespace, _, err := f.DefaultNamespace() if err != nil { return err } // Populate source. if !o.deleteTag { source := args[0] glog.V(3).Infof("Using %q as a source tag", source) sourceKind := o.sourceKind if len(sourceKind) > 0 { sourceKind = determineSourceKind(f, sourceKind) } if len(sourceKind) > 0 { validSources := sets.NewString("imagestreamtag", "istag", "imagestreamimage", "isimage", "docker", "dockerimage") if !validSources.Has(strings.ToLower(sourceKind)) { cmdutil.CheckErr(cmdutil.UsageError(cmd, "invalid source %q; valid values are %v", o.sourceKind, strings.Join(validSources.List(), ", "))) } } ref, err := imageapi.ParseDockerImageReference(source) if err != nil { return fmt.Errorf("invalid SOURCE: %v", err) } switch sourceKind { case "ImageStreamTag", "ImageStreamImage": if len(ref.Registry) > 0 { return fmt.Errorf("server in SOURCE is only allowed when providing a Docker image") } if ref.Namespace == imageapi.DockerDefaultNamespace { ref.Namespace = defaultNamespace } if sourceKind == "ImageStreamTag" { if len(ref.Tag) == 0 { return fmt.Errorf("--source=ImageStreamTag requires a valid <name>:<tag> in SOURCE") } } else { if len(ref.ID) == 0 { return fmt.Errorf("--source=ImageStreamImage requires a valid <name>@<id> in SOURCE") } } case "": if len(ref.Registry) > 0 { sourceKind = "DockerImage" break } if len(ref.ID) > 0 { sourceKind = "ImageStreamImage" break } if len(ref.Tag) > 0 { sourceKind = "ImageStreamTag" break } sourceKind = "DockerImage" } // if we are not aliasing the tag, specify the exact value to copy if sourceKind == "ImageStreamTag" && !o.aliasTag { srcNamespace := ref.Namespace if len(srcNamespace) == 0 { srcNamespace = defaultNamespace } is, err := o.osClient.ImageStreams(srcNamespace).Get(ref.Name) if err != nil { return err } event := imageapi.LatestTaggedImage(is, ref.Tag) if event == nil { return fmt.Errorf("%q is not currently pointing to an image, cannot use it as the source of a tag", args[0]) } if len(event.Image) == 0 { imageRef, err := imageapi.ParseDockerImageReference(event.DockerImageReference) if err != nil { return fmt.Errorf("the image stream tag %q has an invalid pull spec and cannot be used to tag: %v", args[0], err) } ref = imageRef sourceKind = "DockerImage" } else { ref.ID = event.Image ref.Tag = "" sourceKind = "ImageStreamImage" } } args = args[1:] o.sourceKind = sourceKind o.ref = ref glog.V(3).Infof("Source tag %s %#v", o.sourceKind, o.ref) } // Populate destinations. for _, arg := range args { destNamespace, destNameAndTag, err := parseStreamName(defaultNamespace, arg) if err != nil { return err } o.destNamespace = append(o.destNamespace, destNamespace) o.destNameAndTag = append(o.destNameAndTag, destNameAndTag) glog.V(3).Infof("Using \"%s/%s\" as a destination tag", destNamespace, destNameAndTag) } return nil }
// 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 }
// 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 (latestVersion == 0) 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 (latestVersion > 0) // shouldn't have their images updated. if !params.Automatic && config.Status.LatestVersion != 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 { 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 }
// Search will attempt to find imagestreams with names that matches the passed in value func (r ImageStreamSearcher) Search(terms ...string) (ComponentMatches, error) { componentMatches := ComponentMatches{} for _, term := range terms { ref, err := imageapi.ParseDockerImageReference(term) if err != nil || len(ref.Registry) != 0 { return nil, fmt.Errorf("image streams 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 { foundExactInNamespace := false glog.V(4).Infof("checking ImageStreams %s/%s with ref %q", namespace, ref.Name, searchTag) streams, err := r.Client.ImageStreams(namespace).List(kapi.ListOptions{}) if err != nil { if errors.IsNotFound(err) || errors.IsForbidden(err) { continue } return nil, err } ref.Namespace = namespace for i := range streams.Items { stream := &streams.Items[i] score, scored := imageStreamScorer(*stream, ref.Name) if !scored { glog.V(2).Infof("unscored %s: %v", stream.Name, score) continue } // indicate the server knows how to directly tag images var meta map[string]string if stream.Generation > 0 { meta = map[string]string{"direct-tag": "1"} } imageref, _ := imageapi.ParseDockerImageReference(term) imageref.Name = stream.Name imageref.Registry = "" matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) latest := imageapi.LatestTaggedImage(stream, searchTag) if latest == nil || len(latest.Image) == 0 { glog.V(2).Infof("no image recorded for %s/%s:%s", stream.Namespace, stream.Name, searchTag) componentMatches = append(componentMatches, &ComponentMatch{ Value: imageref.Exact(), Argument: fmt.Sprintf("--image-stream=%q", matchName), Name: matchName, Description: fmt.Sprintf("Image stream %s in project %s", stream.Name, stream.Namespace), Score: 0.5 + score, ImageStream: stream, ImageTag: searchTag, Meta: meta, }) continue } imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(stream.Name, latest.Image) if err != nil { if errors.IsNotFound(err) { // continue searching glog.V(2).Infof("tag %q is set, but image %q has been removed", searchTag, latest.Image) continue } return nil, err } imageData := imageStreamImage.Image match := &ComponentMatch{ Value: imageref.Exact(), Argument: fmt.Sprintf("--image-stream=%q", imageref.Exact()), Name: imageref.Name, Description: fmt.Sprintf("Image stream %q (tag %q) in project %q", stream.Name, searchTag, stream.Namespace), Score: score, ImageStream: stream, Image: &imageData.DockerImageMetadata, ImageTag: searchTag, Meta: meta, } glog.V(2).Infof("Adding %s as component match for %q with score %v", match.Description, term, match.Score) if match.Score == 0.0 { foundExactInNamespace = true } componentMatches = append(componentMatches, match) } // If we found an exact match in this namespace, do not continue looking at // other namespaces if foundExactInNamespace && r.StopOnMatch { break } } } return componentMatches, nil }
func TestTrackingTags(t *testing.T) { etcdClient, etcdHelper, storage := setup(t) _ = etcdClient stream := api.ImageStream{ ObjectMeta: kapi.ObjectMeta{ Namespace: "default", Name: "stream", }, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ "tracking": { From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: "2.0", }, }, "tracking2": { From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: "2.0", }, }, }, }, Status: api.ImageStreamStatus{ Tags: map[string]api.TagEventList{ "tracking": { Items: []api.TagEvent{ { DockerImageReference: "foo/bar@sha256:1234", Image: "1234", }, }, }, "nontracking": { Items: []api.TagEvent{ { DockerImageReference: "bar/baz@sha256:9999", Image: "9999", }, }, }, "2.0": { Items: []api.TagEvent{ { DockerImageReference: "foo/bar@sha256:1234", Image: "1234", }, }, }, }, }, } if err := etcdHelper.CreateObj("/imagestreams/default/stream", &stream, nil, 0); err != nil { t.Fatalf("Unable to create stream: %v", err) } image := &api.Image{ ObjectMeta: kapi.ObjectMeta{ Name: "5678", }, DockerImageReference: "foo/bar@sha256:5678", } mapping := api.ImageStreamMapping{ ObjectMeta: kapi.ObjectMeta{ Namespace: "default", Name: "stream", }, Image: *image, Tag: "2.0", } _, err := storage.Create(kapi.NewDefaultContext(), &mapping) if err != nil { t.Fatalf("Unexpected error creating mapping: %v", err) } if err := etcdHelper.ExtractObj("/imagestreams/default/stream", &stream, false); err != nil { t.Fatalf("error extracting updated stream: %v", err) } for _, trackingTag := range []string{"tracking", "tracking2"} { tracking := api.LatestTaggedImage(&stream, trackingTag) if tracking == nil { t.Fatalf("unexpected nil %s TagEvent", trackingTag) } if e, a := image.DockerImageReference, tracking.DockerImageReference; e != a { t.Errorf("dockerImageReference: expected %s, got %s", e, a) } if e, a := image.Name, tracking.Image; e != a { t.Errorf("image: expected %s, got %s", e, a) } } nonTracking := api.LatestTaggedImage(&stream, "nontracking") if nonTracking == nil { t.Fatal("unexpected nil nontracking TagEvent") } if e, a := "bar/baz@sha256:9999", nonTracking.DockerImageReference; e != a { t.Errorf("dockerImageReference: expected %s, got %s", e, a) } if e, a := "9999", nonTracking.Image; e != a { t.Errorf("image: expected %s, got %s", e, a) } }
// Create registers a new image (if it doesn't exist) and updates the // specified ImageStream's tags. If attempts to update the ImageStream fail // with a resource conflict, the update will be retried if the newer // ImageStream has no tag diffs from the previous state. If tag diffs are // detected, the conflict error is returned. func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil { return nil, err } mapping := obj.(*api.ImageStreamMapping) stream, err := s.findStreamForMapping(ctx, mapping) if err != nil { return nil, err } image := mapping.Image tag := mapping.Tag if len(tag) == 0 { tag = api.DefaultImageTag } if err := s.imageRegistry.CreateImage(ctx, &image); err != nil && !errors.IsAlreadyExists(err) { return nil, err } next := api.TagEvent{ Created: unversioned.Now(), DockerImageReference: image.DockerImageReference, Image: image.Name, } err = wait.ExponentialBackoff(wait.Backoff{Steps: maxRetriesOnConflict}, func() (bool, error) { lastEvent := api.LatestTaggedImage(stream, tag) next.Generation = stream.Generation if !api.AddTagEventToImageStream(stream, tag, next) { // nothing actually changed return true, nil } api.UpdateTrackingTags(stream, tag, next) _, err := s.imageStreamRegistry.UpdateImageStreamStatus(ctx, stream) if err == nil { return true, nil } if !errors.IsConflict(err) { return false, err } // If the update conflicts, get the latest stream and check for tag // updates. If the latest tag hasn't changed, retry. latestStream, findLatestErr := s.findStreamForMapping(ctx, mapping) if findLatestErr != nil { return false, findLatestErr } // no previous tag if lastEvent == nil { // The tag hasn't changed, so try again with the updated stream. stream = latestStream return false, nil } // check for tag change newerEvent := api.LatestTaggedImage(latestStream, tag) // generation and creation time differences are ignored lastEvent.Generation = newerEvent.Generation lastEvent.Created = newerEvent.Created if kapi.Semantic.DeepEqual(lastEvent, newerEvent) { // The tag hasn't changed, so try again with the updated stream. stream = latestStream return false, nil } // The tag changed, so return the conflict error back to the client. return false, err }) if err != nil { return nil, err } return &unversioned.Status{Status: unversioned.StatusSuccess}, nil }
// Search will attempt to find imagestreams with names that match the passed in value func (r ImageStreamSearcher) Search(precise bool, terms ...string) (ComponentMatches, []error) { componentMatches := ComponentMatches{} var errs []error for _, term := range terms { ref, err := imageapi.ParseDockerImageReference(term) if err != nil || len(ref.Registry) != 0 { glog.V(2).Infof("image streams must be of the form [<namespace>/]<name>[:<tag>|@<digest>], term %q did not qualify", term) continue } if term == "__imagestream_fail" { errs = append(errs, fmt.Errorf("unable to find the specified image: %s", term)) continue } namespaces := r.Namespaces if len(ref.Namespace) != 0 { namespaces = []string{ref.Namespace} } followTag := false searchTag := ref.Tag if len(searchTag) == 0 { searchTag = imageapi.DefaultImageTag followTag = true } for _, namespace := range namespaces { glog.V(4).Infof("checking ImageStreams %s/%s with ref %q", namespace, ref.Name, searchTag) exact := false streams, err := r.Client.ImageStreams(namespace).List(kapi.ListOptions{}) if err != nil { if errors.IsNotFound(err) || errors.IsForbidden(err) { continue } errs = append(errs, err) continue } original := ref ref.Namespace = namespace for i := range streams.Items { stream := &streams.Items[i] score, scored := imageStreamScorer(*stream, ref.Name) if !scored { glog.V(2).Infof("unscored %s: %v", stream.Name, score) continue } // indicate the server knows how to directly import image stream tags var meta map[string]string if stream.Generation > 0 { meta = map[string]string{"direct-tag": "1"} } imageref := original imageref.Name = stream.Name imageref.Registry = "" matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) addMatch := func(tag string, matchScore float32, image *imageapi.DockerImage, notFound bool) { name := matchName var description, argument string if len(tag) > 0 { name = fmt.Sprintf("%s:%s", name, tag) argument = fmt.Sprintf("--image-stream=%q", name) description = fmt.Sprintf("Image stream %q (tag %q) in project %q", stream.Name, tag, stream.Namespace) } else { argument = fmt.Sprintf("--image-stream=%q --allow-missing-imagestream-tags", name) description = fmt.Sprintf("Image stream %q in project %q", stream.Name, stream.Namespace) } match := &ComponentMatch{ Value: term, Argument: argument, Name: name, Description: description, Score: matchScore, ImageStream: stream, Image: image, ImageTag: tag, Meta: meta, NoTagsFound: notFound, } glog.V(2).Infof("Adding %s as component match for %q with score %v", match.Description, term, matchScore) componentMatches = append(componentMatches, match) } // When an image stream contains a tag that references another local tag, and the user has not // provided a tag themselves (i.e. they asked for mysql and we defaulted to mysql:latest), walk // the chain of references to the end. This ensures that applications can default to using a "stable" // branch by giving the control over version to the image stream author. finalTag := searchTag if specTag, ok := stream.Spec.Tags[searchTag]; ok && followTag { if specTag.From != nil && specTag.From.Kind == "ImageStreamTag" && !strings.Contains(specTag.From.Name, ":") { if imageapi.LatestTaggedImage(stream, specTag.From.Name) != nil { finalTag = specTag.From.Name } } } latest := imageapi.LatestTaggedImage(stream, finalTag) if latest == nil || len(latest.Image) == 0 { glog.V(2).Infof("no image recorded for %s/%s:%s", stream.Namespace, stream.Name, finalTag) if r.AllowMissingTags { addMatch(finalTag, score, nil, false) continue } // Find tags that do exist and return those as partial matches foundOtherTags := false for tag := range stream.Status.Tags { latest := imageapi.LatestTaggedImage(stream, tag) if latest == nil || len(latest.Image) == 0 { continue } foundOtherTags = true // If the user specified a tag in their search string (followTag == false), // then score this match lower. tagScore := score if !followTag { tagScore += 0.5 } addMatch(tag, tagScore, nil, false) } if !foundOtherTags { addMatch("", 0.5+score, nil, true) } continue } imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(stream.Name, latest.Image) if err != nil { if errors.IsNotFound(err) { // continue searching glog.V(2).Infof("tag %q is set, but image %q has been removed", finalTag, latest.Image) continue } errs = append(errs, err) continue } addMatch(finalTag, score, &imageStreamImage.Image.DockerImageMetadata, false) if score == 0.0 { exact = true } } // If we found one or more exact matches in this namespace, do not continue looking at // other namespaces if exact && precise { break } } } return componentMatches, errs }
// HandleImageStream processes the next ImageStream event. func (c *ImageChangeController) HandleImageStream(stream *imageapi.ImageStream) error { glog.V(4).Infof("Build image change controller detected ImageStream change %s", stream.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 bcs, err := c.BuildConfigIndex.GetConfigsForImageStreamTrigger(stream.Namespace, stream.Name) if err != nil { return err } for _, config := range bcs { var ( from *kapi.ObjectReference shouldBuild = false triggeredImage = "" latest *imageapi.TagEvent ) // For every ImageChange trigger find the latest tagged image from the image stream and // invoke a build using that image id. 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 that trigger for _, trigger := range config.Spec.Triggers { if trigger.Type != buildapi.ImageChangeBuildTriggerType { continue } if trigger.ImageChange.From != nil { from = trigger.ImageChange.From } else { from = buildutil.GetInputReference(config.Spec.Strategy) } if from == nil || from.Kind != "ImageStreamTag" { continue } fromStreamName, tag, ok := imageapi.SplitImageStreamTag(from.Name) if !ok { glog.Errorf("Invalid image stream tag: %s in build config %s/%s", from.Name, config.Name, config.Namespace) continue } fromNamespace := from.Namespace if len(fromNamespace) == 0 { fromNamespace = config.Namespace } // only trigger a build if this image stream matches the name and namespace of the stream 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(stream.Status.DockerImageRepository) == 0 || fromStreamName != stream.Name || fromNamespace != stream.Namespace { continue } // This split is safe because ImageStreamTag names always have the form // name:tag. latest = imageapi.LatestTaggedImage(stream, tag) if latest == nil { glog.V(4).Infof("unable to find tagged image: no image recorded for %s/%s:%s", stream.Namespace, stream.Name, tag) continue } glog.V(4).Infof("Found ImageStream %s/%s with tag %s", stream.Namespace, stream.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) buildCauses := []buildapi.BuildTriggerCause{} // instantiate new build request := &buildapi.BuildRequest{ ObjectMeta: kapi.ObjectMeta{ Name: config.Name, Namespace: config.Namespace, }, TriggeredBy: append(buildCauses, buildapi.BuildTriggerCause{ Message: "Image change", ImageChangeBuild: &buildapi.ImageChangeCause{ ImageID: latest.DockerImageReference, FromRef: from, }, }, ), TriggeredByImage: &kapi.ObjectReference{ Kind: "DockerImage", Name: triggeredImage, }, From: from, } if _, err := c.BuildConfigInstantiator.Instantiate(config.Namespace, request); err != nil { if kerrors.IsConflict(err) { utilruntime.HandleError(fmt.Errorf("unable to instantiate Build for BuildConfig %s/%s due to a conflicting update: %v", config.Namespace, config.Name, err)) } else { utilruntime.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", stream.Status.DockerImageRepository) } return nil }
// Handle processes image change triggers associated with imagestream. func (c *ImageChangeController) Handle(stream *imageapi.ImageStream) error { configs, err := c.dcLister.GetConfigsForImageStream(stream) 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 var configsToUpdate []*deployapi.DeploymentConfig for n, config := range configs { glog.V(4).Infof("Detecting image changes for deployment config %q", deployutil.LabelForDeploymentConfig(config)) hasImageChange := false for j := range config.Spec.Triggers { // because config can be copied during this loop, make sure we load from config for subsequent loops trigger := config.Spec.Triggers[j] 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 } if !hasImageChange { // create a copy prior to mutation result, err := deployutil.DeploymentConfigDeepCopy(configs[n]) if err != nil { utilruntime.HandleError(err) continue } configs[n] = result container = &configs[n].Spec.Template.Spec.Containers[i] params = configs[n].Spec.Triggers[j].ImageChangeParams } // Update the image container.Image = latestEvent.DockerImageReference // Log the last triggered image ID params.LastTriggeredImage = latestEvent.DockerImageReference hasImageChange = true } } if hasImageChange { configsToUpdate = append(configsToUpdate, configs[n]) } } // Attempt to regenerate all configs which may contain image updates anyFailed := false for _, config := range configsToUpdate { if _, err := c.dn.DeploymentConfigs(config.Namespace).Update(config); err != nil { utilruntime.HandleError(err) anyFailed = true } else { glog.V(4).Infof("Updated deployment config %q for trigger on image stream %q", deployutil.LabelForDeploymentConfig(config), imageapi.LabelForStream(stream)) } } if anyFailed { return fmt.Errorf("couldn't update some deployment configs for trigger on image stream %q", imageapi.LabelForStream(stream)) } 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 }
// newISTag initializes an image stream tag from an image stream and image. The allowEmptyEvent will create a tag even // in the event that the status tag does does not exist yet (no image has successfully been tagged) or the image is nil. func newISTag(tag string, imageStream *api.ImageStream, image *api.Image, allowEmptyEvent bool) (*api.ImageStreamTag, error) { istagName := api.JoinImageStreamTag(imageStream.Name, tag) event := api.LatestTaggedImage(imageStream, tag) if event == nil || len(event.Image) == 0 { if !allowEmptyEvent { return nil, kapierrors.NewNotFound(api.Resource("imagestreamtags"), istagName) } event = &api.TagEvent{ Created: imageStream.CreationTimestamp, } } ist := &api.ImageStreamTag{ ObjectMeta: kapi.ObjectMeta{ Namespace: imageStream.Namespace, Name: istagName, CreationTimestamp: event.Created, Annotations: map[string]string{}, ResourceVersion: imageStream.ResourceVersion, }, Generation: event.Generation, Conditions: imageStream.Status.Tags[tag].Conditions, } if imageStream.Spec.Tags != nil { if tagRef, ok := imageStream.Spec.Tags[tag]; ok { // copy the spec tag ist.Tag = &tagRef if from := ist.Tag.From; from != nil { copied := *from ist.Tag.From = &copied } if gen := ist.Tag.Generation; gen != nil { copied := *gen ist.Tag.Generation = &copied } // if the imageStream has Spec.Tags[tag].Annotations[k] = v, copy it to the image's annotations // and add them to the istag's annotations if image != nil && image.Annotations == nil { image.Annotations = make(map[string]string) } for k, v := range tagRef.Annotations { ist.Annotations[k] = v if image != nil { image.Annotations[k] = v } } } } if image != nil { if err := api.ImageWithMetadata(image); err != nil { return nil, err } image.DockerImageManifest = "" ist.Image = *image } else { ist.Image = api.Image{} ist.Image.Name = event.Image } // Replace the DockerImageReference with the value from event, which contains // real value from status. This should fix the problem for v1 registries, // where mutliple tags point to a single id and only the first image's metadata // is saved. This in turn will always return the pull spec from the first // imported image, which might be different than the requested tag. ist.Image.DockerImageReference = event.DockerImageReference return ist, nil }