diff --git a/alpha/declcfg/declcfg.go b/alpha/declcfg/declcfg.go index 3474814be..7797baa49 100644 --- a/alpha/declcfg/declcfg.go +++ b/alpha/declcfg/declcfg.go @@ -70,8 +70,8 @@ type ChannelEntry struct { // evaluation in bundlesEqual(). type Bundle struct { Schema string `json:"schema"` - Name string `json:"name"` - Package string `json:"package"` + Name string `json:"name,omitempty"` + Package string `json:"package,omitempty"` Image string `json:"image"` Properties []property.Property `json:"properties,omitempty" hash:"set"` RelatedImages []RelatedImage `json:"relatedImages,omitempty" hash:"set"` diff --git a/alpha/declcfg/load.go b/alpha/declcfg/load.go index 8717299a6..f811b3145 100644 --- a/alpha/declcfg/load.go +++ b/alpha/declcfg/load.go @@ -259,6 +259,17 @@ func LoadFile(root fs.FS, path string) (*DeclarativeConfig, error) { return cfg, nil } +// LoadSlice will compose declarative config components from a slice of Meta objects +func LoadSlice(metas []*Meta) (*DeclarativeConfig, error) { + builder := fbcBuilder{} + for _, meta := range metas { + if err := builder.addMeta(meta); err != nil { + return nil, err + } + } + return &builder.cfg, nil +} + type fbcBuilder struct { cfg DeclarativeConfig diff --git a/alpha/template/basic/basic.go b/alpha/template/basic/basic.go index 18566fbcf..73a6327e9 100644 --- a/alpha/template/basic/basic.go +++ b/alpha/template/basic/basic.go @@ -2,25 +2,58 @@ package basic import ( "context" + "encoding/json" "fmt" "io" "github.com/operator-framework/operator-registry/alpha/action" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/pkg/image" + "k8s.io/apimachinery/pkg/util/yaml" ) +const schema string = "olm.template.basic" + type Template struct { Registry image.Registry } +type BasicTemplate struct { + Schema string `json:"schema"` + Entries []*declcfg.Meta `json:"entries"` +} + +func parseSpec(reader io.Reader) (*BasicTemplate, error) { + bt := &BasicTemplate{} + btDoc := json.RawMessage{} + btDecoder := yaml.NewYAMLOrJSONDecoder(reader, 4096) + err := btDecoder.Decode(&btDoc) + if err != nil { + return nil, fmt.Errorf("decoding template schema: %v", err) + } + err = json.Unmarshal(btDoc, bt) + if err != nil { + return nil, fmt.Errorf("unmarshalling template: %v", err) + } + + if bt.Schema != schema { + return nil, fmt.Errorf("template has unknown schema (%q), should be %q", bt.Schema, schema) + } + + return bt, nil +} + func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) { - cfg, err := declcfg.LoadReader(reader) + bt, err := parseSpec(reader) + if err != nil { + return nil, err + } + cfg, err := declcfg.LoadSlice(bt.Entries) if err != nil { return cfg, err } - outb := cfg.Bundles[:0] // allocate based on max size of input, but empty slice + outb := cfg.Bundles[:0] // populate registry, incl any flags from CLI, and enforce only rendering bundle images r := action.Render{ Registry: t.Registry, @@ -48,3 +81,38 @@ func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.Declar func isBundleTemplate(b *declcfg.Bundle) bool { return b.Schema != "" && b.Image != "" && b.Package == "" && len(b.Properties) == 0 && len(b.RelatedImages) == 0 } + +// FromReader reads FBC from a reader and generates a BasicTemplate from it +func FromReader(r io.Reader) (*BasicTemplate, error) { + var entries []*declcfg.Meta + if err := declcfg.WalkMetasReader(r, func(meta *declcfg.Meta, err error) error { + if err != nil { + return err + } + if meta.Schema == declcfg.SchemaBundle { + var b declcfg.Bundle + if err := json.Unmarshal(meta.Blob, &b); err != nil { + return fmt.Errorf("parse bundle: %v", err) + } + b2 := declcfg.Bundle{ + Schema: b.Schema, + Image: b.Image, + } + meta.Blob, err = json.Marshal(b2) + if err != nil { + return fmt.Errorf("re-serialize bundle: %v", err) + } + } + entries = append(entries, meta) + return nil + }); err != nil { + return nil, err + } + + bt := &BasicTemplate{ + Schema: schema, + Entries: entries, + } + + return bt, nil +} diff --git a/alpha/template/converter/converter.go b/alpha/template/converter/converter.go new file mode 100644 index 000000000..33e28ddcd --- /dev/null +++ b/alpha/template/converter/converter.go @@ -0,0 +1,39 @@ +package converter + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/operator-framework/operator-registry/alpha/template/basic" + "github.com/operator-framework/operator-registry/pkg/image" + "sigs.k8s.io/yaml" +) + +type Converter struct { + FbcReader io.Reader + OutputFormat string + Registry image.Registry +} + +func (c *Converter) Convert() error { + bt, err := basic.FromReader(c.FbcReader) + if err != nil { + return err + } + + b, _ := json.MarshalIndent(bt, "", " ") + if c.OutputFormat == "json" { + fmt.Fprintln(os.Stdout, string(b)) + } else { + y, err := yaml.JSONToYAML(b) + if err != nil { + return err + } + y = append([]byte("---\n"), y...) + fmt.Fprintln(os.Stdout, string(y)) + } + + return nil +} diff --git a/cmd/opm/alpha/cmd.go b/cmd/opm/alpha/cmd.go index 202d9597f..b0077f9cc 100644 --- a/cmd/opm/alpha/cmd.go +++ b/cmd/opm/alpha/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle" + converttemplate "github.com/operator-framework/operator-registry/cmd/opm/alpha/convert-template" "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/template" @@ -26,6 +27,7 @@ func NewCmd(showAlphaHelp bool) *cobra.Command { list.NewCmd(), rendergraph.NewCmd(), template.NewCmd(), + converttemplate.NewCmd(), ) return runCmd } diff --git a/cmd/opm/alpha/convert-template/convert.go b/cmd/opm/alpha/convert-template/convert.go new file mode 100644 index 000000000..23567c766 --- /dev/null +++ b/cmd/opm/alpha/convert-template/convert.go @@ -0,0 +1,66 @@ +package converttemplate + +import ( + "fmt" + "log" + + "github.com/spf13/cobra" + + "github.com/operator-framework/operator-registry/alpha/template/converter" + "github.com/operator-framework/operator-registry/cmd/opm/internal/util" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "convert-template", + Short: "Convert existing FBC to a supported template type", + } + cmd.AddCommand( + newBasicConvertCmd(), + ) + return cmd +} + +func newBasicConvertCmd() *cobra.Command { + var ( + converter converter.Converter + output string + ) + cmd := &cobra.Command{ + Use: "basic [ | -]", + Args: cobra.MaximumNArgs(1), + Short: "Generate a basic template from existing FBC", + Long: `Generate a basic template from existing FBC. + +This command outputs a basic catalog template to STDOUT from input FBC. +If no argument is specified or is '-' input is assumed from STDIN. +`, + RunE: func(c *cobra.Command, args []string) error { + + switch output { + case "yaml": + fallthrough + case "json": + converter.OutputFormat = output + default: + log.Fatalf("invalid --output value %q, expected (json|yaml)", output) + } + + reader, name, err := util.OpenFileOrStdin(c, args) + if err != nil { + return fmt.Errorf("unable to open input: %q", name) + } + + converter.FbcReader = reader + err = converter.Convert() + if err != nil { + return fmt.Errorf("converting: %v", err) + } + + return nil + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml)") + + return cmd +} diff --git a/cmd/opm/alpha/template/basic.go b/cmd/opm/alpha/template/basic.go index 5d34ec2cd..91f4c75d7 100644 --- a/cmd/opm/alpha/template/basic.go +++ b/cmd/opm/alpha/template/basic.go @@ -16,7 +16,6 @@ import ( func newBasicTemplateCmd() *cobra.Command { var ( template basic.Template - output string ) cmd := &cobra.Command{ Use: "basic basic-template-file", @@ -30,13 +29,17 @@ When FILE is '-' or not provided, the template is read from standard input`, // When no arguments or "-" is passed to the command, // assume input is coming from stdin // Otherwise open the file passed to the command - data, source, err := openFileOrStdin(cmd, args) + data, source, err := util.OpenFileOrStdin(cmd, args) if err != nil { log.Fatalf("unable to open %q: %v", source, err) } defer data.Close() var write func(declcfg.DeclarativeConfig, io.Writer) error + output, err := cmd.Flags().GetString("output") + if err != nil { + log.Fatalf("unable to determine output format") + } switch output { case "yaml": write = declcfg.WriteYAML @@ -70,6 +73,5 @@ When FILE is '-' or not provided, the template is read from standard input`, } }, } - cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml)") return cmd } diff --git a/cmd/opm/alpha/template/cmd.go b/cmd/opm/alpha/template/cmd.go index 8a2acefc5..55ac55187 100644 --- a/cmd/opm/alpha/template/cmd.go +++ b/cmd/opm/alpha/template/cmd.go @@ -1,29 +1,27 @@ package template import ( - "io" - "os" - "github.com/spf13/cobra" ) func NewCmd() *cobra.Command { + var output string + runCmd := &cobra.Command{ Use: "render-template", Short: "Render a catalog template type", Args: cobra.NoArgs, } - runCmd.AddCommand(newBasicTemplateCmd()) - runCmd.AddCommand(newSemverTemplateCmd()) + bc := newBasicTemplateCmd() + // bc.Hidden = true + runCmd.AddCommand(bc) - return runCmd -} + sc := newSemverTemplateCmd() + // sc.Hidden = true + runCmd.AddCommand(sc) -func openFileOrStdin(cmd *cobra.Command, args []string) (io.ReadCloser, string, error) { - if len(args) == 0 || args[0] == "-" { - return io.NopCloser(cmd.InOrStdin()), "stdin", nil - } - reader, err := os.Open(args[0]) - return reader, args[0], err + runCmd.PersistentFlags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml)") + + return runCmd } diff --git a/cmd/opm/alpha/template/semver.go b/cmd/opm/alpha/template/semver.go index f67f9596c..b498f6b78 100644 --- a/cmd/opm/alpha/template/semver.go +++ b/cmd/opm/alpha/template/semver.go @@ -15,7 +15,6 @@ import ( ) func newSemverTemplateCmd() *cobra.Command { - output := "" cmd := &cobra.Command{ Use: "semver [FILE]", Short: `Generate a file-based catalog from a single 'semver template' file @@ -28,13 +27,17 @@ When FILE is '-' or not provided, the template is read from standard input`, // When no arguments or "-" is passed to the command, // assume input is coming from stdin // Otherwise open the file passed to the command - data, source, err := openFileOrStdin(cmd, args) + data, source, err := util.OpenFileOrStdin(cmd, args) if err != nil { return err } defer data.Close() var write func(declcfg.DeclarativeConfig, io.Writer) error + output, err := cmd.Flags().GetString("output") + if err != nil { + log.Fatalf("unable to determine output format") + } switch output { case "json": write = declcfg.WriteJSON @@ -79,6 +82,5 @@ When FILE is '-' or not provided, the template is read from standard input`, }, } - cmd.Flags().StringVarP(&output, "output", "o", "json", "Output format (json|yaml|mermaid)") return cmd } diff --git a/cmd/opm/internal/util/util.go b/cmd/opm/internal/util/util.go index a265ba743..9e0e006be 100644 --- a/cmd/opm/internal/util/util.go +++ b/cmd/opm/internal/util/util.go @@ -2,6 +2,7 @@ package util import ( "errors" + "io" "os" "github.com/spf13/cobra" @@ -66,3 +67,11 @@ func CreateCLIRegistry(cmd *cobra.Command) (*containerdregistry.Registry, error) } return reg, nil } + +func OpenFileOrStdin(cmd *cobra.Command, args []string) (io.ReadCloser, string, error) { + if len(args) == 0 || args[0] == "-" { + return io.NopCloser(cmd.InOrStdin()), "stdin", nil + } + reader, err := os.Open(args[0]) + return reader, args[0], err +}