Skip to content

Commit 2629279

Browse files
authored
initial working viz of ignored v0 edges (#1744)
* initial working viz of ignored v0 edges Signed-off-by: grokspawn <[email protected]> * initial semantic refactoring * lint updates Signed-off-by: grokspawn <[email protected]> --------- Signed-off-by: grokspawn <[email protected]>
1 parent a0e2cf8 commit 2629279

File tree

3 files changed

+130
-46
lines changed

3 files changed

+130
-46
lines changed

alpha/declcfg/write.go

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"os"
99
"path/filepath"
10+
"slices"
1011
"sort"
1112
"strings"
1213

@@ -20,6 +21,7 @@ import (
2021
type MermaidWriter struct {
2122
MinEdgeName string
2223
SpecifiedPackageName string
24+
DrawV0Semantics bool
2325
}
2426

2527
type MermaidOption func(*MermaidWriter)
@@ -32,6 +34,7 @@ func NewMermaidWriter(opts ...MermaidOption) *MermaidWriter {
3234
m := &MermaidWriter{
3335
MinEdgeName: minEdgeName,
3436
SpecifiedPackageName: specifiedPackageName,
37+
DrawV0Semantics: true,
3538
}
3639

3740
for _, opt := range opts {
@@ -52,6 +55,12 @@ func WithSpecifiedPackageName(specifiedPackageName string) MermaidOption {
5255
}
5356
}
5457

58+
func WithV0Semantics(drawV0Semantics bool) MermaidOption {
59+
return func(o *MermaidWriter) {
60+
o.DrawV0Semantics = drawV0Semantics
61+
}
62+
}
63+
5564
// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
5665
// mermaid renderers like github, mermaid.live, etc.
5766
// output is sorted lexicographically by package name, and then by channel name
@@ -124,7 +133,10 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
124133
}
125134

126135
var deprecatedPackage string
127-
deprecatedChannels := []string{}
136+
deprecatedChannelIDs := []string{}
137+
decoratedBundleIDs := map[string][]string{"deprecated": {}, "skipped": {}, "deprecatedskipped": {}}
138+
linkID := 0
139+
skippedLinkIDs := []string{}
128140

129141
for _, c := range cfg.Channels {
130142
filteredChannel := writer.filterChannel(&c, versionMap, minVersion, minEdgePackage)
@@ -137,58 +149,102 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
137149
}
138150

139151
channelID := fmt.Sprintf("%s-%s", filteredChannel.Package, filteredChannel.Name)
140-
pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", filteredChannel.Name))
141-
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, filteredChannel.Name))
152+
fmt.Fprintf(pkgBuilder, " %%%% channel %q\n", filteredChannel.Name)
153+
fmt.Fprintf(pkgBuilder, " subgraph %s[%q]\n", channelID, filteredChannel.Name)
142154

143155
if depByPackage.Has(filteredChannel.Package) {
144156
deprecatedPackage = filteredChannel.Package
145157
}
146158

147159
if depByChannel.Has(filteredChannel.Name) {
148-
deprecatedChannels = append(deprecatedChannels, channelID)
160+
deprecatedChannelIDs = append(deprecatedChannelIDs, channelID)
149161
}
150162

