// AddTriggerEdges creates edges that point to named Docker image repositories for each image used in the deployment. func AddTriggerEdges(g osgraph.MutableUniqueGraph, node *deploygraph.DeploymentConfigNode) *deploygraph.DeploymentConfigNode { podTemplate := node.DeploymentConfig.Spec.Template if podTemplate == nil { return node } deployapi.EachTemplateImage( &podTemplate.Spec, deployapi.DeploymentConfigHasTrigger(node.DeploymentConfig), func(image deployapi.TemplateImage, err error) { if err != nil { return } if image.From != nil { if len(image.From.Name) == 0 { return } name, tag, _ := imageapi.SplitImageStreamTag(image.From.Name) in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(image.From.Namespace, name, tag)) g.AddEdge(in, node, TriggersDeploymentEdgeKind) return } tag := image.Ref.Tag image.Ref.Tag = "" in := imagegraph.EnsureDockerRepositoryNode(g, image.Ref.String(), tag) g.AddEdge(in, node, UsedInDeploymentEdgeKind) }) return node }
// AddInputOutputEdges links the build config to other nodes for the images and source repositories it depends on. func AddInputOutputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) *buildgraph.BuildConfigNode { output := node.BuildConfig.Parameters.Output to := output.To switch { case to != nil && len(to.Name) > 0: out := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(to.Namespace, node.BuildConfig.Namespace), to.Name, output.Tag)) g.AddEdge(node, out, BuildOutputEdgeKind) case len(output.DockerImageReference) > 0: out := imagegraph.EnsureDockerRepositoryNode(g, output.DockerImageReference, output.Tag) g.AddEdge(node, out, BuildOutputEdgeKind) } if in := buildgraph.EnsureSourceRepositoryNode(g, node.BuildConfig.Parameters.Source); in != nil { g.AddEdge(in, node, BuildInputEdgeKind) } from := buildutil.GetImageStreamForStrategy(node.BuildConfig.Parameters.Strategy) if from != nil { switch from.Kind { case "DockerImage": if ref, err := imageapi.ParseDockerImageReference(from.Name); err == nil { tag := ref.Tag ref.Tag = "" in := imagegraph.EnsureDockerRepositoryNode(g, ref.String(), tag) g.AddEdge(in, node, BuildInputImageEdgeKind) } case "ImageStream": tag := imageapi.DefaultImageTag in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name, tag)) g.AddEdge(in, node, BuildInputImageEdgeKind) case "ImageStreamTag": name, tag, _ := imageapi.SplitImageStreamTag(from.Name) in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), name, tag)) g.AddEdge(in, node, BuildInputImageEdgeKind) case "ImageStreamImage": glog.V(4).Infof("Ignoring ImageStreamImage reference in BuildConfig %s/%s", node.BuildConfig.Namespace, node.BuildConfig.Name) } } return node }
// RunBuildChain contains all the necessary functionality for the OpenShift // experimental build-chain command func (o *BuildChainOptions) RunBuildChain() error { ist := imagegraph.MakeImageStreamTagObjectMeta(o.defaultNamespace, o.name, o.tag) desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly) if err != nil { if _, isNotFoundErr := err.(describe.NotFoundErr); isNotFoundErr { // Try to get the imageStreamTag via a direct GET if _, getErr := o.t.ImageStreamTags(o.defaultNamespace).Get(o.name, o.tag); getErr != nil { return getErr } fmt.Printf("Image stream tag '%s:%s' in %q doesn't have any dependencies.\n", o.name, o.tag, o.defaultNamespace) return nil } return err } fmt.Println(desc) return nil }
func imageRefNode(g osgraph.MutableUniqueGraph, ref *kapi.ObjectReference, bc *buildapi.BuildConfig) graph.Node { if ref == nil { return nil } switch ref.Kind { case "DockerImage": if ref, err := imageapi.ParseDockerImageReference(ref.Name); err == nil { tag := ref.Tag ref.Tag = "" return imagegraph.EnsureDockerRepositoryNode(g, ref.String(), tag) } case "ImageStream": return imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name, imageapi.DefaultImageTag)) case "ImageStreamTag": return imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name)) case "ImageStreamImage": return imagegraph.FindOrCreateSyntheticImageStreamImageNode(g, imagegraph.MakeImageStreamImageObjectMeta(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name)) } return nil }
// AddInputOutputEdges links the build config to other nodes for the images and source repositories it depends on. func AddInputOutputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) *buildgraph.BuildConfigNode { output := node.BuildConfig.Spec.Output to := output.To switch { case to == nil: case to.Kind == "DockerImage": out := imagegraph.EnsureDockerRepositoryNode(g, to.Name, "") g.AddEdge(node, out, BuildOutputEdgeKind) case to.Kind == "ImageStreamTag": out := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(to.Namespace, node.BuildConfig.Namespace), to.Name)) g.AddEdge(node, out, BuildOutputEdgeKind) } if in := buildgraph.EnsureSourceRepositoryNode(g, node.BuildConfig.Spec.Source); in != nil { g.AddEdge(in, node, BuildInputEdgeKind) } from := buildutil.GetImageStreamForStrategy(node.BuildConfig.Spec.Strategy) if from != nil { switch from.Kind { case "DockerImage": if ref, err := imageapi.ParseDockerImageReference(from.Name); err == nil { tag := ref.Tag ref.Tag = "" in := imagegraph.EnsureDockerRepositoryNode(g, ref.String(), tag) g.AddEdge(in, node, BuildInputImageEdgeKind) } case "ImageStream": in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name, imageapi.DefaultImageTag)) g.AddEdge(in, node, BuildInputImageEdgeKind) case "ImageStreamTag": in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name)) g.AddEdge(in, node, BuildInputImageEdgeKind) case "ImageStreamImage": in := imagegraph.FindOrCreateSyntheticImageStreamImageNode(g, imagegraph.MakeImageStreamImageObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name)) g.AddEdge(in, node, BuildInputImageEdgeKind) } } return node }
func TestChainDescriber(t *testing.T) { tests := []struct { testName string namespaces sets.String output string defaultNamespace string name string tag string path string humanReadable map[string]int dot []string expectedErr error includeInputImg bool }{ { testName: "human readable test - single namespace", namespaces: sets.NewString("test"), output: "", defaultNamespace: "test", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/single-namespace-bcs.yaml", humanReadable: map[string]int{ "imagestreamtag/ruby-20-centos7:latest": 1, "\tbc/ruby-hello-world": 1, "\t\timagestreamtag/ruby-hello-world:latest": 1, "\tbc/ruby-sample-build": 1, "\t\timagestreamtag/origin-ruby-sample:latest": 1, }, expectedErr: nil, }, { testName: "dot test - single namespace", namespaces: sets.NewString("test"), output: "dot", defaultNamespace: "test", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/single-namespace-bcs.yaml", dot: []string{ "digraph \"ruby-20-centos7:latest\" {", "// Node definitions.", "[label=\"BuildConfig|test/ruby-hello-world\"];", "[label=\"BuildConfig|test/ruby-sample-build\"];", "[label=\"ImageStreamTag|test/ruby-hello-world:latest\"];", "[label=\"ImageStreamTag|test/ruby-20-centos7:latest\"];", "[label=\"ImageStreamTag|test/origin-ruby-sample:latest\"];", "", "// Edge definitions.", "[label=\"BuildOutput\"];", "[label=\"BuildOutput\"];", "[label=\"BuildInputImage,BuildTriggerImage\"];", "[label=\"BuildInputImage,BuildTriggerImage\"];", "}", }, expectedErr: nil, }, { testName: "human readable test - multiple namespaces", namespaces: sets.NewString("test", "master", "default"), output: "", defaultNamespace: "master", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-namespaces-bcs.yaml", humanReadable: map[string]int{ "<master imagestreamtag/ruby-20-centos7:latest>": 1, "\t<default bc/ruby-hello-world>": 1, "\t\t<test imagestreamtag/ruby-hello-world:latest>": 1, "\t<test bc/ruby-sample-build>": 1, "\t\t<another imagestreamtag/origin-ruby-sample:latest>": 1, }, expectedErr: nil, }, { testName: "dot test - multiple namespaces", namespaces: sets.NewString("test", "master", "default"), output: "dot", defaultNamespace: "master", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-namespaces-bcs.yaml", dot: []string{ "digraph \"ruby-20-centos7:latest\" {", "// Node definitions.", "[label=\"BuildConfig|default/ruby-hello-world\"];", "[label=\"BuildConfig|test/ruby-sample-build\"];", "[label=\"ImageStreamTag|test/ruby-hello-world:latest\"];", "[label=\"ImageStreamTag|master/ruby-20-centos7:latest\"];", "[label=\"ImageStreamTag|another/origin-ruby-sample:latest\"];", "", "// Edge definitions.", "[label=\"BuildOutput\"];", "[label=\"BuildOutput\"];", "[label=\"BuildInputImage,BuildTriggerImage\"];", "[label=\"BuildInputImage,BuildTriggerImage\"];", "}", }, expectedErr: nil, }, { testName: "human readable - multiple triggers - triggeronly", name: "ruby-20-centos7", defaultNamespace: "test", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml", namespaces: sets.NewString("test"), humanReadable: map[string]int{ "imagestreamtag/ruby-20-centos7:latest": 1, "\tbc/parent1": 1, "\t\timagestreamtag/parent1img:latest": 1, "\t\t\tbc/child2": 2, "\t\t\t\timagestreamtag/child2img:latest": 2, "\tbc/parent2": 1, "\t\timagestreamtag/parent2img:latest": 1, "\t\t\tbc/child3": 2, "\t\t\t\timagestreamtag/child3img:latest": 2, "\t\t\tbc/child1": 1, "\t\t\t\timagestreamtag/child1img:latest": 1, "\tbc/parent3": 1, "\t\timagestreamtag/parent3img:latest": 1, }, }, { testName: "human readable - multiple triggers - trigger+input", name: "ruby-20-centos7", defaultNamespace: "test", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml", namespaces: sets.NewString("test"), includeInputImg: true, humanReadable: map[string]int{ "imagestreamtag/ruby-20-centos7:latest": 1, "\tbc/parent1": 1, "\t\timagestreamtag/parent1img:latest": 1, "\t\t\tbc/child1": 2, "\t\t\t\timagestreamtag/child1img:latest": 2, "\t\t\tbc/child2": 2, "\t\t\t\timagestreamtag/child2img:latest": 2, "\t\t\tbc/child3": 3, "\t\t\t\timagestreamtag/child3img:latest": 3, "\tbc/parent2": 1, "\t\timagestreamtag/parent2img:latest": 1, "\tbc/parent3": 1, "\t\timagestreamtag/parent3img:latest": 1, }, }, } for _, test := range tests { o := ktestclient.NewObjects(kapi.Scheme, kapi.Scheme) if len(test.path) > 0 { if err := ktestclient.AddObjectsFromPath(test.path, o, kapi.Scheme); err != nil { t.Fatal(err) } } oc, _ := testclient.NewFixtureClients(o) ist := imagegraph.MakeImageStreamTagObjectMeta(test.defaultNamespace, test.name, test.tag) desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist, test.includeInputImg) t.Logf("%s: output:\n%s\n\n", test.testName, desc) if err != test.expectedErr { t.Fatalf("%s: error mismatch: expected %v, got %v", test.testName, test.expectedErr, err) } got := strings.Split(desc, "\n") switch test.output { case "dot": if len(test.dot) != len(got) { t.Fatalf("%s: expected %d lines, got %d:\n%s", test.testName, len(test.dot), len(got), desc) } for _, expected := range test.dot { if !strings.Contains(desc, expected) { t.Errorf("%s: unexpected description:\n%s\nexpected line in it:\n%s", test.testName, desc, expected) } } case "": if lenReadable(test.humanReadable) != len(got) { t.Fatalf("%s: expected %d lines, got %d:\n%s", test.testName, lenReadable(test.humanReadable), len(got), desc) } for _, line := range got { if _, ok := test.humanReadable[line]; !ok { t.Errorf("%s: unexpected line: %s", test.testName, line) } test.humanReadable[line]-- } for line, cnt := range test.humanReadable { if cnt != 0 { t.Errorf("%s: unexpected number of lines for [%s]: %d", test.testName, line, cnt) } } } } }
func TestChainDescriber(t *testing.T) { tests := []struct { testName string namespaces kutil.StringSet output string defaultNamespace string name string tag string path string humanReadable map[string]struct{} dot []string expectedErr error }{ { testName: "human readable test - single namespace", namespaces: kutil.NewStringSet("test"), output: "", defaultNamespace: "test", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/single-namespace-bcs.yaml", humanReadable: map[string]struct{}{ "imagestreamtag/ruby-20-centos7:latest": {}, "\tbc/ruby-hello-world": {}, "\t\timagestreamtag/ruby-hello-world:latest": {}, "\tbc/ruby-sample-build": {}, "\t\timagestreamtag/origin-ruby-sample:latest": {}, }, expectedErr: nil, }, { testName: "dot test - single namespace", namespaces: kutil.NewStringSet("test"), output: "dot", defaultNamespace: "test", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/single-namespace-bcs.yaml", dot: []string{ "digraph \"ruby-20-centos7:latest\" {", "// Node definitions.", "[label=\"BuildConfig|test/ruby-hello-world\"];", "[label=\"BuildConfig|test/ruby-sample-build\"];", "[label=\"ImageStreamTag|test/ruby-hello-world:latest\"];", "[label=\"ImageStreamTag|test/ruby-20-centos7:latest\"];", "[label=\"ImageStreamTag|test/origin-ruby-sample:latest\"];", "", "// Edge definitions.", "[label=\"BuildOutput\"];", "[label=\"BuildOutput\"];", "[label=\"BuildInputImage\"];", "[label=\"BuildInputImage\"];", "}", }, expectedErr: nil, }, { testName: "human readable test - multiple namespaces", namespaces: kutil.NewStringSet("test", "master", "default"), output: "", defaultNamespace: "master", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-namespaces-bcs.yaml", humanReadable: map[string]struct{}{ "<master imagestreamtag/ruby-20-centos7:latest>": {}, "\t<default bc/ruby-hello-world>": {}, "\t\t<test imagestreamtag/ruby-hello-world:latest>": {}, "\t<test bc/ruby-sample-build>": {}, "\t\t<another imagestreamtag/origin-ruby-sample:latest>": {}, }, expectedErr: nil, }, { testName: "dot test - multiple namespaces", namespaces: kutil.NewStringSet("test", "master", "default"), output: "dot", defaultNamespace: "master", name: "ruby-20-centos7", tag: "latest", path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-namespaces-bcs.yaml", dot: []string{ "digraph \"ruby-20-centos7:latest\" {", "// Node definitions.", "[label=\"BuildConfig|default/ruby-hello-world\"];", "[label=\"BuildConfig|test/ruby-sample-build\"];", "[label=\"ImageStreamTag|test/ruby-hello-world:latest\"];", "[label=\"ImageStreamTag|master/ruby-20-centos7:latest\"];", "[label=\"ImageStreamTag|another/origin-ruby-sample:latest\"];", "", "// Edge definitions.", "[label=\"BuildOutput\"];", "[label=\"BuildOutput\"];", "[label=\"BuildInputImage\"];", "[label=\"BuildInputImage\"];", "}", }, expectedErr: nil, }, } for _, test := range tests { o := ktestclient.NewObjects(kapi.Scheme, kapi.Scheme) if len(test.path) > 0 { if err := ktestclient.AddObjectsFromPath(test.path, o, kapi.Scheme); err != nil { t.Fatal(err) } } oc, _ := testclient.NewFixtureClients(o) ist := imagegraph.MakeImageStreamTagObjectMeta(test.defaultNamespace, test.name, test.tag) desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist) if err != test.expectedErr { t.Fatalf("%s: error mismatch: expected %v, got %v", test.testName, test.expectedErr, err) } got := strings.Split(desc, "\n") switch test.output { case "dot": if len(test.dot) != len(got) { t.Fatalf("%s: expected %d lines, got %d", test.testName, len(test.dot), len(got)) } for _, expected := range test.dot { if !strings.Contains(desc, expected) { t.Errorf("%s: unexpected description:\n%s\nexpected line in it:\n%s", test.testName, desc, expected) } } case "": if len(test.humanReadable) != len(got) { t.Fatalf("%s: expected %d lines, got %d", test.testName, len(test.humanReadable), len(got)) } for _, line := range got { if _, ok := test.humanReadable[line]; !ok { t.Errorf("%s: unexpected line: %s", test.testName, line) } } } } }