Skip to content

Commit f253963

Browse files
Merge pull request #400 from grokspawn/master
provide the capability to filter mermaid output to a single package name / skipRange support (#1023)
2 parents 104da7e + 7388cba commit f253963

File tree

7 files changed

+370
-112
lines changed

7 files changed

+370
-112
lines changed

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

Lines changed: 151 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"os"
89
"sort"
910
"strings"
1011

@@ -14,17 +15,47 @@ import (
1415
"sigs.k8s.io/yaml"
1516
)
1617

18+
type MermaidWriter struct {
19+
MinEdgeName string
20+
SpecifiedPackageName string
21+
}
22+
23+
type MermaidOption func(*MermaidWriter)
24+
25+
func NewMermaidWriter(opts ...MermaidOption) *MermaidWriter {
26+
const (
27+
minEdgeName = ""
28+
specifiedPackageName = ""
29+
)
30+
m := &MermaidWriter{
31+
MinEdgeName: minEdgeName,
32+
SpecifiedPackageName: specifiedPackageName,
33+
}
34+
35+
for _, opt := range opts {
36+
opt(m)
37+
}
38+
return m
39+
}
40+
41+
func WithMinEdgeName(minEdgeName string) MermaidOption {
42+
return func(o *MermaidWriter) {
43+
o.MinEdgeName = minEdgeName
44+
}
45+
}
46+
47+
func WithSpecifiedPackageName(specifiedPackageName string) MermaidOption {
48+
return func(o *MermaidWriter) {
49+
o.SpecifiedPackageName = specifiedPackageName
50+
}
51+
}
52+
1753
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
1854
// mermaid renderers like github, mermaid.live, etc.
1955
// output is sorted lexicographically by package name, and then by channel name
2056
// if provided, minEdgeName will be used as the lower bound for edges in the output graph
2157
//
22-
// NB: Output has wrapper comments stating the skipRange edge caveat in HTML comment format, which cannot be parsed by mermaid renderers.
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.
25-
//
2658
// Example output:
27-
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
2859
// graph LR
2960
//
3061
// %% package "neuvector-certified-operator-rhmp"
@@ -40,8 +71,7 @@ import (
4071
// end
4172
//
4273
// end
43-
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
44-
func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName string) error {
74+
func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer) error {
4575
pkgs := map[string]*strings.Builder{}
4676

4777
sort.Slice(cfg.Channels, func(i, j int) bool {
@@ -53,14 +83,29 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
5383
return err
5484
}
5585

56-
if _, ok := versionMap[minEdgeName]; !ok {
57-
if minEdgeName != "" {
58-
return fmt.Errorf("unknown minimum edge name: %q", minEdgeName)
86+
// establish a 'floor' version, either specified by user or entirely open
87+
minVersion := semver.Version{Major: 0, Minor: 0, Patch: 0}
88+
89+
if writer.MinEdgeName != "" {
90+
if _, ok := versionMap[writer.MinEdgeName]; !ok {
91+
return fmt.Errorf("unknown minimum edge name: %q", writer.MinEdgeName)
5992
}
93+
minVersion = versionMap[writer.MinEdgeName]
6094
}
6195

96+
// build increasing-version-ordered bundle names, so we can meaningfully iterate over a range
97+
orderedBundles := []string{}
98+
for n, _ := range versionMap {
99+
orderedBundles = append(orderedBundles, n)
100+
}
101+
sort.Slice(orderedBundles, func(i, j int) bool {
102+
return versionMap[orderedBundles[i]].LT(versionMap[orderedBundles[j]])
103+
})
104+
105+
minEdgePackage := writer.getMinEdgePackage(&cfg)
106+
62107
for _, c := range cfg.Channels {
63-
filteredChannel := filterChannel(&c, versionMap, minEdgeName)
108+
filteredChannel := writer.filterChannel(&c, versionMap, minVersion, minEdgePackage)
64109
if filteredChannel != nil {
65110
pkgBuilder, ok := pkgs[c.Package]
66111
if !ok {
@@ -73,11 +118,10 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
73118
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, filteredChannel.Name))
74119

75120
for _, ce := range filteredChannel.Entries {
76-
if versionMap[ce.Name].GE(versionMap[minEdgeName]) {
121+
if versionMap[ce.Name].GE(minVersion) {
77122
entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
78123
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))
79124

80-
// no support for SkipRange yet
81125
if len(ce.Replaces) > 0 {
82126
replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
83127
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
@@ -88,13 +132,25 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
88132
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "skips", skipsId, s))
89133
}
90134
}
135+
if len(ce.SkipRange) > 0 {
136+
skipRange, err := semver.ParseRange(ce.SkipRange)
137+
if err == nil {
138+
for _, edgeName := range filteredChannel.Entries {
139+
if skipRange(versionMap[edgeName.Name]) {
140+
skipRangeId := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
141+
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- \"%s(%s)\" --> %s[%q]\n", entryId, ce.Name, "skipRange", ce.SkipRange, skipRangeId, edgeName.Name))
142+
}
143+
}
144+
} else {
145+
fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
146+
}
147+
}
91148
}
92149
}
93150
pkgBuilder.WriteString(" end\n")
94151
}
95152
}
96153

