Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/completions/bash/oadm
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,7 @@ _oadm_build-chain()
flags+=("--all")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--reverse")
flags+=("--trigger-only")
flags+=("--api-version=")
flags+=("--certificate-authority=")
Expand Down
1 change: 1 addition & 0 deletions contrib/completions/bash/oc
Original file line number Diff line number Diff line change
Expand Up @@ -4398,6 +4398,7 @@ _oc_adm_build-chain()
flags+=("--all")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--reverse")
flags+=("--trigger-only")
flags+=("--api-version=")
flags+=("--certificate-authority=")
Expand Down
3 changes: 3 additions & 0 deletions contrib/completions/bash/openshift
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,7 @@ _openshift_admin_build-chain()
flags+=("--all")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--reverse")
flags+=("--trigger-only")
flags+=("--api-version=")
flags+=("--certificate-authority=")
Expand Down Expand Up @@ -7949,6 +7950,7 @@ _openshift_cli_adm_build-chain()
flags+=("--all")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--reverse")
flags+=("--trigger-only")
flags+=("--api-version=")
flags+=("--certificate-authority=")
Expand Down Expand Up @@ -14690,6 +14692,7 @@ _openshift_ex_build-chain()
flags+=("--all")
flags+=("--output=")
two_word_flags+=("-o")
flags+=("--reverse")
flags+=("--trigger-only")
flags+=("--api-version=")
flags+=("--certificate-authority=")
Expand Down
11 changes: 11 additions & 0 deletions pkg/api/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,17 @@ func RemoveInboundEdges(nodes []graph.Node) EdgeFunc {
}
}

func RemoveOutboundEdges(nodes []graph.Node) EdgeFunc {
return func(g Interface, from, to graph.Node, edgeKinds sets.String) bool {
for _, node := range nodes {
if node == from {
return false
}
}
return true
}
}

// EdgeSubgraph returns the directed subgraph with only the edges that match the
// provided function.
func (g Graph) EdgeSubgraph(edgeFn EdgeFunc) Graph {
Expand Down
56 changes: 48 additions & 8 deletions pkg/cmd/cli/describe/chaindescriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import (
)

// NotFoundErr is returned when the imageStreamTag (ist) of interest cannot
// be found in the graph. This doesn't mean though that the ist does not
// be found in the graph. This doesn't mean though that the IST does not
// exist. A user may have an image stream without a build configuration
// pointing at it. In that case, the ist of interest simply doesn't have
// pointing at it. In that case, the IST of interest simply doesn't have
// other dependant ists
type NotFoundErr string

Expand Down Expand Up @@ -79,7 +79,7 @@ func (d *ChainDescriber) MakeGraph() (osgraph.Graph, error) {
// image stream tag (name:tag) in namespace. Namespace is needed here
// because image stream tags with the same name can be found across
// different namespaces.
func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImages bool) (string, error) {
func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImages, reverse bool) (string, error) {
g, err := d.MakeGraph()
if err != nil {
return "", err
Expand All @@ -96,8 +96,13 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImag
buildInputEdgeKinds = append(buildInputEdgeKinds, buildedges.BuildInputImageEdgeKind)
}

// Partition down to the subgraph containing the ist of interest
partitioned := partition(g, istNode, buildInputEdgeKinds)
// Partition down to the subgraph containing the imagestreamtag of interest
var partitioned osgraph.Graph
if reverse {
partitioned = partitionReverse(g, istNode, buildInputEdgeKinds)
} else {
partitioned = partition(g, istNode, buildInputEdgeKinds)
}

switch strings.ToLower(d.outputFormat) {
case "dot":
Expand All @@ -107,7 +112,7 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImag
}
return string(data), nil
case "":
return d.humanReadableOutput(partitioned, d.namer, istNode), nil
return d.humanReadableOutput(partitioned, d.namer, istNode, reverse), nil
}

return "", fmt.Errorf("unknown specified format %q", d.outputFormat)
Expand All @@ -124,7 +129,7 @@ func partition(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) o
edgeFn := osgraph.EdgesOfKind(edgeKinds...)
sub := g.Subgraph(nodeFn, edgeFn)

// Filter out inbound edges to the ist of interest
// Filter out inbound edges to the IST of interest
edgeFn = osgraph.RemoveInboundEdges([]graph.Node{root})
sub = sub.Subgraph(nodeFn, edgeFn)

Expand All @@ -144,11 +149,46 @@ func partition(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) o
return sub.SubgraphWithNodes(desired, osgraph.ExistingDirectEdge)
}

