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
36 changes: 36 additions & 0 deletions alpha/template/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package api

import (
"context"
"io"

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

// BundleRenderer defines the function signature for rendering a string containing a bundle image/path/file into a DeclarativeConfig fragment
// It's provided as a discrete type to allow for easy mocking in tests as well as facilitating variable
// restrictions on reference types
type BundleRenderer func(context.Context, string) (*declcfg.DeclarativeConfig, error)

// Template defines the common interface for all template types
type Template interface {
// RenderBundle renders a bundle image reference into a DeclarativeConfig fragment.
// This function is used to render a single bundle image reference by a template instance,
// and is provided to the template on construction.
// This is typically used in the call to Render the template to DeclarativeConfig, and
// needs to be configurable to handle different bundle image formats and configurations.
RenderBundle(ctx context.Context, imageRef string) (*declcfg.DeclarativeConfig, error)
// Render processes the raw template yaml/json input and returns an expanded DeclarativeConfig
// in the case where expansion fails, it returns an error
Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error)
// Schema returns the schema identifier for this template type
Schema() string
}

// Factory creates template instances based on schema
type Factory interface {
// CreateTemplate creates a new template instance with the given RenderBundle function
CreateTemplate(renderBundle BundleRenderer) Template
// Schema returns the schema identifier this factory handles
Schema() string
}
87 changes: 60 additions & 27 deletions alpha/template/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,30 @@ import (
"k8s.io/apimachinery/pkg/util/yaml"

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

const schema string = "olm.template.basic"

type Template struct {
RenderBundle func(context.Context, string) (*declcfg.DeclarativeConfig, error)
type basicTemplate struct {
renderBundle api.BundleRenderer
}

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)
// new creates a new basic template instance
func new(renderBundle api.BundleRenderer) api.Template {
return &basicTemplate{
renderBundle: renderBundle,
}
}

return bt, nil
// RenderBundle expands the bundle image reference into a DeclarativeConfig fragment.
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 extracts the spec from the reader and converts it to a standalone DeclarativeConfig,
// expanding any bundle image references into full olm.bundle DeclarativeConfig
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 +58,57 @@ func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.Declar
return cfg, nil
}

// Schema returns the schema identifier for this template type
func (t *basicTemplate) Schema() string {
return schema
}

// Factory represents the basic template factory
type Factory struct{}

// CreateTemplate creates a new template instance with the given RenderBundle function
func (f *Factory) CreateTemplate(renderBundle api.BundleRenderer) api.Template {
return new(renderBundle)
}

// Schema returns the schema supported by this factory
func (f *Factory) Schema() string {
return schema
}

type BasicTemplateData struct {
Copy link

Choose a reason for hiding this comment

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

does it need to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's used by the template converter to be able to make a basic template from a full FBC representation, so it does need to be public.
I'm sure that I could pivot this away, but the purpose of this PR was to change the functional flow of the CLI commands to be able to simplify them (not needing to always specify the template type) and I could follow up with such things in later efforts.

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 +134,7 @@ func FromReader(r io.Reader) (*BasicTemplate, error) {
return nil, err
}

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

import (
"fmt"
"io"
"slices"
"strings"
"sync"
"text/tabwriter"

"github.com/operator-framework/operator-registry/alpha/template/api"
"github.com/operator-framework/operator-registry/alpha/template/basic"
"github.com/operator-framework/operator-registry/alpha/template/semver"
)

// Re-export api types for backward compatibility
type (
BundleRenderer = api.BundleRenderer
Template = api.Template
Factory = api.Factory
)

type Registry interface {
Register(factory Factory)
GetSupportedTypes() []string
HasType(templateType string) bool
HasSchema(schema string) bool
CreateTemplateBySchema(reader io.Reader, renderBundle BundleRenderer) (Template, io.Reader, error)
CreateTemplateByType(templateType string, renderBundle BundleRenderer) (Template, error)
GetSupportedSchemas() []string
HelpText() string
}

// registry maintains a mapping of schema identifiers to template factories
type registry struct {
mu sync.RWMutex
factories map[string]Factory
}

// NewRegistry creates a new registry and registers all built-in template factories.
func NewRegistry() Registry {
r := &registry{
factories: make(map[string]Factory),
}
r.Register(&basic.Factory{})
r.Register(&semver.Factory{})
return r
}

func (r *registry) 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()
}

// Register adds a template factory to the registry
func (r *registry) Register(factory Factory) {
r.mu.Lock()
defer r.mu.Unlock()
r.factories[factory.Schema()] = factory
}

// CreateTemplateBySchema creates a template instance based on the schema found in the input
// and returns a reader that can be used to render the template. The returned reader includes
// both the data consumed during schema detection and the remaining unconsumed data.
func (r *registry) CreateTemplateBySchema(reader io.Reader, renderBundle BundleRenderer) (Template, io.Reader, error) {
schema, replayReader, err := detectSchema(reader)
if err != nil {
return nil, nil, err
}

r.mu.RLock()
factory, exists := r.factories[schema]
defer r.mu.RUnlock()
if !exists {
return nil, nil, &UnknownSchemaError{Schema: schema}
}

return factory.CreateTemplate(renderBundle), replayReader, nil
}

func (r *registry) CreateTemplateByType(templateType string, renderBundle BundleRenderer) (Template, error) {
r.mu.RLock()
factory, exists := r.factories[templateType]

Choose a reason for hiding this comment

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

FYI, this change introduces a regression in v1.62.0 because the factories are indexed by the full schema but templateType is only the last portion.

Issue opened:

defer r.mu.RUnlock()
if !exists {
return nil, &UnknownSchemaError{Schema: templateType}
}

return factory.CreateTemplate(renderBundle), nil
}

// GetSupportedSchemas returns all supported schema identifiers
func (r *registry) GetSupportedSchemas() []string {
r.mu.RLock()
defer r.mu.RUnlock()
schemas := make([]string, 0, len(r.factories))
for schema := range r.factories {
schemas = append(schemas, schema)
}
slices.Sort(schemas)
return schemas
}

// GetSupportedTypes returns all supported template types
// TODO: in future, might store the type separately from the schema
// right now it's just the last part of the schema string
func (r *registry) GetSupportedTypes() []string {
r.mu.RLock()
defer r.mu.RUnlock()
types := make([]string, 0, len(r.factories))
for schema := range r.factories {
types = append(types, schema[strings.LastIndex(schema, ".")+1:])
}
slices.Sort(types)
return types
}

func (r *registry) HasSchema(schema string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, exists := r.factories[schema]
return exists
}

func (r *registry) HasType(templateType string) bool {
types := r.GetSupportedTypes()
return slices.Contains(types, templateType)
}

// UnknownSchemaError is returned when a schema is not recognized
type UnknownSchemaError struct {
Schema string
}

func (e *UnknownSchemaError) Error() string {
return "unknown template schema: " + e.Schema
}
Loading
Loading