Skip to content

Commit 938b01d

Browse files
committed
detect template type from input, but respect explicit specification
Signed-off-by: grokspawn <jordan@nimblewidget.com>
1 parent e7b9dee commit 938b01d

File tree

11 files changed

+552
-370
lines changed

11 files changed

+552
-370
lines changed

alpha/template/basic/basic.go

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,33 @@ import (
99
"k8s.io/apimachinery/pkg/util/yaml"
1010

1111
"github.com/operator-framework/operator-registry/alpha/declcfg"
12+
"github.com/operator-framework/operator-registry/alpha/template"
1213
)
1314

1415
const schema string = "olm.template.basic"
1516

16-
type Template struct {
17-
RenderBundle func(context.Context, string) (*declcfg.DeclarativeConfig, error)
17+
func init() {
18+
template.GetTemplateRegistry().Register(&Factory{})
1819
}
1920

2021
type BasicTemplate struct {
21-
Schema string `json:"schema"`
22-
Entries []*declcfg.Meta `json:"entries"`
22+
renderBundle template.BundleRenderer
2323
}
2424

25-
func parseSpec(reader io.Reader) (*BasicTemplate, error) {
26-
bt := &BasicTemplate{}
27-
btDoc := json.RawMessage{}
28-
btDecoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
29-
err := btDecoder.Decode(&btDoc)
30-
if err != nil {
31-
return nil, fmt.Errorf("decoding template schema: %v", err)
32-
}
33-
err = json.Unmarshal(btDoc, bt)
34-
if err != nil {
35-
return nil, fmt.Errorf("unmarshalling template: %v", err)
36-
}
37-
38-
if bt.Schema != schema {
39-
return nil, fmt.Errorf("template has unknown schema (%q), should be %q", bt.Schema, schema)
25+
// NewTemplate creates a new basic template instance
26+
func NewTemplate(renderBundle template.BundleRenderer) template.Template {
27+
return &BasicTemplate{
28+
renderBundle: renderBundle,
4029
}
30+
}
4131

42-
return bt, nil
32+
// RenderBundle implements the template.Template interface
33+
func (t *BasicTemplate) RenderBundle(ctx context.Context, image string) (*declcfg.DeclarativeConfig, error) {
34+
return t.renderBundle(ctx, image)
4335
}
4436

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

64+
// Schema implements the template.Template interface
65+
func (t *BasicTemplate) Schema() string {
66+
return schema
67+
}
68+
69+
// Factory implements the template.TemplateFactory interface
70+
type Factory struct{}
71+
72+
// CreateTemplate implements the template.TemplateFactory interface
73+
func (f *Factory) CreateTemplate(renderBundle template.BundleRenderer) template.Template {
74+
return NewTemplate(renderBundle)
75+
}
76+
77+
// Schema implements the template.TemplateFactory interface
78+
func (f *Factory) Schema() string {
79+
return schema
80+
}
81+
82+
type BasicTemplateData struct {
83+
Schema string `json:"schema"`
84+
Entries []*declcfg.Meta `json:"entries"`
85+
}
86+
87+
func parseSpec(reader io.Reader) (*BasicTemplateData, error) {
88+
bt := &BasicTemplateData{}
89+
btDoc := json.RawMessage{}
90+
btDecoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
91+
err := btDecoder.Decode(&btDoc)
92+
if err != nil {
93+
return nil, fmt.Errorf("decoding template schema: %v", err)
94+
}
95+
err = json.Unmarshal(btDoc, bt)
96+
if err != nil {
97+
return nil, fmt.Errorf("unmarshalling template: %v", err)
98+
}
99+
100+
if bt.Schema != schema {
101+
return nil, fmt.Errorf("template has unknown schema (%q), should be %q", bt.Schema, schema)
102+
}
103+
104+
return bt, nil
105+
}
106+
71107
// isBundleTemplate identifies a Bundle template source as having a Schema and Image defined
72108
// but no Properties, RelatedImages or Package defined
73109
func isBundleTemplate(b *declcfg.Bundle) bool {
74110
return b.Schema != "" && b.Image != "" && b.Package == "" && len(b.Properties) == 0 && len(b.RelatedImages) == 0
75111
}
76112

77-
// FromReader reads FBC from a reader and generates a BasicTemplate from it
78-
func FromReader(r io.Reader) (*BasicTemplate, error) {
113+
// FromReader reads FBC from a reader and generates a BasicTemplateData from it
114+
func FromReader(r io.Reader) (*BasicTemplateData, error) {
79115
var entries []*declcfg.Meta
80116
if err := declcfg.WalkMetasReader(r, func(meta *declcfg.Meta, err error) error {
81117
if err != nil {
@@ -101,7 +137,7 @@ func FromReader(r io.Reader) (*BasicTemplate, error) {
101137
return nil, err
102138
}
103139

104-
bt := &BasicTemplate{
140+
bt := &BasicTemplateData{
105141
Schema: schema,
106142
Entries: entries,
107143
}

alpha/template/registry.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package template
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"text/tabwriter"
7+
)
8+
9+
var tr = NewTemplateRegistry()
10+
11+
// GetTemplateRegistry returns the global template registry
12+
func GetTemplateRegistry() *TemplateRegistry {
13+
return tr
14+
}
15+
16+
func (tr *TemplateRegistry) HelpText() string {
17+
var help strings.Builder
18+
supportedTypes := tr.GetSupportedTypes()
19+
help.WriteString("\n")
20+
tabber := tabwriter.NewWriter(&help, 0, 0, 1, ' ', 0)
21+
for _, t := range supportedTypes {
22+
fmt.Fprintf(tabber, " - %s\n", t)
23+
}
24+
tabber.Flush()
25+
return help.String()
26+
}

alpha/template/schema.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package template
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
9+
"k8s.io/apimachinery/pkg/util/yaml"
10+
)
11+
12+
// detectSchema reads the input, extracts the schema field, and returns a reader
13+
// that includes the consumed data followed by the remaining stream data.
14+
// This works when the input is stdin or a file (since stdin cannot be closed and reopened)
15+
// and complies with the requirement that each supplied schema has a defined "schema" field,
16+
// without attempting to load all input into memory.
17+
func detectSchema(reader io.Reader) (string, io.Reader, error) {
18+
// Capture what's read during schema detection
19+
var capturedData bytes.Buffer
20+
teeReader := io.TeeReader(reader, &capturedData)
21+
22+
// Read the input into a raw message
23+
rawDoc := json.RawMessage{}
24+
decoder := yaml.NewYAMLOrJSONDecoder(teeReader, 4096)
25+
err := decoder.Decode(&rawDoc)
26+
if err != nil {
27+
return "", nil, fmt.Errorf("decoding template input: %v", err)
28+
}
29+
30+
// Parse the raw message to extract schema
31+
var schemaDoc struct {
32+
Schema string `json:"schema"`
33+
}
34+
err = json.Unmarshal(rawDoc, &schemaDoc)
35+
if err != nil {
36+
return "", nil, fmt.Errorf("unmarshalling template schema: %v", err)
37+
}
38+
39+
if schemaDoc.Schema == "" {
40+
return "", nil, fmt.Errorf("template input missing required 'schema' field")
41+
}
42+
43+
// Create a reader that combines the captured data with the remaining stream
44+
replayReader := io.MultiReader(&capturedData, reader)
45+
46+
return schemaDoc.Schema, replayReader, nil
47+
}

alpha/template/semver/semver.go

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,55 @@ import (
1515

1616
"github.com/operator-framework/operator-registry/alpha/declcfg"
1717
"github.com/operator-framework/operator-registry/alpha/property"
18+
"github.com/operator-framework/operator-registry/alpha/template"
1819
)
1920

20-
func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error) {
21+
// IO structs -- BEGIN
22+
type semverTemplateBundleEntry struct {
23+
Image string `json:"image,omitempty"`
24+
}
25+
26+
type semverTemplateChannelBundles struct {
27+
Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"`
28+
}
29+
30+
type SemverTemplateData struct {
31+
Schema string `json:"schema"`
32+
GenerateMajorChannels bool `json:"generateMajorChannels,omitempty"`
33+
GenerateMinorChannels bool `json:"generateMinorChannels,omitempty"`
34+
DefaultChannelTypePreference streamType `json:"defaultChannelTypePreference,omitempty"`
35+
Candidate semverTemplateChannelBundles `json:"candidate,omitempty"`
36+
Fast semverTemplateChannelBundles `json:"fast,omitempty"`
37+
Stable semverTemplateChannelBundles `json:"stable,omitempty"`
38+
39+
pkg string `json:"-"` // the derived package name
40+
defaultChannel string `json:"-"` // detected "most stable" channel head
41+
}
42+
43+
// IO structs -- END
44+
45+
// SemverTemplate implements the common template interface
46+
type SemverTemplate struct {
47+
renderBundle template.BundleRenderer
48+
}
49+
50+
// NewTemplate creates a new semver template instance
51+
func NewTemplate(renderBundle template.BundleRenderer) template.Template {
52+
return &SemverTemplate{
53+
renderBundle: renderBundle,
54+
}
55+
}
56+
57+
// RenderBundle implements the template.Template interface
58+
func (t *SemverTemplate) RenderBundle(ctx context.Context, image string) (*declcfg.DeclarativeConfig, error) {
59+
return t.renderBundle(ctx, image)
60+
}
61+
62+
// Render implements the template.Template interface
63+
func (t *SemverTemplate) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
2164
var out declcfg.DeclarativeConfig
2265

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

61-
func buildBundleList(t semverTemplate) map[string]string {
104+
// Schema implements the template.Template interface
105+
func (t *SemverTemplate) Schema() string {
106+
return schema
107+
}
108+
109+
// Factory implements the template.TemplateFactory interface
110+
type Factory struct{}
111+
112+
// CreateTemplate implements the template.TemplateFactory interface
113+
func (f *Factory) CreateTemplate(renderBundle template.BundleRenderer) template.Template {
114+
return NewTemplate(renderBundle)
115+
}
116+
117+
// Schema implements the template.TemplateFactory interface
118+
func (f *Factory) Schema() string {
119+
return schema
120+
}
121+
122+
const schema string = "olm.semver"
123+
124+
func init() {
125+
template.GetTemplateRegistry().Register(&Factory{})
126+
}
127+
128+
// channel "archetypes", restricted in this iteration to just these
129+
type channelArchetype string
130+
131+
const (
132+
candidateChannelArchetype channelArchetype = "candidate"
133+
fastChannelArchetype channelArchetype = "fast"
134+
stableChannelArchetype channelArchetype = "stable"
135+
)
136+
137+
// mapping channel name --> stability, where higher values indicate greater stability
138+
var channelPriorities = map[channelArchetype]int{candidateChannelArchetype: 0, fastChannelArchetype: 1, stableChannelArchetype: 2}
139+
140+
// sorting capability for a slice according to the assigned channelPriorities
141+
type byChannelPriority []channelArchetype
142+
143+
func (b byChannelPriority) Len() int { return len(b) }
144+
func (b byChannelPriority) Less(i, j int) bool {
145+
return channelPriorities[b[i]] < channelPriorities[b[j]]
146+
}
147+
func (b byChannelPriority) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
148+
149+
type streamType string
150+
151+
const defaultStreamType streamType = ""
152+
const minorStreamType streamType = "minor"
153+
const majorStreamType streamType = "major"
154+
155+
// general preference for minor channels
156+
var streamTypePriorities = map[streamType]int{minorStreamType: 2, majorStreamType: 1, defaultStreamType: 0}
157+
158+
// map of archetypes --> bundles --> bundle-version from the input file
159+
type bundleVersions map[channelArchetype]map[string]semver.Version // e.g. srcv["stable"]["example-operator.v1.0.0"] = 1.0.0
160+
161+
// the "high-water channel" struct functions as a freely-rising indicator of the "most stable" channel head, so we can use that
162+
// later as the package's defaultChannel attribute
163+
type highwaterChannel struct {
164+
archetype channelArchetype
165+
kind streamType
166+
version semver.Version
167+
name string
168+
}
169+
170+
// entryTuple represents a channel entry with its associated metadata
171+
type entryTuple struct {
172+
arch channelArchetype
173+
kind streamType
174+
parent string
175+
name string
176+
version semver.Version
177+
index int
178+
}
179+
180+
func buildBundleList(t SemverTemplateData) map[string]string {
62181
dict := make(map[string]string)
63182
for _, bl := range []semverTemplateChannelBundles{t.Candidate, t.Fast, t.Stable} {
64183
for _, b := range bl.Bundles {
@@ -70,13 +189,13 @@ func buildBundleList(t semverTemplate) map[string]string {
70189
return dict
71190
}
72191

73-
func readFile(reader io.Reader) (*semverTemplate, error) {
192+
func readFile(reader io.Reader) (*SemverTemplateData, error) {
74193
data, err := io.ReadAll(reader)
75194
if err != nil {
76195
return nil, err
77196
}
78197

79-
sv := semverTemplate{}
198+
sv := SemverTemplateData{}
80199
if err := yaml.UnmarshalStrict(data, &sv); err != nil {
81200
return nil, err
82201
}
@@ -115,7 +234,7 @@ func readFile(reader io.Reader) (*semverTemplate, error) {
115234
return &sv, nil
116235
}
117236

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

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

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

154273
// we iterate over the channel bundles from the template, to:
@@ -210,7 +329,7 @@ func (sv *semverTemplate) getVersionsFromChannel(semverBundles []semverTemplateB
210329
// - 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.
211330
// 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
212331

213-
func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
332+
func (sv *SemverTemplateData) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
214333
outChannels := []declcfg.Channel{}
215334

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

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

293412
// sort to force partitioning by archetype --> kind --> semver

0 commit comments

Comments
 (0)