97-
out.Write([]byte("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n"))
98154
out.Write([]byte("graph LR\n"))
99155
pkgNames := []string{}
100156
for pname, _ := range pkgs {
@@ -109,50 +165,87 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
109165
out.Write([]byte(pkgs[pkgName].String()))
110166
out.Write([]byte(" end\n"))
111167
}
112-
out.Write([]byte("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n"))
113168

114169
return nil
115170
}
116171

117172
// filters the channel edges to include only those which are greater-than-or-equal to the edge named by startVersion
118173
// 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 == "" {
174+
func (writer *MermaidWriter) filterChannel(c *Channel, versionMap map[string]semver.Version, minVersion semver.Version, minEdgePackage string) *Channel {
175+
// short-circuit if no active filters
176+
if writer.MinEdgeName == "" && writer.SpecifiedPackageName == "" {
122177
return c
123178
}
124-
// convert the edge name to the version so we don't have to duplicate the lookup
125-
minVersion := versionMap[minEdgeName]
179+
180+
// short-circuit if channel's package doesn't match filter
181+
if writer.SpecifiedPackageName != "" && c.Package != writer.SpecifiedPackageName {
182+
return nil
183+
}
184+
185+
// short-circuit if channel package is mismatch from filter
186+
if minEdgePackage != "" && c.Package != minEdgePackage {
187+
return nil
188+
}
126189

127190
out := &Channel{Name: c.Name, Package: c.Package, Properties: c.Properties, Entries: []ChannelEntry{}}
128191
for _, ce := range c.Entries {
129192
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)
193+
if writer.MinEdgeName == "" {
194+
// no minimum-edge specified
195+
filteredCe.SkipRange = ce.SkipRange
196+
filteredCe.Replaces = ce.Replaces
197+
filteredCe.Skips = append(filteredCe.Skips, ce.Skips...)
198+
199+
// accumulate IFF there are any relevant skips/skipRange/replaces remaining or there never were any to begin with
200+
// for the case where all skip/skipRange/replaces are retained, this is effectively the original edge with validated linkages
201+
if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 || len(filteredCe.SkipRange) > 0 {
202+
out.Entries = append(out.Entries, filteredCe)
203+
} else {
204+
if len(ce.Replaces) == 0 && len(ce.SkipRange) == 0 && len(ce.Skips) == 0 {
205+
out.Entries = append(out.Entries, filteredCe)
147206
}
148207
}
149-
if len(filteredSkips) > 0 {
150-
filteredCe.Skips = filteredSkips
208+
} else {
209+
if ce.Name == writer.MinEdgeName {
210+
// edge is the 'floor', meaning that since all references are "backward references", and we don't want any references from this edge
211+
// accumulate w/o references
212+
out.Entries = append(out.Entries, filteredCe)
213+
} else {
214+
// edge needs to be filtered to determine if it is below the floor (bad) or on/above (good)
215+
if len(ce.Replaces) > 0 && versionMap[ce.Replaces].GTE(minVersion) {
216+
filteredCe.Replaces = ce.Replaces
217+
}
218+
if len(ce.Skips) > 0 {
219+
filteredSkips := []string{}
220+
for _, s := range ce.Skips {
221+
if versionMap[s].GTE(minVersion) {
222+
filteredSkips = append(filteredSkips, s)
223+
}
224+
}
225+
if len(filteredSkips) > 0 {
226+
filteredCe.Skips = filteredSkips
227+
}
228+
}
229+
if len(ce.SkipRange) > 0 {
230+
skipRange, err := semver.ParseRange(ce.SkipRange)
231+
// if skipRange can't be parsed, just don't filter based on it
232+
if err == nil && skipRange(minVersion) {
233+
// specified range includes our floor
234+
filteredCe.SkipRange = ce.SkipRange
235+
}
236+
}
237+
// accumulate IFF there are any relevant skips/skipRange/replaces remaining, or there never were any to begin with (NOP)
238+
// but the edge name satisfies the minimum-edge constraint
239+
// for the case where all skip/skipRange/replaces are retained, this is effectively `ce` but with validated linkages
240+
if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 || len(filteredCe.SkipRange) > 0 {
241+
out.Entries = append(out.Entries, filteredCe)
242+
} else {
243+
if len(ce.Replaces) == 0 && len(ce.SkipRange) == 0 && len(ce.Skips) == 0 && versionMap[filteredCe.Name].GTE(minVersion) {
244+
out.Entries = append(out.Entries, filteredCe)
245+
}
246+
}
151247
}
152248
}
153-
if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 {
154-
out.Entries = append(out.Entries, filteredCe)
155-
}
156249
}
157250

158251
if len(out.Entries) > 0 {
@@ -193,6 +286,22 @@ func getBundleVersions(cfg *DeclarativeConfig) (map[string]semver.Version, error
193286
return entries, nil
194287
}
195288

289+
func (writer *MermaidWriter) getMinEdgePackage(cfg *DeclarativeConfig) string {
290+
if writer.MinEdgeName == "" {
291+
return ""
292+
}
293+
294+
for _, c := range cfg.Channels {
295+
for _, ce := range c.Entries {
296+
if writer.MinEdgeName == ce.Name {
297+
return c.Package
298+
}
299+
}
300+
}
301+
302+
return ""
303+
}
304+
196305
func WriteJSON(cfg DeclarativeConfig, w io.Writer) error {
197306
enc := json.NewEncoder(w)
198307
enc.SetIndent("", " ")

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

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -472,16 +472,19 @@ func removeJSONWhitespace(cfg *DeclarativeConfig) {
472472

473473
func TestWriteMermaidChannels(t *testing.T) {
474474
type spec struct {
475-
name string
476-
cfg DeclarativeConfig
477-
expected string
475+
name string
476+
cfg DeclarativeConfig
477+
startEdge string
478+
packageFilter string
479+
expected string
478480
}
479481
specs := []spec{
480482
{
481-
name: "Success",
482-
cfg: buildValidDeclarativeConfig(true),
483-
expected: `<!-- PLEASE NOTE: skipRange edges are not currently displayed -->
484-
graph LR
483+
name: "SuccessNoFilters",
484+
cfg: buildValidDeclarativeConfig(true),
485+
startEdge: "",
486+
packageFilter: "",
487+
expected: `graph LR
485488
%% package "anakin"
486489
subgraph "anakin"
487490
%% channel "dark"
@@ -509,15 +512,52 @@ graph LR
509512
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]-- replaces --> boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
510513
end
511514
end
512-
<!-- PLEASE NOTE: skipRange edges are not currently displayed -->
515+
`,
516+
},
517+
{
518+
name: "SuccessMinEdgeFilter",
519+
cfg: buildValidDeclarativeConfig(true),
520+
startEdge: "anakin.v0.1.0",
521+
packageFilter: "",
522+
expected: `graph LR
523+
%% package "anakin"
524+
subgraph "anakin"
525+
%% channel "dark"
526+
subgraph anakin-dark["dark"]
527+
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
528+
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
529+
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]-- skips --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
530+
end
531+
%% channel "light"
532+
subgraph anakin-light["light"]
533+
anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
534+
end
535+
end
536+
`,
537+
},
538+
{
539+
name: "SuccessPackageNameFilter",
540+
cfg: buildValidDeclarativeConfig(true),
541+
startEdge: "",
542+
packageFilter: "boba-fett",
543+
expected: `graph LR
544+
%% package "boba-fett"
545+
subgraph "boba-fett"
546+
%% channel "mando"
547+
subgraph boba-fett-mando["mando"]
548+
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
549+
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
550+
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]-- replaces --> boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
551+
end
552+
end
513553
`,
514554
},
515555
}
516-
startVersion := ""
517556
for _, s := range specs {
518557
t.Run(s.name, func(t *testing.T) {
519558
var buf bytes.Buffer
520-
err := WriteMermaidChannels(s.cfg, &buf, startVersion)
559+
writer := NewMermaidWriter(WithMinEdgeName(s.startEdge), WithSpecifiedPackageName(s.packageFilter))
560+
err := writer.WriteChannels(s.cfg, &buf)
521561
require.NoError(t, err)
522562
require.Equal(t, s.expected, buf.String())
523563
})

0 commit comments

Comments
 (0)