Skip to content

Commit 130a8d2

Browse files
committed
move the mermaid-format graph output to its own command since this fits a different use-case than "generate valid file-based-catalog of X" (#1013)
provide the ability to specify a minimum-edge-name to filter out edges below the version of interest Signed-off-by: Jordan Keister <[email protected]> Upstream-commit: 63c19321f9f75fee5da66daeacbffbd4148dad8e Upstream-repository: operator-registry
1 parent aec6a49 commit 130a8d2

File tree

12 files changed

+397
-81
lines changed

12 files changed

+397
-81
lines changed

staging/operator-registry/alpha/declcfg/write.go

Lines changed: 137 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,67 +8,90 @@ import (
88
"sort"
99
"strings"
1010

11+
"github.com/blang/semver/v4"
12+
"github.com/operator-framework/operator-registry/alpha/property"
1113
"k8s.io/apimachinery/pkg/util/sets"
1214
"sigs.k8s.io/yaml"
1315
)
1416

1517
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
1618
// mermaid renderers like github, mermaid.live, etc.
1719
// output is sorted lexicographically by package name, and then by channel name
20+
// if provided, minEdgeName will be used as the lower bound for edges in the output graph
1821
//
1922
// NB: Output has wrapper comments stating the skipRange edge caveat in HTML comment format, which cannot be parsed by mermaid renderers.
20-
// This is deliberate, and intended as an explicit acknowledgement of the limitations, instead of requiring the user to notice the missing edges upon inspection.
23+
//
24+
// This is deliberate, and intended as an explicit acknowledgement of the limitations, instead of requiring the user to notice the missing edges upon inspection.
2125
//
2226
// Example output:
2327
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
2428
// graph LR
25-
// %% package "neuvector-certified-operator-rhmp"
26-
// subgraph "neuvector-certified-operator-rhmp"
27-
// %% channel "beta"
28-
// subgraph neuvector-certified-operator-rhmp-beta["beta"]
29-
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
30-
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
31-
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]
32-
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- replaces --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
33-
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- skips --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
34-
// end
35-
// end
29+
//
30+
// %% package "neuvector-certified-operator-rhmp"
31+
// subgraph "neuvector-certified-operator-rhmp"
32+
// %% channel "beta"
33+
// subgraph neuvector-certified-operator-rhmp-beta["beta"]
34+
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
35+
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
36+
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]
37+
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- replaces --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
38+
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- skips --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
39+
// end
40+
// end
41+
//
3642
// end
3743
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
38-
func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer) error {
44+
func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName string) error {
3945
pkgs := map[string]*strings.Builder{}
4046

4147
sort.Slice(cfg.Channels, func(i, j int) bool {
4248
return cfg.Channels[i].Name < cfg.Channels[j].Name
4349
})
4450

45-
for _, c := range cfg.Channels {
46-
pkgBuilder, ok := pkgs[c.Package]
47-
if !ok {
48-
pkgBuilder = &strings.Builder{}
49-
pkgs[c.Package] = pkgBuilder
51+
versionMap, err := getBundleVersions(&cfg)
52+
if err != nil {
53+
return err
54+
}
55+
56+
if _, ok := versionMap[minEdgeName]; !ok {
57+
if minEdgeName != "" {
58+
return fmt.Errorf("unknown minimum edge name: %q", minEdgeName)
5059
}
51-
channelID := fmt.Sprintf("%s-%s", c.Package, c.Name)
52-
pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", c.Name))
53-
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, c.Name))
54-
55-
for _, ce := range c.Entries {
56-
entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
57-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))
58-
59-
// no support for SkipRange yet
60-
if len(ce.Replaces) > 0 {
61-
replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
62-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
60+
}
61+
62+
for _, c := range cfg.Channels {
63+
filteredChannel := filterChannel(&c, versionMap, minEdgeName)
64+
if filteredChannel != nil {
65+
pkgBuilder, ok := pkgs[c.Package]
66+
if !ok {
67+
pkgBuilder = &strings.Builder{}
68+
pkgs[c.Package] = pkgBuilder
6369
}
64-
if len(ce.Skips) > 0 {
65-
for _, s := range ce.Skips {
66-
skipsId := fmt.Sprintf("%s-%s", channelID, s)
67-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "skips", skipsId, s))
70+
71+
channelID := fmt.Sprintf("%s-%s", filteredChannel.Package, filteredChannel.Name)
72+
pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", filteredChannel.Name))
73+
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, filteredChannel.Name))
74+
75+
for _, ce := range filteredChannel.Entries {
76+
if versionMap[ce.Name].GE(versionMap[minEdgeName]) {
77+
entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
78+
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))
79+
80+
// no support for SkipRange yet
81+
if len(ce.Replaces) > 0 {
82+
replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
83+
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
84+
}
85+
if len(ce.Skips) > 0 {
86+
for _, s := range ce.Skips {
87+
skipsId := fmt.Sprintf("%s-%s", channelID, s)
88+
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "skips", skipsId, s))
89+
}
90+
}
6891
}
6992
}
93+
pkgBuilder.WriteString(" end\n")
7094
}
71-
pkgBuilder.WriteString(" end\n")
7295
}
7396

