// BuildConfig adds a graph node for the specific build config if it does not exist, // and will link the build config to other nodes for the images and source repositories // it depends on. func BuildConfig(g MutableUniqueGraph, config *build.BuildConfig) graph.Node { node, found := g.FindOrCreate( UniqueName(fmt.Sprintf("%d|%s/%s", BuildConfigGraphKind, config.Namespace, config.Name)), func(node Node) graph.Node { return &BuildConfigNode{ Node: node, BuildConfig: config, } }, ) if found { return node } output := config.Parameters.Output to := output.To switch { case to != nil && len(to.Name) > 0: out := ImageStreamTag(g, defaultNamespace(to.Namespace, config.Namespace), to.Name, output.Tag) g.AddEdge(node, out, BuildOutputGraphEdgeKind) case len(output.DockerImageReference) > 0: out := DockerRepository(g, output.DockerImageReference, output.Tag) g.AddEdge(node, out, BuildOutputGraphEdgeKind) } if in, ok := SourceRepository(g, config.Parameters.Source); ok { g.AddEdge(in, node, BuildInputGraphEdgeKind) } from := buildutil.GetImageStreamForStrategy(config.Parameters.Strategy) if from != nil { switch from.Kind { case "DockerImage": if ref, err := image.ParseDockerImageReference(from.Name); err == nil { tag := ref.Tag ref.Tag = "" in := DockerRepository(g, ref.String(), tag) g.AddEdge(in, node, BuildInputImageGraphEdgeKind) } case "ImageStream": tag := image.DefaultImageTag in := ImageStreamTag(g, defaultNamespace(from.Namespace, config.Namespace), from.Name, tag) g.AddEdge(in, node, BuildInputImageGraphEdgeKind) case "ImageStreamTag": name, tag, _ := image.SplitImageStreamTag(from.Name) in := ImageStreamTag(g, defaultNamespace(from.Namespace, config.Namespace), name, tag) g.AddEdge(in, node, BuildInputImageGraphEdgeKind) case "ImageStreamImage": glog.V(4).Infof("Ignoring ImageStreamImage reference in BuildConfig %s/%s", config.Namespace, config.Name) } } return node }
// getStreams iterates over a given set of build configurations // and extracts all the image streams which trigger a // build when the image stream is updated func getStreams(configs []buildapi.BuildConfig) map[string][]string { glog.V(1).Infof("Scanning BuildConfigs") avoidDuplicates := make(map[string][]string) for _, cfg := range configs { glog.V(1).Infof("Scanning BuildConfigs %v", cfg) for _, tr := range cfg.Triggers { glog.V(1).Infof("Scanning trigger %v", tr) from := buildutil.GetImageStreamForStrategy(cfg.Parameters.Strategy) glog.V(1).Infof("Strategy from= %v", from) if tr.ImageChange != nil && from != nil && from.Name != "" { glog.V(1).Infof("found ICT with from %s kind %s", from.Name, from.Kind) var name, tag string switch from.Kind { case "ImageStreamTag": bits := strings.Split(from.Name, ":") name = bits[0] tag = bits[1] default: // ImageStreamImage and DockerImage are never updated and so never // trigger builds. continue } var stream string switch from.Namespace { case "": stream = join(cfg.Namespace, name) default: stream = join(from.Namespace, name) } uniqueTag := true for _, prev := range avoidDuplicates[stream] { if prev == tag { uniqueTag = false break } } glog.V(1).Infof("checking unique tag %v %s", uniqueTag, tag) if uniqueTag { avoidDuplicates[stream] = append(avoidDuplicates[stream], tag) } } } } return avoidDuplicates }
// findStreamDeps accepts an image stream and a list of build // configurations and returns the dependency tree of the specified // image stream func findStreamDeps(stream, tag string, buildConfigList []buildapi.BuildConfig) (*Node, error) { root := &Node{ FullName: stream, Tags: []string{tag}, } namespace, name, err := split(stream) if err != nil { return nil, err } // Search all build configurations in order to find the image // streams depending on the specified image stream var childNamespace, childName, childTag string for _, cfg := range buildConfigList { for _, tr := range cfg.Triggers { from := buildutil.GetImageStreamForStrategy(cfg.Parameters.Strategy) if from == nil || from.Kind != "ImageStreamTag" || tr.ImageChange == nil { continue } // Setup zeroed fields to their default values if from.Namespace == "" { from.Namespace = cfg.Namespace } fromTag := strings.Split(from.Name, ":")[1] parentStream := namespace + "/" + name + ":" + tag if buildutil.NameFromImageStream("", from, fromTag) == parentStream { // Either To & Tag or DockerImageReference will be used as output if cfg.Parameters.Output.To != nil && cfg.Parameters.Output.To.Name != "" { childName = cfg.Parameters.Output.To.Name childTag = cfg.Parameters.Output.Tag if cfg.Parameters.Output.To.Namespace != "" { childNamespace = cfg.Parameters.Output.To.Namespace } else { childNamespace = cfg.Namespace } } else { ref, err := imageapi.ParseDockerImageReference(cfg.Parameters.Output.DockerImageReference) if err != nil { return nil, err } childName = ref.Name childTag = ref.Tag childNamespace = cfg.Namespace } childStream := join(childNamespace, childName) // Build all children and their dependency trees recursively child, err := findStreamDeps(childStream, childTag, buildConfigList) if err != nil { return nil, err } // Add edge between root and child cfgFullName := join(cfg.Namespace, cfg.Name) root.Edges = append(root.Edges, NewEdge(cfgFullName, child.FullName)) // If the child depends on root via more than one tag, we have to make sure // that only one single instance of the child will make it into root.Children cont := false for _, stream := range root.Children { if stream.FullName == child.FullName { // Just pass the tag along and discard the current child stream.Tags = append(stream.Tags, child.Tags...) cont = true break } } if cont { // Do not append this child in root.Children. It's already in there continue } root.Children = append(root.Children, child) } } } return root, 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 }