// partitionReverse the graph down to a subgraph starting from the given root
func partitionReverse(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) osgraph.Graph {
// Filter out all but BuildConfig and ImageStreamTag nodes
nodeFn := osgraph.NodesOfKind(buildgraph.BuildConfigNodeKind, imagegraph.ImageStreamTagNodeKind)
// Filter out all but BuildInputImage and BuildOutput edges
edgeKinds := []string{}
edgeKinds = append(edgeKinds, buildInputEdgeKinds...)
edgeKinds = append(edgeKinds, buildedges.BuildOutputEdgeKind)
edgeFn := osgraph.EdgesOfKind(edgeKinds...)
sub := g.Subgraph(nodeFn, edgeFn)

// Filter out inbound edges to the IST of interest
edgeFn = osgraph.RemoveOutboundEdges([]graph.Node{root})
sub = sub.Subgraph(nodeFn, edgeFn)

// Check all paths leading from the root node, collect any
// node found in them, and create the desired subgraph
desired := []graph.Node{root}
paths := path.DijkstraAllPaths(sub)
for _, node := range sub.Nodes() {
if node == root {
continue
}
path, _, _ := paths.Between(node, root)
if len(path) != 0 {
desired = append(desired, node)
}
}
return sub.SubgraphWithNodes(desired, osgraph.ExistingDirectEdge)
}

// humanReadableOutput traverses the provided graph using DFS and outputs it
// in a human-readable format. It starts from the provided root, assuming it
// is an imageStreamTag node and continues to the rest of the graph handling
// only imageStreamTag and buildConfig nodes.
func (d *ChainDescriber) humanReadableOutput(g osgraph.Graph, f osgraph.Namer, root graph.Node) string {
func (d *ChainDescriber) humanReadableOutput(g osgraph.Graph, f osgraph.Namer, root graph.Node, reverse bool) string {
if reverse {
g = g.EdgeSubgraph(osgraph.ReverseExistingDirectEdge)
}

var singleNamespace bool
if len(d.namespaces) == 1 && !d.namespaces.Has(kapi.NamespaceAll) {
singleNamespace = true
Expand Down
21 changes: 20 additions & 1 deletion pkg/cmd/cli/describe/chaindescriber_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestChainDescriber(t *testing.T) {
testName string
namespaces sets.String
output string
reverse bool
defaultNamespace string
name string
tag string
Expand Down Expand Up @@ -161,6 +162,24 @@ func TestChainDescriber(t *testing.T) {
"\t\tistag/parent3img:latest": 1,
},
},
{
testName: "human readable - multiple triggers - triggeronly - reverse",
name: "child2img",
reverse: true,
defaultNamespace: "test",
tag: "latest",
path: "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml",
namespaces: sets.NewString("test"),
humanReadable: map[string]int{
"istag/child2img:latest": 1,
"\tbc/child2": 1,
"\t\tistag/parent1img:latest": 1,
"\t\t\tbc/parent1": 1,
"\t\t\t\tistag/ruby-22-centos7:latest": 2,
"\t\tistag/parent3img:latest": 1,
"\t\t\tbc/parent3": 1,
},
},
}

for _, test := range tests {
Expand All @@ -174,7 +193,7 @@ func TestChainDescriber(t *testing.T) {
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)
desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist, test.includeInputImg, test.reverse)
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)
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/experimental/buildchain/buildchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type BuildChainOptions struct {
namespaces sets.String
allNamespaces bool
triggerOnly bool
reverse bool

output string

Expand Down Expand Up @@ -77,6 +78,7 @@ func NewCmdBuildChain(name, fullName string, f *clientcmd.Factory, out io.Writer

cmd.Flags().BoolVar(&options.allNamespaces, "all", false, "Build dependency tree for the specified image stream tag across all namespaces")
cmd.Flags().BoolVar(&options.triggerOnly, "trigger-only", true, "If true, only include dependencies based on build triggers. If false, include all dependencies.")
cmd.Flags().BoolVar(&options.reverse, "reverse", false, "If true, show the istags dependencies instead of its dependants.")
cmd.Flags().StringVarP(&options.output, "output", "o", "", "Output format of dependency tree")
return cmd
}
Expand Down Expand Up @@ -160,7 +162,7 @@ func (o *BuildChainOptions) Validate() error {
func (o *BuildChainOptions) RunBuildChain() error {
ist := imagegraph.MakeImageStreamTagObjectMeta2(o.defaultNamespace, o.name)

desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly)
desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly, o.reverse)
if err != nil {
if _, isNotFoundErr := err.(describe.NotFoundErr); isNotFoundErr {
name, tag, _ := imageapi.SplitImageStreamTag(o.name)
Expand Down