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
172 changes: 137 additions & 35 deletions alpha/declcfg/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,90 @@ import (
"sort"
"strings"

"github.com/blang/semver/v4"
"github.com/operator-framework/operator-registry/alpha/property"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/yaml"
)

// 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
// if provided, minEdgeName will be used as the lower bound for edges in the output graph
//
// NB: Output has wrapper comments stating the skipRange edge caveat in HTML comment format, which cannot be parsed by mermaid renderers.
// This is deliberate, and intended as an explicit acknowledgement of the limitations, instead of requiring the user to notice the missing edges upon inspection.
//
// This is deliberate, and intended as an explicit acknowledgement of the limitations, instead of requiring the user to notice the missing edges upon inspection.
//
// Example output:
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
// graph LR
// %% package "neuvector-certified-operator-rhmp"
// subgraph "neuvector-certified-operator-rhmp"
// %% channel "beta"
// subgraph neuvector-certified-operator-rhmp-beta["beta"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]
// 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"]
// 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"]
// end
// end
//
// %% package "neuvector-certified-operator-rhmp"
// subgraph "neuvector-certified-operator-rhmp"
// %% channel "beta"
// subgraph neuvector-certified-operator-rhmp-beta["beta"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]
// 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"]
// 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"]
// end
// end
//
// end
// <!-- PLEASE NOTE: skipRange edges are not currently displayed -->
func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer) error {
func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer, minEdgeName string) error {
pkgs := map[string]*strings.Builder{}

sort.Slice(cfg.Channels, func(i, j int) bool {
return cfg.Channels[i].Name < cfg.Channels[j].Name
})

for _, c := range cfg.Channels {
pkgBuilder, ok := pkgs[c.Package]
if !ok {
pkgBuilder = &strings.Builder{}
pkgs[c.Package] = pkgBuilder
versionMap, err := getBundleVersions(&cfg)
if err != nil {
return err
}

if _, ok := versionMap[minEdgeName]; !ok {
if minEdgeName != "" {
return fmt.Errorf("unknown minimum edge name: %q", minEdgeName)
}
channelID := fmt.Sprintf("%s-%s", c.Package, c.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", c.Name))
pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, c.Name))

for _, ce := range c.Entries {
entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))

// no support for SkipRange yet
if len(ce.Replaces) > 0 {
replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
}

for _, c := range cfg.Channels {
filteredChannel := filterChannel(&c, versionMap, minEdgeName)
if filteredChannel != nil {
pkgBuilder, ok := pkgs[c.Package]
if !ok {
pkgBuilder = &strings.Builder{}
pkgs[c.Package] = pkgBuilder
}
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", entryId, ce.Name, "skips", skipsId, s))

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))

for _, ce := range filteredChannel.Entries {
if versionMap[ce.Name].GE(versionMap[minEdgeName]) {
entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))

// no support for SkipRange yet
if len(ce.Replaces) > 0 {
replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
}
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", entryId, ce.Name, "skips", skipsId, s))
}
}
}
}
pkgBuilder.WriteString(" end\n")
}
pkgBuilder.WriteString(" end\n")
}

out.Write([]byte("<!-- PLEASE NOTE: skipRange edges are not currently displayed -->\n"))
Expand All @@ -91,6 +114,85 @@ func WriteMermaidChannels(cfg DeclarativeConfig, out io.Writer) error {
return nil
}

// filters the channel edges to include only those which are greater-than-or-equal to the edge named by startVersion
// returns a nil channel if all edges are filtered out
func filterChannel(c *Channel, versionMap map[string]semver.Version, minEdgeName string) *Channel {
// short-circuit if no specified startVersion
if minEdgeName == "" {
return c
}
// convert the edge name to the version so we don't have to duplicate the lookup
minVersion := versionMap[minEdgeName]

out := &Channel{Name: c.Name, Package: c.Package, Properties: c.Properties, Entries: []ChannelEntry{}}
for _, ce := range c.Entries {
filteredCe := ChannelEntry{Name: ce.Name}
// short-circuit to take the edge name (but no references to earlier versions)
if ce.Name == minEdgeName {
out.Entries = append(out.Entries, filteredCe)
continue
}
// if len(ce.SkipRange) > 0 {
// }
if len(ce.Replaces) > 0 {
if versionMap[ce.Replaces].GTE(minVersion) {
filteredCe.Replaces = ce.Replaces
}
}
if len(ce.Skips) > 0 {
filteredSkips := []string{}
for _, s := range ce.Skips {
if versionMap[s].GTE(minVersion) {
filteredSkips = append(filteredSkips, s)
}
}
if len(filteredSkips) > 0 {
filteredCe.Skips = filteredSkips
}
}
if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 {
out.Entries = append(out.Entries, filteredCe)
}
}

if len(out.Entries) > 0 {
return out
} else {
return nil
}
}

