5
5
"encoding/json"
6
6
"fmt"
7
7
"io"
8
+ "os"
8
9
"sort"
9
10
"strings"
10
11
@@ -14,17 +15,47 @@ import (
14
15
"sigs.k8s.io/yaml"
15
16
)
16
17
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
+
17
53
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
18
54
// mermaid renderers like github, mermaid.live, etc.
19
55
// output is sorted lexicographically by package name, and then by channel name
20
56
// if provided, minEdgeName will be used as the lower bound for edges in the output graph
21
57
//
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
- //
26
58
// Example output:
27
- // <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
28
59
// graph LR
29
60
//
30
61
// %% package "neuvector-certified-operator-rhmp"
@@ -40,8 +71,7 @@ import (
40
71
// end
41
72
//
42
73
// 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 {
45
75
pkgs := map [string ]* strings.Builder {}
46
76
47
77
sort .Slice (cfg .Channels , func (i , j int ) bool {
@@ -53,14 +83,29 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
53
83
return err
54
84
}
55
85
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 )
59
92
}
93
+ minVersion = versionMap [writer .MinEdgeName ]
60
94
}
61
95
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
+
62
107
for _ , c := range cfg .Channels {
63
- filteredChannel := filterChannel (& c , versionMap , minEdgeName )
108
+ filteredChannel := writer . filterChannel (& c , versionMap , minVersion , minEdgePackage )
64
109
if filteredChannel != nil {
65
110
pkgBuilder , ok := pkgs [c .Package ]
66
111
if ! ok {
@@ -73,11 +118,10 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
73
118
pkgBuilder .WriteString (fmt .Sprintf (" subgraph %s[%q]\n " , channelID , filteredChannel .Name ))
74
119
75
120
for _ , ce := range filteredChannel .Entries {
76
- if versionMap [ce .Name ].GE (versionMap [ minEdgeName ] ) {
121
+ if versionMap [ce .Name ].GE (minVersion ) {
77
122
entryId := fmt .Sprintf ("%s-%s" , channelID , ce .Name )
78
123
pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]\n " , entryId , ce .Name ))
79
124
80
- // no support for SkipRange yet
81
125
if len (ce .Replaces ) > 0 {
82
126
replacesId := fmt .Sprintf ("%s-%s" , channelID , ce .Replaces )
83
127
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
88
132
pkgBuilder .WriteString (fmt .Sprintf (" %s[%q]-- %s --> %s[%q]\n " , entryId , ce .Name , "skips" , skipsId , s ))
89
133
}
90
134
}
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
+ }
91
148
}
92
149
}
93
150
pkgBuilder .WriteString (" end\n " )
94
151
}
95
152
}
96
153
97
- out .Write ([]byte ("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n " ))
98
154
out .Write ([]byte ("graph LR\n " ))
99
155
pkgNames := []string {}
100
156
for pname , _ := range pkgs {
@@ -109,50 +165,87 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName stri
109
165
out .Write ([]byte (pkgs [pkgName ].String ()))
110
166
out .Write ([]byte (" end\n " ))
111
167
}
112
- out .Write ([]byte ("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n " ))
113
168
114
169
return nil
115
170
}
116
171
117
172
// filters the channel edges to include only those which are greater-than-or-equal to the edge named by startVersion
118
173
// 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 == "" {
122
177
return c
123
178
}
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
+ }
126
189
127
190
out := & Channel {Name : c .Name , Package : c .Package , Properties : c .Properties , Entries : []ChannelEntry {}}
128
191
for _ , ce := range c .Entries {
129
192
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 )
147
206
}
148
207
}
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
+ }
151
247
}
152
248
}
153
- if len (filteredCe .Replaces ) > 0 || len (filteredCe .Skips ) > 0 {
154
- out .Entries = append (out .Entries , filteredCe )
155
- }
156
249
}
157
250
158
251
if len (out .Entries ) > 0 {
@@ -193,6 +286,22 @@ func getBundleVersions(cfg *DeclarativeConfig) (map[string]semver.Version, error
193
286
return entries , nil
194
287
}
195
288
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
+
196
305
func WriteJSON (cfg DeclarativeConfig , w io.Writer ) error {
197
306
enc := json .NewEncoder (w )
198
307
enc .SetIndent ("" , " " )
0 commit comments