151-
for _, ce := range filteredChannel.Entries {
152-
if versionMap[ce.Name].GE(minVersion) {
153-
bundleDeprecation := ""
154-
if depByBundle.Has(ce.Name) {
155-
bundleDeprecation = ":::deprecated"
163+
// sort edges by decreasing version
164+
sortedEntries := make([]*ChannelEntry, 0, len(filteredChannel.Entries))
165+
for i := range filteredChannel.Entries {
166+
sortedEntries = append(sortedEntries, &filteredChannel.Entries[i])
167+
}
168+
sort.Slice(sortedEntries, func(i, j int) bool {
169+
// Sort by decreasing version: greater version comes first
170+
return versionMap[sortedEntries[i].Name].GT(versionMap[sortedEntries[j].Name])
171+
})
172+
173+
skippedEntities := sets.Set[string]{}
174+
175+
const (
176+
captureNewEntry = true
177+
processExisting = false
178+
)
179+
handleSemantics := func(edge string, linkID int, captureNew bool) {
180+
if writer.DrawV0Semantics {
181+
if captureNew {
182+
if skippedEntities.Has(edge) {
183+
skippedLinkIDs = append(skippedLinkIDs, fmt.Sprintf("%d", linkID))
184+
} else {
185+
skippedEntities.Insert(edge)
186+
}
187+
} else {
188+
if skippedEntities.Has(edge) {
189+
skippedLinkIDs = append(skippedLinkIDs, fmt.Sprintf("%d", linkID))
190+
}
156191
}
192+
}
193+
}
157194

158-
entryID := fmt.Sprintf("%s-%s", channelID, ce.Name)
159-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]%s\n", entryID, ce.Name, bundleDeprecation))
195+
for _, ce := range sortedEntries {
196+
entryID := fmt.Sprintf("%s-%s", channelID, ce.Name)
197+
fmt.Fprintf(pkgBuilder, " %s[%q]\n", entryID, ce.Name)
198+
199+
// mermaid allows specification of only a single decoration class, so any combinations must be independently represented
200+
switch {
201+
case depByBundle.Has(ce.Name) && skippedEntities.Has(ce.Name):
202+
decoratedBundleIDs["deprecatedskipped"] = append(decoratedBundleIDs["deprecatedskipped"], entryID)
203+
case depByBundle.Has(ce.Name):
204+
decoratedBundleIDs["deprecated"] = append(decoratedBundleIDs["deprecated"], entryID)
205+
case skippedEntities.Has(ce.Name):
206+
decoratedBundleIDs["skipped"] = append(decoratedBundleIDs["skipped"], entryID)
207+
}
160208

161-
if len(ce.Replaces) > 0 {
162-
replacesID := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
163-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", replacesID, ce.Replaces, "replace", entryID, ce.Name))
164-
}
165-
if len(ce.Skips) > 0 {
166-
for _, s := range ce.Skips {
167-
skipsID := fmt.Sprintf("%s-%s", channelID, s)
168-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", skipsID, s, "skip", entryID, ce.Name))
169-
}
209+
if len(ce.Skips) > 0 {
210+
for _, s := range ce.Skips {
211+
skipsID := fmt.Sprintf("%s-%s", channelID, s)
212+
fmt.Fprintf(pkgBuilder, " %s[%q]-- %s --> %s[%q]\n", skipsID, s, "skip", entryID, ce.Name)
213+
handleSemantics(s, linkID, captureNewEntry)
214+
linkID++
170215
}
171-
if len(ce.SkipRange) > 0 {
172-
skipRange, err := semver.ParseRange(ce.SkipRange)
173-
if err == nil {
174-
for _, edgeName := range filteredChannel.Entries {
175-
if skipRange(versionMap[edgeName.Name]) {
176-
skipRangeID := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
177-
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- \"%s(%s)\" --> %s[%q]\n", skipRangeID, edgeName.Name, "skipRange", ce.SkipRange, entryID, ce.Name))
178-
}
216+
}
217+
if len(ce.SkipRange) > 0 {
218+
skipRange, err := semver.ParseRange(ce.SkipRange)
219+
if err == nil {
220+
for _, edgeName := range filteredChannel.Entries {
221+
if skipRange(versionMap[edgeName.Name]) {
222+
skipRangeID := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
223+
fmt.Fprintf(pkgBuilder, " %s[%q]-- \"%s(%s)\" --> %s[%q]\n", skipRangeID, edgeName.Name, "skipRange", ce.SkipRange, entryID, ce.Name)
224+
handleSemantics(ce.Name, linkID, processExisting)
225+
linkID++
179226
}
180-
} else {
181-
fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
182227
}
228+
} else {
229+
fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
183230
}
184231
}
232+
// have to process replaces last, because applicablity can be impacted by skips
233+
if len(ce.Replaces) > 0 {
234+
replacesID := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
235+
fmt.Fprintf(pkgBuilder, " %s[%q]-- %s --> %s[%q]\n", replacesID, ce.Replaces, "replace", entryID, ce.Name)
236+
handleSemantics(ce.Name, linkID, processExisting)
237+
linkID++
238+
}
185239
}
186-
pkgBuilder.WriteString(" end\n")
240+
fmt.Fprintf(pkgBuilder, " end\n")
187241
}
188242
}
189243

190244
_, _ = out.Write([]byte("graph LR\n"))
191245
_, _ = out.Write([]byte(" classDef deprecated fill:#E8960F\n"))
246+
_, _ = out.Write([]byte(" classDef skipped stroke:#FF0000,stroke-width:4px\n"))
247+
_, _ = out.Write([]byte(" classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px\n"))
192248
pkgNames := []string{}
193249
for pname := range pkgs {
194250
pkgNames = append(pkgNames, pname)
@@ -197,22 +253,35 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
197253
return pkgNames[i] < pkgNames[j]
198254
})
199255
for _, pkgName := range pkgNames {
200-
_, _ = out.Write([]byte(fmt.Sprintf(" %%%% package %q\n", pkgName)))
201-
_, _ = out.Write([]byte(fmt.Sprintf(" subgraph %q\n", pkgName)))
256+
_, _ = fmt.Fprintf(out, " %%%% package %q\n", pkgName)
257+
_, _ = fmt.Fprintf(out, " subgraph %q\n", pkgName)
202258
_, _ = out.Write([]byte(pkgs[pkgName].String()))
203259
_, _ = out.Write([]byte(" end\n"))
204260
}
205261

206262
if deprecatedPackage != "" {
207-
_, _ = out.Write([]byte(fmt.Sprintf("style %s fill:#989695\n", deprecatedPackage)))
263+
_, _ = fmt.Fprintf(out, "style %s fill:#989695\n", deprecatedPackage)
264+
}
265+
266+
if len(deprecatedChannelIDs) > 0 {
267+
for _, deprecatedChannel := range deprecatedChannelIDs {
268+
_, _ = fmt.Fprintf(out, "style %s fill:#DCD0FF\n", deprecatedChannel)
269+
}
208270
}
209271

210-
if len(deprecatedChannels) > 0 {
211-
for _, deprecatedChannel := range deprecatedChannels {
212-
_, _ = out.Write([]byte(fmt.Sprintf("style %s fill:#DCD0FF\n", deprecatedChannel)))
272+
// express the decoration classes
273+
for key := range decoratedBundleIDs {
274+
if len(decoratedBundleIDs[key]) > 0 {
275+
b := slices.Clone(decoratedBundleIDs[key])
276+
slices.Sort(b)
277+
_, _ = fmt.Fprintf(out, "class %s %s\n", strings.Join(b, ","), key)
213278
}
214279
}
215280

281+
if len(skippedLinkIDs) > 0 {
282+
_, _ = fmt.Fprintf(out, "linkStyle %s %s\n", strings.Join(skippedLinkIDs, ","), "stroke:#FF0000,stroke-width:3px,stroke-dasharray:5;")
283+
}
284+
216285
return nil
217286
}
218287

alpha/declcfg/write_test.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,35 +527,40 @@ func TestWriteMermaidChannels(t *testing.T) {
527527
packageFilter: "",
528528
expected: `graph LR
529529
classDef deprecated fill:#E8960F
530+
classDef skipped stroke:#FF0000,stroke-width:4px
531+
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
530532
%% package "anakin"
531533
subgraph "anakin"
532534
%% channel "dark"
533535
subgraph anakin-dark["dark"]
534-
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]:::deprecated
535-
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
536-
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
537536
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
538-
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
539537
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]-- skip --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
538+
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
539+
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
540+
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
541+
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]
540542
end
541543
%% channel "light"
542544
subgraph anakin-light["light"]
543-
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]:::deprecated
544545
anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
545546
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
547+
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]
546548
end
547549
end
548550
%% package "boba-fett"
549551
subgraph "boba-fett"
550552
%% channel "mando"
551553
subgraph boba-fett-mando["mando"]
552-
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
553554
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
554555
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]-- replace --> boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
556+
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
555557
end
556558
end
557559
style anakin fill:#989695
558560
style anakin-light fill:#DCD0FF
561+
class anakin-dark-anakin.v0.0.1,anakin-light-anakin.v0.0.1 deprecated
562+
class anakin-dark-anakin.v0.1.0 skipped
563+
linkStyle 2 stroke:#FF0000,stroke-width:3px,stroke-dasharray:5;
559564
`,
560565
},
561566
{
@@ -565,13 +570,15 @@ style anakin-light fill:#DCD0FF
565570
packageFilter: "",
566571
expected: `graph LR
567572
classDef deprecated fill:#E8960F
573+
classDef skipped stroke:#FF0000,stroke-width:4px
574+
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
568575
%% package "anakin"
569576
subgraph "anakin"
570577
%% channel "dark"
571578
subgraph anakin-dark["dark"]
572-
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
573579
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
574580
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]-- skip --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
581+
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
575582
end
576583
%% channel "light"
577584
subgraph anakin-light["light"]
@@ -580,6 +587,7 @@ style anakin-light fill:#DCD0FF
580587
end
581588
style anakin fill:#989695
582589
style anakin-light fill:#DCD0FF
590+
class anakin-dark-anakin.v0.1.0 skipped
583591
`,
584592
},
585593
{
@@ -589,13 +597,15 @@ style anakin-light fill:#DCD0FF
589597
packageFilter: "boba-fett",
590598
expected: `graph LR
591599
classDef deprecated fill:#E8960F
600+
classDef skipped stroke:#FF0000,stroke-width:4px
601+
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
592602
%% package "boba-fett"
593603
subgraph "boba-fett"
594604
%% channel "mando"
595605
subgraph boba-fett-mando["mando"]
596-
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
597606
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
598607
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]-- replace --> boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
608+
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
599609
end
600610
end
601611
`,

cmd/opm/alpha/render-graph/cmd.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func NewCmd() *cobra.Command {
1818
render action.Render
1919
minEdge string
2020
specifiedPackageName string
21+
drawV0Semantics bool
2122
)
2223
cmd := &cobra.Command{
2324
Use: "render-graph [index-image | fbc-dir]",
@@ -67,13 +68,17 @@ $ opm alpha render-graph quay.io/operatorhubio/catalog:latest | \
6768
log.Fatal(err)
6869
}
6970

70-
writer := declcfg.NewMermaidWriter(declcfg.WithMinEdgeName(minEdge), declcfg.WithSpecifiedPackageName(specifiedPackageName))
71+
writer := declcfg.NewMermaidWriter(
72+
declcfg.WithMinEdgeName(minEdge),
73+
declcfg.WithSpecifiedPackageName(specifiedPackageName),
74+
declcfg.WithV0Semantics(drawV0Semantics))
7175
if err := writer.WriteChannels(*cfg, os.Stdout); err != nil {
7276
log.Fatal(err)
7377
}
7478
},
7579
}
7680
cmd.Flags().StringVar(&minEdge, "minimum-edge", "", "the channel edge to be used as the lower bound of the set of edges composing the upgrade graph; default is to include all edges")
7781
cmd.Flags().StringVarP(&specifiedPackageName, "package-name", "p", "", "a specific package name to filter output; default is to include all packages in reference")
82+
cmd.Flags().BoolVar(&drawV0Semantics, "draw-v0-semantics", false, "whether to indicate OLMv0 semantics in the output; default is to simply represent the upgrade graph")
7883
return cmd
7984
}

0 commit comments

Comments
 (0)