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+
196305func WriteJSON (cfg DeclarativeConfig , w io.Writer ) error {
197306 enc := json .NewEncoder (w )
198307 enc .SetIndent ("" , " " )
0 commit comments