Skip to content
Open
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
84 changes: 60 additions & 24 deletions alpha/template/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,33 @@ import (
"k8s.io/apimachinery/pkg/util/yaml"

"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/template"
)

const schema string = "olm.template.basic"

type Template struct {
RenderBundle func(context.Context, string) (*declcfg.DeclarativeConfig, error)
func init() {
template.GetTemplateRegistry().Register(&Factory{})
}

type BasicTemplate struct {
Copy link

Choose a reason for hiding this comment

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

does this need to be public given that we have Template interface?

Schema string `json:"schema"`
Entries []*declcfg.Meta `json:"entries"`
renderBundle template.BundleRenderer
}

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)
// NewTemplate creates a new basic template instance
func NewTemplate(renderBundle template.BundleRenderer) template.Template {
Copy link

Choose a reason for hiding this comment

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

could we shorter the constructor name to New given that it is a part of template/basic package?

return &BasicTemplate{
renderBundle: renderBundle,
}
}

return bt, nil
// RenderBundle implements the template.Template interface
Copy link

Choose a reason for hiding this comment

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

these docs lines are not very much helpful, can you expand for clarity?

func (t *BasicTemplate) RenderBundle(ctx context.Context, image string) (*declcfg.DeclarativeConfig, error) {
return t.renderBundle(ctx, image)
}

func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
// Render implements the template.Template interface
func (t *BasicTemplate) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
bt, err := parseSpec(reader)
if err != nil {
return nil, err
Expand All @@ -68,14 +61,57 @@ func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.Declar
return cfg, nil
}

// Schema implements the template.Template interface
func (t *BasicTemplate) Schema() string {
return schema
}

// Factory implements the template.TemplateFactory interface
type Factory struct{}

// CreateTemplate implements the template.TemplateFactory interface
func (f *Factory) CreateTemplate(renderBundle template.BundleRenderer) template.Template {
return NewTemplate(renderBundle)
}

// Schema implements the template.TemplateFactory interface
func (f *Factory) Schema() string {
return schema
}

type BasicTemplateData struct {
Schema string `json:"schema"`
Entries []*declcfg.Meta `json:"entries"`
}

func parseSpec(reader io.Reader) (*BasicTemplateData, error) {
bt := &BasicTemplateData{}
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
}