7497
out.Write([]byte("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n"))
@@ -91,6 +114,85 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer) error {
91114
return nil
92115
}
93116

117+
// filters the channel edges to include only those which are greater-than-or-equal to the edge named by startVersion
118+
// returns a nil channel if all edges are filtered out
119+
func filterChannel(c *Channel, versionMap map[string]semver.Version, minEdgeName string) *Channel {
120+
// short-circuit if no specified startVersion
121+
if minEdgeName == "" {
122+
return c
123+
}
124+
// convert the edge name to the version so we don't have to duplicate the lookup
125+
minVersion := versionMap[minEdgeName]
126+
127+
out := &Channel{Name: c.Name, Package: c.Package, Properties: c.Properties, Entries: []ChannelEntry{}}
128+
for _, ce := range c.Entries {
129+
filteredCe := ChannelEntry{Name: ce.Name}
130+
// short-circuit to take the edge name (but no references to earlier versions)
131+
if ce.Name == minEdgeName {
132+
out.Entries = append(out.Entries, filteredCe)
133+
continue
134+
}
135+
// if len(ce.SkipRange) > 0 {
136+
// }
137+
if len(ce.Replaces) > 0 {
138+
if versionMap[ce.Replaces].GTE(minVersion) {
139+
filteredCe.Replaces = ce.Replaces
140+
}
141+
}
142+
if len(ce.Skips) > 0 {
143+
filteredSkips := []string{}
144+
for _, s := range ce.Skips {
145+
if versionMap[s].GTE(minVersion) {
146+
filteredSkips = append(filteredSkips, s)
147+
}
148+
}
149+
if len(filteredSkips) > 0 {
150+
filteredCe.Skips = filteredSkips
151+
}
152+
}
153+
if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 {
154+
out.Entries = append(out.Entries, filteredCe)
155+
}
156+
}
157+
158+
if len(out.Entries) > 0 {
159+
return out
160+
} else {
161+
return nil
162+
}
163+
}
164+
165+
func parseVersionProperty(b *Bundle) (*semver.Version, error) {
166+
props, err := property.Parse(b.Properties)
167+
if err != nil {
168+
return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
169+
}
170+
if len(props.Packages) != 1 {
171+
return nil, fmt.Errorf("bundle %q has multiple %q properties, expected exactly 1", b.Name, property.TypePackage)
172+
}
173+
v, err := semver.Parse(props.Packages[0].Version)
174+
if err != nil {
175+
return nil, fmt.Errorf("bundle %q has invalid version %q: %v", b.Name, props.Packages[0].Version, err)
176+
}
177+
178+
return &v, nil
179+
}
180+
181+
func getBundleVersions(cfg *DeclarativeConfig) (map[string]semver.Version, error) {
182+
entries := make(map[string]semver.Version)
183+
for index := range cfg.Bundles {
184+
if _, ok := entries[cfg.Bundles[index].Name]; !ok {
185+
ver, err := parseVersionProperty(&cfg.Bundles[index])
186+
if err != nil {
187+
return entries, err
188+
}
189+
entries[cfg.Bundles[index].Name] = *ver
190+
}
191+
}
192+
193+
return entries, nil
194+
}
195+
94196
func WriteJSON(cfg DeclarativeConfig, w io.Writer) error {
95197
enc := json.NewEncoder(w)
96198
enc.SetIndent("", " ")

staging/operator-registry/alpha/declcfg/write_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,11 @@ graph LR
513513
`,
514514
},
515515
}
516+
startVersion := ""
516517
for _, s := range specs {
517518
t.Run(s.name, func(t *testing.T) {
518519
var buf bytes.Buffer
519-
err := WriteMermaidChannels(s.cfg, &buf)
520+
err := WriteMermaidChannels(s.cfg, &buf, startVersion)
520521
require.NoError(t, err)
521522
require.Equal(t, s.expected, buf.String())
522523
})

staging/operator-registry/cmd/opm/alpha/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle"
77
"github.com/operator-framework/operator-registry/cmd/opm/alpha/diff"
88
"github.com/operator-framework/operator-registry/cmd/opm/alpha/list"
9+
rendergraph "github.com/operator-framework/operator-registry/cmd/opm/alpha/render-graph"
910
"github.com/operator-framework/operator-registry/cmd/opm/alpha/veneer"
1011
)
1112

@@ -20,6 +21,7 @@ func NewCmd() *cobra.Command {
2021
runCmd.AddCommand(
2122
bundle.NewCmd(),
2223
list.NewCmd(),
24+
rendergraph.NewCmd(),
2325
diff.NewCmd(),
2426
veneer.NewCmd(),
2527
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package rendergraph
2+
3+
import (
4+
"io"
5+
"log"
6+
"os"
7+
8+
"github.com/operator-framework/operator-registry/alpha/action"
9+
"github.com/operator-framework/operator-registry/alpha/declcfg"
10+
"github.com/operator-framework/operator-registry/cmd/opm/internal/util"
11+
"github.com/sirupsen/logrus"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func NewCmd() *cobra.Command {
16+
var (
17+
render action.Render
18+
minEdge string
19+
)
20+
cmd := &cobra.Command{
21+
Use: "render-graph [index-image | fbc-dir | bundle-image]",
22+
Short: "Generate mermaid-formatted view of upgrade graph of operators in an index",
23+
Long: `Generate mermaid-formatted view of upgrade graphs of operators in an index`,
24+
Args: cobra.MinimumNArgs(1),
25+
Run: func(cmd *cobra.Command, args []string) {
26+
// The bundle loading impl is somewhat verbose, even on the happy path,
27+
// so discard all logrus default logger logs. Any important failures will be
28+
// returned from render.Run and logged as fatal errors.
29+
logrus.SetOutput(io.Discard)
30+
31+
registry, err := util.CreateCLIRegistry(cmd)
32+
if err != nil {
33+
log.Fatal(err)
34+
}
35+
36+
render.Refs = args
37+
render.AllowedRefMask = action.RefBundleImage | action.RefDCImage | action.RefDCDir // all non-sqlite
38+
render.Registry = registry
39+
40+
cfg, err := render.Run(cmd.Context())
41+
if err != nil {
42+
log.Fatal(err)
43+
}
44+
45+
if err := declcfg.WriteMermaidChannels(*cfg, os.Stdout, minEdge); err != nil {
46+
log.Fatal(err)
47+
}
48+
},
49+
}
50+
cmd.Flags().StringVar(&minEdge, "minimum-edge", "", "the channel edge to be used as the lower bound of the set of edges composing the upgrade graph")
51+
return cmd
52+
}

staging/operator-registry/cmd/opm/alpha/veneer/semver.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ func newSemverCmd() *cobra.Command {
3636
case "yaml":
3737
write = declcfg.WriteYAML
3838
case "mermaid":
39-
write = declcfg.WriteMermaidChannels
39+
write = func(cfg declcfg.DeclarativeConfig, writer io.Writer) error {
40+
startVersion := ""
41+
return declcfg.WriteMermaidChannels(cfg, writer, startVersion)
42+
}
4043
default:
4144
return fmt.Errorf("invalid output format %q", output)
4245
}

staging/operator-registry/cmd/opm/render/cmd.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ func NewCmd() *cobra.Command {
3636
write = declcfg.WriteYAML
3737
case "json":
3838
write = declcfg.WriteJSON
39-
case "mermaid":
40-
write = declcfg.WriteMermaidChannels
4139
default:
42-
log.Fatalf("invalid --output value %q, expected (json|yaml|mermaid)", output)
40+
log.Fatalf("invalid --output value %q, expected (json|yaml)", output)
4341
}
4442

4543
// The bundle loading impl is somewhat verbose, even on the happy path,
@@ -65,7 +63,7 @@ func NewCmd() *cobra.Command {
6563
}
6664
},
6765
}
68-
cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml|mermaid)")
66+
cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml)")
6967
return cmd
7068
}
7169

0 commit comments

Comments
 (0)