func parseVersionProperty(b *Bundle) (*semver.Version, error) {
props, err := property.Parse(b.Properties)
if err != nil {
return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
}
if len(props.Packages) != 1 {
return nil, fmt.Errorf("bundle %q has multiple %q properties, expected exactly 1", b.Name, property.TypePackage)
}
v, err := semver.Parse(props.Packages[0].Version)
if err != nil {
return nil, fmt.Errorf("bundle %q has invalid version %q: %v", b.Name, props.Packages[0].Version, err)
}

return &v, nil
}

func getBundleVersions(cfg *DeclarativeConfig) (map[string]semver.Version, error) {
entries := make(map[string]semver.Version)
for index := range cfg.Bundles {
if _, ok := entries[cfg.Bundles[index].Name]; !ok {
ver, err := parseVersionProperty(&cfg.Bundles[index])
if err != nil {
return entries, err
}
entries[cfg.Bundles[index].Name] = *ver
}
}

return entries, nil
}

func WriteJSON(cfg DeclarativeConfig, w io.Writer) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
Expand Down
3 changes: 2 additions & 1 deletion alpha/declcfg/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,11 @@ graph LR
`,
},
}
startVersion := ""
for _, s := range specs {
t.Run(s.name, func(t *testing.T) {
var buf bytes.Buffer
err := WriteMermaidChannels(s.cfg, &buf)
err := WriteMermaidChannels(s.cfg, &buf, startVersion)
require.NoError(t, err)
require.Equal(t, s.expected, buf.String())
})
Expand Down
2 changes: 2 additions & 0 deletions cmd/opm/alpha/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle"
"github.com/operator-framework/operator-registry/cmd/opm/alpha/list"
rendergraph "github.com/operator-framework/operator-registry/cmd/opm/alpha/render-graph"
"github.com/operator-framework/operator-registry/cmd/opm/alpha/veneer"
)

Expand All @@ -19,6 +20,7 @@ func NewCmd() *cobra.Command {
runCmd.AddCommand(
bundle.NewCmd(),
list.NewCmd(),
rendergraph.NewCmd(),
veneer.NewCmd(),
)
return runCmd
Expand Down
52 changes: 52 additions & 0 deletions cmd/opm/alpha/render-graph/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package rendergraph

import (
"io"
"log"
"os"

"github.com/operator-framework/operator-registry/alpha/action"
"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/cmd/opm/internal/util"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
var (
render action.Render
minEdge string
)
cmd := &cobra.Command{
Use: "render-graph [index-image | fbc-dir | bundle-image]",
Short: "Generate mermaid-formatted view of upgrade graph of operators in an index",
Long: `Generate mermaid-formatted view of upgrade graphs of operators in an index`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// The bundle loading impl is somewhat verbose, even on the happy path,
// so discard all logrus default logger logs. Any important failures will be
// returned from render.Run and logged as fatal errors.
logrus.SetOutput(io.Discard)

registry, err := util.CreateCLIRegistry(cmd)
if err != nil {
log.Fatal(err)
}

render.Refs = args
render.AllowedRefMask = action.RefBundleImage | action.RefDCImage | action.RefDCDir // all non-sqlite
render.Registry = registry

cfg, err := render.Run(cmd.Context())
if err != nil {
log.Fatal(err)
}

if err := declcfg.WriteMermaidChannels(*cfg, os.Stdout, minEdge); 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")
return cmd
}
5 changes: 4 additions & 1 deletion cmd/opm/alpha/veneer/semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ func newSemverCmd() *cobra.Command {
case "yaml":
write = declcfg.WriteYAML
case "mermaid":
write = declcfg.WriteMermaidChannels
write = func(cfg declcfg.DeclarativeConfig, writer io.Writer) error {
startVersion := ""
return declcfg.WriteMermaidChannels(cfg, writer, startVersion)
}
default:
return fmt.Errorf("invalid output format %q", output)
}
Expand Down
6 changes: 2 additions & 4 deletions cmd/opm/render/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ func NewCmd() *cobra.Command {
write = declcfg.WriteYAML
case "json":
write = declcfg.WriteJSON
case "mermaid":
write = declcfg.WriteMermaidChannels
default:
log.Fatalf("invalid --output value %q, expected (json|yaml|mermaid)", output)
log.Fatalf("invalid --output value %q, expected (json|yaml)", output)
}

// The bundle loading impl is somewhat verbose, even on the happy path,
Expand All @@ -65,7 +63,7 @@ func NewCmd() *cobra.Command {
}
},
}
cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml|mermaid)")
cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml)")
return cmd
}

Expand Down