// isBundleTemplate identifies a Bundle template source as having a Schema and Image defined
// but no Properties, RelatedImages or Package defined
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) {
// FromReader reads FBC from a reader and generates a BasicTemplateData from it
func FromReader(r io.Reader) (*BasicTemplateData, error) {
var entries []*declcfg.Meta
if err := declcfg.WalkMetasReader(r, func(meta *declcfg.Meta, err error) error {
if err != nil {
Expand All @@ -101,7 +137,7 @@ func FromReader(r io.Reader) (*BasicTemplate, error) {
return nil, err
}

bt := &BasicTemplate{
bt := &BasicTemplateData{
Schema: schema,
Entries: entries,
}
Expand Down
26 changes: 26 additions & 0 deletions alpha/template/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package template

import (
"fmt"
"strings"
"text/tabwriter"
)

var tr = NewTemplateRegistry()

// GetTemplateRegistry returns the global template registry
func GetTemplateRegistry() *TemplateRegistry {
return tr
}

func (r *TemplateRegistry) HelpText() string {
var help strings.Builder
supportedTypes := r.GetSupportedTypes()
help.WriteString("\n")
tabber := tabwriter.NewWriter(&help, 0, 0, 1, ' ', 0)
for _, item := range supportedTypes {
fmt.Fprintf(tabber, " - %s\n", item)
}
tabber.Flush()
return help.String()
}
47 changes: 47 additions & 0 deletions alpha/template/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package template

import (
"bytes"
"encoding/json"
"fmt"
"io"

"k8s.io/apimachinery/pkg/util/yaml"
)

// detectSchema reads the input, extracts the schema field, and returns a reader
// that includes the consumed data followed by the remaining stream data.
// This works when the input is stdin or a file (since stdin cannot be closed and reopened)
// and complies with the requirement that each supplied schema has a defined "schema" field,
// without attempting to load all input into memory.
func detectSchema(reader io.Reader) (string, io.Reader, error) {
// Capture what's read during schema detection
var capturedData bytes.Buffer
teeReader := io.TeeReader(reader, &capturedData)

// Read the input into a raw message
rawDoc := json.RawMessage{}
decoder := yaml.NewYAMLOrJSONDecoder(teeReader, 4096)
err := decoder.Decode(&rawDoc)
if err != nil {
return "", nil, fmt.Errorf("decoding template input: %v", err)
}

// Parse the raw message to extract schema
var schemaDoc struct {
Schema string `json:"schema"`
}
err = json.Unmarshal(rawDoc, &schemaDoc)
if err != nil {
return "", nil, fmt.Errorf("unmarshalling template schema: %v", err)
}

if schemaDoc.Schema == "" {
return "", nil, fmt.Errorf("template input missing required 'schema' field")
}

// Create a reader that combines the captured data with the remaining stream
replayReader := io.MultiReader(&capturedData, reader)

return schemaDoc.Schema, replayReader, nil
}
137 changes: 128 additions & 9 deletions alpha/template/semver/semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,55 @@ import (

"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
"github.com/operator-framework/operator-registry/alpha/template"
)

func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error) {
// IO structs -- BEGIN
type semverTemplateBundleEntry struct {
Image string `json:"image,omitempty"`
}

type semverTemplateChannelBundles struct {
Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"`
}

type SemverTemplateData struct {
Schema string `json:"schema"`
GenerateMajorChannels bool `json:"generateMajorChannels,omitempty"`
GenerateMinorChannels bool `json:"generateMinorChannels,omitempty"`
DefaultChannelTypePreference streamType `json:"defaultChannelTypePreference,omitempty"`
Candidate semverTemplateChannelBundles `json:"candidate,omitempty"`
Fast semverTemplateChannelBundles `json:"fast,omitempty"`
Stable semverTemplateChannelBundles `json:"stable,omitempty"`

pkg string `json:"-"` // the derived package name
defaultChannel string `json:"-"` // detected "most stable" channel head
}

// IO structs -- END

// SemverTemplate implements the common template interface
type SemverTemplate struct {
renderBundle template.BundleRenderer
}

// NewTemplate creates a new semver template instance
func NewTemplate(renderBundle template.BundleRenderer) template.Template {
return &SemverTemplate{
renderBundle: renderBundle,
}
}

// RenderBundle implements the template.Template interface
func (t *SemverTemplate) RenderBundle(ctx context.Context, image string) (*declcfg.DeclarativeConfig, error) {
return t.renderBundle(ctx, image)
}

// Render implements the template.Template interface
func (t *SemverTemplate) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
var out declcfg.DeclarativeConfig

sv, err := readFile(t.Data)
sv, err := readFile(reader)
if err != nil {
return nil, fmt.Errorf("render: unable to read file: %v", err)
}
Expand Down Expand Up @@ -58,7 +101,83 @@ func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error
return &out, nil
}

func buildBundleList(t semverTemplate) map[string]string {
// Schema implements the template.Template interface
func (t *SemverTemplate) Schema() string {
return schema
}

// Factory implements the template.TemplateFactory interface
type Factory struct{}

// CreateTemplate implements the template.TemplateFactory interface
func (f *Factory) CreateTemplate(renderBundle template.BundleRenderer) template.Template {
return NewTemplate(renderBundle)
}

// Schema implements the template.TemplateFactory interface
func (f *Factory) Schema() string {
return schema
}

const schema string = "olm.semver"

func init() {
template.GetTemplateRegistry().Register(&Factory{})
}

// channel "archetypes", restricted in this iteration to just these
type channelArchetype string

const (
candidateChannelArchetype channelArchetype = "candidate"
fastChannelArchetype channelArchetype = "fast"
stableChannelArchetype channelArchetype = "stable"
)

// mapping channel name --> stability, where higher values indicate greater stability
var channelPriorities = map[channelArchetype]int{candidateChannelArchetype: 0, fastChannelArchetype: 1, stableChannelArchetype: 2}

// sorting capability for a slice according to the assigned channelPriorities
type byChannelPriority []channelArchetype

func (b byChannelPriority) Len() int { return len(b) }
func (b byChannelPriority) Less(i, j int) bool {
return channelPriorities[b[i]] < channelPriorities[b[j]]
}
func (b byChannelPriority) Swap(i, j int) { b[i], b[j] = b[j], b[i] }

type streamType string

const defaultStreamType streamType = ""
const minorStreamType streamType = "minor"
const majorStreamType streamType = "major"

// general preference for minor channels
var streamTypePriorities = map[streamType]int{minorStreamType: 2, majorStreamType: 1, defaultStreamType: 0}

// map of archetypes --> bundles --> bundle-version from the input file
type bundleVersions map[channelArchetype]map[string]semver.Version // e.g. srcv["stable"]["example-operator.v1.0.0"] = 1.0.0

// the "high-water channel" struct functions as a freely-rising indicator of the "most stable" channel head, so we can use that
// later as the package's defaultChannel attribute
type highwaterChannel struct {
archetype channelArchetype
kind streamType
version semver.Version
name string
}

// entryTuple represents a channel entry with its associated metadata
type entryTuple struct {
arch channelArchetype
kind streamType
parent string
name string
version semver.Version
index int
}

func buildBundleList(t SemverTemplateData) map[string]string {
dict := make(map[string]string)
for _, bl := range []semverTemplateChannelBundles{t.Candidate, t.Fast, t.Stable} {
for _, b := range bl.Bundles {
Expand All @@ -70,13 +189,13 @@ func buildBundleList(t semverTemplate) map[string]string {
return dict
}

func readFile(reader io.Reader) (*semverTemplate, error) {
func readFile(reader io.Reader) (*SemverTemplateData, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}

sv := semverTemplate{}
sv := SemverTemplateData{}
if err := yaml.UnmarshalStrict(data, &sv); err != nil {
return nil, err
}
Expand Down Expand Up @@ -115,7 +234,7 @@ func readFile(reader io.Reader) (*semverTemplate, error) {
return &sv, nil
}

func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.DeclarativeConfig, bundleDict map[string]string) (*bundleVersions, error) {
func (sv *SemverTemplateData) getVersionsFromStandardChannels(cfg *declcfg.DeclarativeConfig, bundleDict map[string]string) (*bundleVersions, error) {
versions := bundleVersions{}

bdm, err := sv.getVersionsFromChannel(sv.Candidate.Bundles, bundleDict, cfg)
Expand Down Expand Up @@ -148,7 +267,7 @@ func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.Declarati
return &versions, nil
}

func (sv *semverTemplate) getVersionsFromChannel(semverBundles []semverTemplateBundleEntry, bundleDict map[string]string, cfg *declcfg.DeclarativeConfig) (map[string]semver.Version, error) {
func (sv *SemverTemplateData) getVersionsFromChannel(semverBundles []semverTemplateBundleEntry, bundleDict map[string]string, cfg *declcfg.DeclarativeConfig) (map[string]semver.Version, error) {
entries := make(map[string]semver.Version)

// we iterate over the channel bundles from the template, to:
Expand Down Expand Up @@ -210,7 +329,7 @@ func (sv *semverTemplate) getVersionsFromChannel(semverBundles []semverTemplateB
// - within the same minor version (Y-stream), the head of the channel should have a 'skips' encompassing all lesser Y.Z versions of the bundle enumerated in the template.
// along the way, uses a highwaterChannel marker to identify the "most stable" channel head to be used as the default channel for the generated package

func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
func (sv *SemverTemplateData) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
outChannels := []declcfg.Channel{}

// sort the channel archetypes in ascending order so we can traverse the bundles in order of
Expand Down Expand Up @@ -287,7 +406,7 @@ func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []dec
return outChannels
}

func (sv *semverTemplate) linkChannels(unlinkedChannels map[string]*declcfg.Channel, entries []entryTuple) []declcfg.Channel {
func (sv *SemverTemplateData) linkChannels(unlinkedChannels map[string]*declcfg.Channel, entries []entryTuple) []declcfg.Channel {
channels := []declcfg.Channel{}

// sort to force partitioning by archetype --> kind --> semver
Expand Down
Loading
Loading