| ... | ... |
@@ -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) |