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