Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 106 additions & 37 deletions alpha/declcfg/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"sort"
"strings"

Expand All @@ -20,6 +21,7 @@ import (
type MermaidWriter struct {
MinEdgeName string
SpecifiedPackageName string
DrawV0Semantics bool
}

type MermaidOption func(*MermaidWriter)
Expand All @@ -32,6 +34,7 @@ func NewMermaidWriter(opts ...MermaidOption) *MermaidWriter {
m := &MermaidWriter{
MinEdgeName: minEdgeName,
SpecifiedPackageName: specifiedPackageName,
DrawV0Semantics: true,
}

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

func WithV0Semantics(drawV0Semantics bool) MermaidOption {
return func(o *MermaidWriter) {
o.DrawV0Semantics = drawV0Semantics
}
}

// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
// mermaid renderers like github, mermaid.live, etc.
// output is sorted lexicographically by package name, and then by channel name
Expand Down Expand Up @@ -124,7 +133,10 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
}

var deprecatedPackage string
deprecatedChannels := []string{}
deprecatedChannelIDs := []string{}
decoratedBundleIDs := map[string][]string{"deprecated": {}, "skipped": {}, "deprecatedskipped": {}}
linkID := 0
skippedLinkIDs := []string{}

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

channelID := fmt.Sprintf("%s-%s", filteredChannel.Package, filteredChannel.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", filteredChannel.Name))
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, filteredChannel.Name))
fmt.Fprintf(pkgBuilder, " %%%% channel %q\n", filteredChannel.Name)
fmt.Fprintf(pkgBuilder, " subgraph %s[%q]\n", channelID, filteredChannel.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


if depByPackage.Has(filteredChannel.Package) {
deprecatedPackage = filteredChannel.Package
}

if depByChannel.Has(filteredChannel.Name) {
deprecatedChannels = append(deprecatedChannels, channelID)
deprecatedChannelIDs = append(deprecatedChannelIDs, channelID)
}

for _, ce := range filteredChannel.Entries {
if versionMap[ce.Name].GE(minVersion) {
bundleDeprecation := ""
if depByBundle.Has(ce.Name) {
bundleDeprecation = ":::deprecated"
// sort edges by decreasing version
sortedEntries := make([]*ChannelEntry, 0, len(filteredChannel.Entries))
for i := range filteredChannel.Entries {
sortedEntries = append(sortedEntries, &filteredChannel.Entries[i])
}
sort.Slice(sortedEntries, func(i, j int) bool {
// Sort by decreasing version: greater version comes first
return versionMap[sortedEntries[i].Name].GT(versionMap[sortedEntries[j].Name])
})

skippedEntities := sets.Set[string]{}

const (
captureNewEntry = true
processExisting = false
)
handleSemantics := func(edge string, linkID int, captureNew bool) {
if writer.DrawV0Semantics {
if captureNew {
if skippedEntities.Has(edge) {
skippedLinkIDs = append(skippedLinkIDs, fmt.Sprintf("%d", linkID))
} else {
skippedEntities.Insert(edge)
}
} else {
if skippedEntities.Has(edge) {
skippedLinkIDs = append(skippedLinkIDs, fmt.Sprintf("%d", linkID))
}
}
}
}

entryID := fmt.Sprintf("%s-%s", channelID, ce.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]%s\n", entryID, ce.Name, bundleDeprecation))
for _, ce := range sortedEntries {
entryID := fmt.Sprintf("%s-%s", channelID, ce.Name)
fmt.Fprintf(pkgBuilder, " %s[%q]\n", entryID, ce.Name)

// mermaid allows specification of only a single decoration class, so any combinations must be independently represented
switch {
case depByBundle.Has(ce.Name) && skippedEntities.Has(ce.Name):
decoratedBundleIDs["deprecatedskipped"] = append(decoratedBundleIDs["deprecatedskipped"], entryID)
case depByBundle.Has(ce.Name):
decoratedBundleIDs["deprecated"] = append(decoratedBundleIDs["deprecated"], entryID)
case skippedEntities.Has(ce.Name):
decoratedBundleIDs["skipped"] = append(decoratedBundleIDs["skipped"], entryID)
}

if len(ce.Replaces) > 0 {
replacesID := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", replacesID, ce.Replaces, "replace", entryID, ce.Name))
}
if len(ce.Skips) > 0 {
for _, s := range ce.Skips {
skipsID := fmt.Sprintf("%s-%s", channelID, s)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", skipsID, s, "skip", entryID, ce.Name))
}
if len(ce.Skips) > 0 {
for _, s := range ce.Skips {
skipsID := fmt.Sprintf("%s-%s", channelID, s)
fmt.Fprintf(pkgBuilder, " %s[%q]-- %s --> %s[%q]\n", skipsID, s, "skip", entryID, ce.Name)
handleSemantics(s, linkID, captureNewEntry)
linkID++
}
if len(ce.SkipRange) > 0 {
skipRange, err := semver.ParseRange(ce.SkipRange)
if err == nil {
for _, edgeName := range filteredChannel.Entries {
if skipRange(versionMap[edgeName.Name]) {
skipRangeID := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- \"%s(%s)\" --> %s[%q]\n", skipRangeID, edgeName.Name, "skipRange", ce.SkipRange, entryID, ce.Name))
}
}
if len(ce.SkipRange) > 0 {
skipRange, err := semver.ParseRange(ce.SkipRange)
if err == nil {
for _, edgeName := range filteredChannel.Entries {
if skipRange(versionMap[edgeName.Name]) {
skipRangeID := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
fmt.Fprintf(pkgBuilder, " %s[%q]-- \"%s(%s)\" --> %s[%q]\n", skipRangeID, edgeName.Name, "skipRange", ce.SkipRange, entryID, ce.Name)
handleSemantics(ce.Name, linkID, processExisting)
linkID++
}
} else {
fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
}
} else {
fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
}
}
// have to process replaces last, because applicablity can be impacted by skips
if len(ce.Replaces) > 0 {
replacesID := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
fmt.Fprintf(pkgBuilder, " %s[%q]-- %s --> %s[%q]\n", replacesID, ce.Replaces, "replace", entryID, ce.Name)
handleSemantics(ce.Name, linkID, processExisting)
linkID++
}
}
pkgBuilder.WriteString(" end\n")
fmt.Fprintf(pkgBuilder, " end\n")
}
}

_, _ = out.Write([]byte("graph LR\n"))
_, _ = out.Write([]byte(" classDef deprecated fill:#E8960F\n"))
_, _ = out.Write([]byte(" classDef skipped stroke:#FF0000,stroke-width:4px\n"))
_, _ = out.Write([]byte(" classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px\n"))
pkgNames := []string{}
for pname := range pkgs {
pkgNames = append(pkgNames, pname)
Expand All @@ -197,22 +253,35 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer)
return pkgNames[i] < pkgNames[j]
})
for _, pkgName := range pkgNames {
_, _ = out.Write([]byte(fmt.Sprintf(" %%%% package %q\n", pkgName)))
_, _ = out.Write([]byte(fmt.Sprintf(" subgraph %q\n", pkgName)))
_, _ = fmt.Fprintf(out, " %%%% package %q\n", pkgName)
_, _ = fmt.Fprintf(out, " subgraph %q\n", pkgName)
_, _ = out.Write([]byte(pkgs[pkgName].String()))
_, _ = out.Write([]byte(" end\n"))
}

if deprecatedPackage != "" {
_, _ = out.Write([]byte(fmt.Sprintf("style %s fill:#989695\n", deprecatedPackage)))
_, _ = fmt.Fprintf(out, "style %s fill:#989695\n", deprecatedPackage)
}

if len(deprecatedChannelIDs) > 0 {
for _, deprecatedChannel := range deprecatedChannelIDs {
_, _ = fmt.Fprintf(out, "style %s fill:#DCD0FF\n", deprecatedChannel)
}
}

if len(deprecatedChannels) > 0 {
for _, deprecatedChannel := range deprecatedChannels {
_, _ = out.Write([]byte(fmt.Sprintf("style %s fill:#DCD0FF\n", deprecatedChannel)))
// express the decoration classes
for key := range decoratedBundleIDs {
if len(decoratedBundleIDs[key]) > 0 {
b := slices.Clone(decoratedBundleIDs[key])
slices.Sort(b)
_, _ = fmt.Fprintf(out, "class %s %s\n", strings.Join(b, ","), key)
}
}

if len(skippedLinkIDs) > 0 {
_, _ = fmt.Fprintf(out, "linkStyle %s %s\n", strings.Join(skippedLinkIDs, ","), "stroke:#FF0000,stroke-width:3px,stroke-dasharray:5;")
}

return nil
}

Expand Down
26 changes: 18 additions & 8 deletions alpha/declcfg/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,35 +527,40 @@ func TestWriteMermaidChannels(t *testing.T) {
packageFilter: "",
expected: `graph LR
classDef deprecated fill:#E8960F
classDef skipped stroke:#FF0000,stroke-width:4px
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
%% package "anakin"
subgraph "anakin"
%% channel "dark"
subgraph anakin-dark["dark"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]:::deprecated
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]-- skip --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]
end
%% channel "light"
subgraph anakin-light["light"]
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]:::deprecated
anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]-- replace --> anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
anakin-light-anakin.v0.0.1["anakin.v0.0.1"]
end
end
%% package "boba-fett"
subgraph "boba-fett"
%% channel "mando"
subgraph boba-fett-mando["mando"]
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
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"]
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
end
end
style anakin fill:#989695
style anakin-light fill:#DCD0FF
class anakin-dark-anakin.v0.0.1,anakin-light-anakin.v0.0.1 deprecated
class anakin-dark-anakin.v0.1.0 skipped
linkStyle 2 stroke:#FF0000,stroke-width:3px,stroke-dasharray:5;
`,
},
{
Expand All @@ -565,13 +570,15 @@ style anakin-light fill:#DCD0FF
packageFilter: "",
expected: `graph LR
classDef deprecated fill:#E8960F
classDef skipped stroke:#FF0000,stroke-width:4px
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
%% package "anakin"
subgraph "anakin"
%% channel "dark"
subgraph anakin-dark["dark"]
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]-- skip --> anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
end
%% channel "light"
subgraph anakin-light["light"]
Expand All @@ -580,6 +587,7 @@ style anakin-light fill:#DCD0FF
end
style anakin fill:#989695
style anakin-light fill:#DCD0FF
class anakin-dark-anakin.v0.1.0 skipped
`,
},
{
Expand All @@ -589,13 +597,15 @@ style anakin-light fill:#DCD0FF
packageFilter: "boba-fett",
expected: `graph LR
classDef deprecated fill:#E8960F
classDef skipped stroke:#FF0000,stroke-width:4px
classDef deprecatedskipped fill:#E8960F,stroke:#FF0000,stroke-width:4px
%% package "boba-fett"
subgraph "boba-fett"
%% channel "mando"
subgraph boba-fett-mando["mando"]
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
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"]
boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
end
end
`,
Expand Down
7 changes: 6 additions & 1 deletion cmd/opm/alpha/render-graph/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func NewCmd() *cobra.Command {
render action.Render
minEdge string
specifiedPackageName string
drawV0Semantics bool
)
cmd := &cobra.Command{
Use: "render-graph [index-image | fbc-dir]",
Expand Down Expand Up @@ -67,13 +68,17 @@ $ opm alpha render-graph quay.io/operatorhubio/catalog:latest | \
log.Fatal(err)
}

writer := declcfg.NewMermaidWriter(declcfg.WithMinEdgeName(minEdge), declcfg.WithSpecifiedPackageName(specifiedPackageName))
writer := declcfg.NewMermaidWriter(
declcfg.WithMinEdgeName(minEdge),
declcfg.WithSpecifiedPackageName(specifiedPackageName),
declcfg.WithV0Semantics(drawV0Semantics))
if err := writer.WriteChannels(*cfg, os.Stdout); err != nil {
log.Fatal(err)
}
},
}
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")
cmd.Flags().StringVarP(&specifiedPackageName, "package-name", "p", "", "a specific package name to filter output; default is to include all packages in reference")
cmd.Flags().BoolVar(&drawV0Semantics, "draw-v0-semantics", false, "whether to indicate OLMv0 semantics in the output; default is to simply represent the upgrade graph")
return cmd
}
Loading