@@ -8,67 +8,90 @@ import (
8
8
"sort"
9
9
"strings"
10
10
11
+ "github.com/blang/semver/v4"
12
+ "github.com/operator-framework/operator-registry/alpha/property"
11
13
"k8s.io/apimachinery/pkg/util/sets"
12
14
"sigs.k8s.io/yaml"
13
15
)
14
16
15
17
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
16
18
// mermaid renderers like github, mermaid.live, etc.
17
19
// 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
18
21
//
19
22
// 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.
21
25
//
22
26
// Example output:
23
27
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
24
28
// 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
+ //
36
42
// end
37
43
// <!-- 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 {
39
45
pkgs := map [string ]* strings.Builder {}
40
46
41
47
sort .Slice (cfg .Channels , func (i , j int ) bool {
42
48
return cfg .Channels [i ].Name < cfg .Channels [j ].Name
43
49
})
44
50
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 )
50
59
}
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
63
69
}
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
+ }
68
91
}
69
92
}
93
+ pkgBuilder .WriteString (" end\n " )
70
94
}
71
- pkgBuilder .WriteString (" end\n " )
72
95
}
73
96
74
97
out .Write ([]byte ("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n " ))
@@ -91,6 +114,85 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer) error {
91
114
return nil
92
115
}
93
116
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
+
94
196
func WriteJSON (cfg DeclarativeConfig , w io.Writer ) error {
95
197
enc := json .NewEncoder (w )
96
198
enc .SetIndent ("" , " " )
0 commit comments