... | ... |
@@ -2130,6 +2130,7 @@ _openshift_admin_build-chain() |
2130 | 2130 |
flags+=("--all") |
2131 | 2131 |
flags+=("--output=") |
2132 | 2132 |
two_word_flags+=("-o") |
2133 |
+ flags+=("--reverse") |
|
2133 | 2134 |
flags+=("--trigger-only") |
2134 | 2135 |
flags+=("--api-version=") |
2135 | 2136 |
flags+=("--certificate-authority=") |
... | ... |
@@ -7949,6 +7950,7 @@ _openshift_cli_adm_build-chain() |
7949 | 7949 |
flags+=("--all") |
7950 | 7950 |
flags+=("--output=") |
7951 | 7951 |
two_word_flags+=("-o") |
7952 |
+ flags+=("--reverse") |
|
7952 | 7953 |
flags+=("--trigger-only") |
7953 | 7954 |
flags+=("--api-version=") |
7954 | 7955 |
flags+=("--certificate-authority=") |
... | ... |
@@ -14690,6 +14692,7 @@ _openshift_ex_build-chain() |
14690 | 14690 |
flags+=("--all") |
14691 | 14691 |
flags+=("--output=") |
14692 | 14692 |
two_word_flags+=("-o") |
14693 |
+ flags+=("--reverse") |
|
14693 | 14694 |
flags+=("--trigger-only") |
14694 | 14695 |
flags+=("--api-version=") |
14695 | 14696 |
flags+=("--certificate-authority=") |
... | ... |
@@ -434,6 +434,17 @@ func RemoveInboundEdges(nodes []graph.Node) EdgeFunc { |
434 | 434 |
} |
435 | 435 |
} |
436 | 436 |
|
437 |
+func RemoveOutboundEdges(nodes []graph.Node) EdgeFunc { |
|
438 |
+ return func(g Interface, from, to graph.Node, edgeKinds sets.String) bool { |
|
439 |
+ for _, node := range nodes { |
|
440 |
+ if node == from { |
|
441 |
+ return false |
|
442 |
+ } |
|
443 |
+ } |
|
444 |
+ return true |
|
445 |
+ } |
|
446 |
+} |
|
447 |
+ |
|
437 | 448 |
// EdgeSubgraph returns the directed subgraph with only the edges that match the |
438 | 449 |
// provided function. |
439 | 450 |
func (g Graph) EdgeSubgraph(edgeFn EdgeFunc) Graph { |
... | ... |
@@ -23,9 +23,9 @@ import ( |
23 | 23 |
) |
24 | 24 |
|
25 | 25 |
// NotFoundErr is returned when the imageStreamTag (ist) of interest cannot |
26 |
-// be found in the graph. This doesn't mean though that the ist does not |
|
26 |
+// be found in the graph. This doesn't mean though that the IST does not |
|
27 | 27 |
// exist. A user may have an image stream without a build configuration |
28 |
-// pointing at it. In that case, the ist of interest simply doesn't have |
|
28 |
+// pointing at it. In that case, the IST of interest simply doesn't have |
|
29 | 29 |
// other dependant ists |
30 | 30 |
type NotFoundErr string |
31 | 31 |
|
... | ... |
@@ -79,7 +79,7 @@ func (d *ChainDescriber) MakeGraph() (osgraph.Graph, error) { |
79 | 79 |
// image stream tag (name:tag) in namespace. Namespace is needed here |
80 | 80 |
// because image stream tags with the same name can be found across |
81 | 81 |
// different namespaces. |
82 |
-func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImages bool) (string, error) { |
|
82 |
+func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImages, reverse bool) (string, error) { |
|
83 | 83 |
g, err := d.MakeGraph() |
84 | 84 |
if err != nil { |
85 | 85 |
return "", err |
... | ... |
@@ -96,8 +96,13 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImag |
96 | 96 |
buildInputEdgeKinds = append(buildInputEdgeKinds, buildedges.BuildInputImageEdgeKind) |
97 | 97 |
} |
98 | 98 |
|
99 |
- // Partition down to the subgraph containing the ist of interest |
|
100 |
- partitioned := partition(g, istNode, buildInputEdgeKinds) |
|
99 |
+ // Partition down to the subgraph containing the imagestreamtag of interest |
|
100 |
+ var partitioned osgraph.Graph |
|
101 |
+ if reverse { |
|
102 |
+ partitioned = partitionReverse(g, istNode, buildInputEdgeKinds) |
|
103 |
+ } else { |
|
104 |
+ partitioned = partition(g, istNode, buildInputEdgeKinds) |
|
105 |
+ } |
|
101 | 106 |
|
102 | 107 |
switch strings.ToLower(d.outputFormat) { |
103 | 108 |
case "dot": |
... | ... |
@@ -107,7 +112,7 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImag |
107 | 107 |
} |
108 | 108 |
return string(data), nil |
109 | 109 |
case "": |
110 |
- return d.humanReadableOutput(partitioned, d.namer, istNode), nil |
|
110 |
+ return d.humanReadableOutput(partitioned, d.namer, istNode, reverse), nil |
|
111 | 111 |
} |
112 | 112 |
|
113 | 113 |
return "", fmt.Errorf("unknown specified format %q", d.outputFormat) |
... | ... |
@@ -124,7 +129,7 @@ func partition(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) o |
124 | 124 |
edgeFn := osgraph.EdgesOfKind(edgeKinds...) |
125 | 125 |
sub := g.Subgraph(nodeFn, edgeFn) |
126 | 126 |
|
127 |
- // Filter out inbound edges to the ist of interest |
|
127 |
+ // Filter out inbound edges to the IST of interest |
|
128 | 128 |
edgeFn = osgraph.RemoveInboundEdges([]graph.Node{root}) |
129 | 129 |
sub = sub.Subgraph(nodeFn, edgeFn) |
130 | 130 |
|
... | ... |
@@ -144,11 +149,46 @@ func partition(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) o |
144 | 144 |
return sub.SubgraphWithNodes(desired, osgraph.ExistingDirectEdge) |
145 | 145 |
} |
146 | 146 |
|
147 |
+// partitionReverse the graph down to a subgraph starting from the given root |
|
148 |
+func partitionReverse(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) osgraph.Graph { |
|
149 |
+ // Filter out all but BuildConfig and ImageStreamTag nodes |
|
150 |
+ nodeFn := osgraph.NodesOfKind(buildgraph.BuildConfigNodeKind, imagegraph.ImageStreamTagNodeKind) |
|
151 |
+ // Filter out all but BuildInputImage and BuildOutput edges |
|
152 |
+ edgeKinds := []string{} |
|
153 |
+ edgeKinds = append(edgeKinds, buildInputEdgeKinds...) |
|
154 |
+ edgeKinds = append(edgeKinds, buildedges.BuildOutputEdgeKind) |
|
155 |
+ edgeFn := osgraph.EdgesOfKind(edgeKinds...) |
|
156 |
+ sub := g.Subgraph(nodeFn, edgeFn) |
|
157 |
+ |
|
158 |
+ // Filter out inbound edges to the IST of interest |
|
159 |
+ edgeFn = osgraph.RemoveOutboundEdges([]graph.Node{root}) |
|
160 |
+ sub = sub.Subgraph(nodeFn, edgeFn) |
|
161 |
+ |
|
162 |
+ // Check all paths leading from the root node, collect any |
|
163 |
+ // node found in them, and create the desired subgraph |
|
164 |
+ desired := []graph.Node{root} |
|
165 |
+ paths := path.DijkstraAllPaths(sub) |
|
166 |
+ for _, node := range sub.Nodes() { |
|
167 |
+ if node == root { |
|
168 |
+ continue |
|
169 |
+ } |
|
170 |
+ path, _, _ := paths.Between(node, root) |
|
171 |
+ if len(path) != 0 { |
|
172 |
+ desired = append(desired, node) |
|
173 |
+ } |
|
174 |
+ } |
|
175 |
+ return sub.SubgraphWithNodes(desired, osgraph.ExistingDirectEdge) |
|
176 |
+} |
|
177 |
+ |
|
147 | 178 |
// humanReadableOutput traverses the provided graph using DFS and outputs it |
148 | 179 |
// in a human-readable format. It starts from the provided root, assuming it |
149 | 180 |
// is an imageStreamTag node and continues to the rest of the graph handling |
150 | 181 |
// only imageStreamTag and buildConfig nodes. |
151 |
-func (d *ChainDescriber) humanReadableOutput(g osgraph.Graph, f osgraph.Namer, root graph.Node) string { |
|
182 |
+func (d *ChainDescriber) humanReadableOutput(g osgraph.Graph, f osgraph.Namer, root graph.Node, reverse bool) string { |
|
183 |
+ if reverse { |
|
184 |
+ g = g.EdgeSubgraph(osgraph.ReverseExistingDirectEdge) |
|
185 |
+ } |
|
186 |
+ |
|
152 | 187 |
var singleNamespace bool |
153 | 188 |
if len(d.namespaces) == 1 && !d.namespaces.Has(kapi.NamespaceAll) { |
154 | 189 |
singleNamespace = true |
... | ... |
@@ -19,6 +19,7 @@ func TestChainDescriber(t *testing.T) { |
19 | 19 |
testName string |
20 | 20 |
namespaces sets.String |
21 | 21 |
output string |
22 |
+ reverse bool |
|
22 | 23 |
defaultNamespace string |
23 | 24 |
name string |
24 | 25 |
tag string |
... | ... |
@@ -161,6 +162,24 @@ func TestChainDescriber(t *testing.T) { |
161 | 161 |
"\t\tistag/parent3img:latest": 1, |
162 | 162 |
}, |
163 | 163 |
}, |
164 |
+ { |
|
165 |
+ testName: "human readable - multiple triggers - triggeronly - reverse", |
|
166 |
+ name: "child2img", |
|
167 |
+ reverse: true, |
|
168 |
+ defaultNamespace: "test", |
|
169 |
+ tag: "latest", |
|
170 |
+ path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml", |
|
171 |
+ namespaces: sets.NewString("test"), |
|
172 |
+ humanReadable: map[string]int{ |
|
173 |
+ "istag/child2img:latest": 1, |
|
174 |
+ "\tbc/child2": 1, |
|
175 |
+ "\t\tistag/parent1img:latest": 1, |
|
176 |
+ "\t\t\tbc/parent1": 1, |
|
177 |
+ "\t\t\t\tistag/ruby-22-centos7:latest": 2, |
|
178 |
+ "\t\tistag/parent3img:latest": 1, |
|
179 |
+ "\t\t\tbc/parent3": 1, |
|
180 |
+ }, |
|
181 |
+ }, |
|
164 | 182 |
} |
165 | 183 |
|
166 | 184 |
for _, test := range tests { |
... | ... |
@@ -174,7 +193,7 @@ func TestChainDescriber(t *testing.T) { |
174 | 174 |
oc, _ := testclient.NewFixtureClients(o) |
175 | 175 |
ist := imagegraph.MakeImageStreamTagObjectMeta(test.defaultNamespace, test.name, test.tag) |
176 | 176 |
|
177 |
- desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist, test.includeInputImg) |
|
177 |
+ desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist, test.includeInputImg, test.reverse) |
|
178 | 178 |
t.Logf("%s: output:\n%s\n\n", test.testName, desc) |
179 | 179 |
if err != test.expectedErr { |
180 | 180 |
t.Fatalf("%s: error mismatch: expected %v, got %v", test.testName, test.expectedErr, err) |
... | ... |
@@ -49,6 +49,7 @@ type BuildChainOptions struct { |
49 | 49 |
namespaces sets.String |
50 | 50 |
allNamespaces bool |
51 | 51 |
triggerOnly bool |
52 |
+ reverse bool |
|
52 | 53 |
|
53 | 54 |
output string |
54 | 55 |
|
... | ... |
@@ -77,6 +78,7 @@ func NewCmdBuildChain(name, fullName string, f *clientcmd.Factory, out io.Writer |
77 | 77 |
|
78 | 78 |
cmd.Flags().BoolVar(&options.allNamespaces, "all", false, "Build dependency tree for the specified image stream tag across all namespaces") |
79 | 79 |
cmd.Flags().BoolVar(&options.triggerOnly, "trigger-only", true, "If true, only include dependencies based on build triggers. If false, include all dependencies.") |
80 |
+ cmd.Flags().BoolVar(&options.reverse, "reverse", false, "If true, show the istags dependencies instead of its dependants.") |
|
80 | 81 |
cmd.Flags().StringVarP(&options.output, "output", "o", "", "Output format of dependency tree") |
81 | 82 |
return cmd |
82 | 83 |
} |
... | ... |
@@ -160,7 +162,7 @@ func (o *BuildChainOptions) Validate() error { |
160 | 160 |
func (o *BuildChainOptions) RunBuildChain() error { |
161 | 161 |
ist := imagegraph.MakeImageStreamTagObjectMeta2(o.defaultNamespace, o.name) |
162 | 162 |
|
163 |
- desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly) |
|
163 |
+ desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly, o.reverse) |
|
164 | 164 |
if err != nil { |
165 | 165 |
if _, isNotFoundErr := err.(describe.NotFoundErr); isNotFoundErr { |
166 | 166 |
name, tag, _ := imageapi.SplitImageStreamTag(o.name) |