Skip to content

Commit d6531d6

Browse files
committed
detect template type from input, but respect explicit specification
Signed-off-by: grokspawn <[email protected]> Assisted-by: claude
1 parent 9cf7ce2 commit d6531d6

File tree

13 files changed

+600
-538
lines changed

13 files changed

+600
-538
lines changed

alpha/template/basic/basic.go

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,29 @@ 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)
18-
}
19-
2017
type BasicTemplate struct {
21-
Schema string `json:"schema"`
22-
Entries []*declcfg.Meta `json:"entries"`
18+
renderBundle template.BundleRenderer
2319
}
2420

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)
21+
// NewTemplate creates a new basic template instance
22+
func NewTemplate(renderBundle template.BundleRenderer) template.Template {
23+
return &BasicTemplate{
24+
renderBundle: renderBundle,
4025
}
26+
}
4127

42-
return bt, nil
28+
// RenderBundle implements the template.Template interface
29+
func (t *BasicTemplate) RenderBundle(ctx context.Context, image string) (*declcfg.DeclarativeConfig, error) {
30+
return t.renderBundle(ctx, image)
4331
}
4432

45-
func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
33+
// Render implements the template.Template interface
34+
func (t *BasicTemplate) Render(ctx context.Context, reader io.Reader) (*declcfg.DeclarativeConfig, error) {
4635
bt, err := parseSpec(reader)
4736
if err != nil {
4837
return nil, err
@@ -68,14 +57,57 @@ func (t Template) Render(ctx context.Context, reader io.Reader) (*declcfg.Declar
6857
return cfg, nil
6958
}
7059

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

77-
// FromReader reads FBC from a reader and generates a BasicTemplate from it
78-
func FromReader(r io.Reader) (*BasicTemplate, error) {
109+
// FromReader reads FBC from a reader and generates a BasicTemplateData from it
110+
func FromReader(r io.Reader) (*BasicTemplateData, error) {
79111
var entries []*declcfg.Meta
80112
if err := declcfg.WalkMetasReader(r, func(meta *declcfg.Meta, err error) error {
81113
if err != nil {
@@ -101,7 +133,7 @@ func FromReader(r io.Reader) (*BasicTemplate, error) {
101133
return nil, err
102134
}
103135

104-
bt := &BasicTemplate{
136+
bt := &BasicTemplateData{
105137
Schema: schema,
106138
Entries: entries,
107139
}

alpha/template/schema.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package template
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
8+
"k8s.io/apimachinery/pkg/util/yaml"
9+
)
10+
11+
// detectSchema reads the input and extracts the schema field
12+
func detectSchema(reader io.Reader) (string, error) {
13+
// Read the input into a raw message
14+
rawDoc := json.RawMessage{}
15+
decoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
16+
err := decoder.Decode(&rawDoc)
17+
if err != nil {
18+
return "", fmt.Errorf("decoding template input: %v", err)
19+
}
20+
21+
// Parse the raw message to extract schema
22+
var schemaDoc struct {
23+
Schema string `json:"schema"`
24+
}
25+
err = json.Unmarshal(rawDoc, &schemaDoc)
26+
if err != nil {
27+
return "", fmt.Errorf("unmarshalling template schema: %v", err)
28+
}
29+
30+
if schemaDoc.Schema == "" {
31+
return "", fmt.Errorf("template input missing required 'schema' field")
32+
}
33+
34+
return schemaDoc.Schema, nil
35+
}

alpha/template/semver/semver.go

Lines changed: 124 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,79 @@ 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+
// channel "archetypes", restricted in this iteration to just these
125+
type channelArchetype string
126+
127+
const (
128+
candidateChannelArchetype channelArchetype = "candidate"
129+
fastChannelArchetype channelArchetype = "fast"
130+
stableChannelArchetype channelArchetype = "stable"
131+
)
132+
133+
// mapping channel name --> stability, where higher values indicate greater stability
134+
var channelPriorities = map[channelArchetype]int{candidateChannelArchetype: 0, fastChannelArchetype: 1, stableChannelArchetype: 2}
135+
136+
// sorting capability for a slice according to the assigned channelPriorities
137+
type byChannelPriority []channelArchetype
138+
139+
func (b byChannelPriority) Len() int { return len(b) }
140+
func (b byChannelPriority) Less(i, j int) bool {
141+
return channelPriorities[b[i]] < channelPriorities[b[j]]
142+
}
143+
func (b byChannelPriority) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
144+
145+
type streamType string
146+
147+
const defaultStreamType streamType = ""
148+
const minorStreamType streamType = "minor"
149+
const majorStreamType streamType = "major"
150+
151+
// general preference for minor channels
152+
var streamTypePriorities = map[streamType]int{minorStreamType: 2, majorStreamType: 1, defaultStreamType: 0}
153+
154+
// map of archetypes --> bundles --> bundle-version from the input file
155+
type bundleVersions map[channelArchetype]map[string]semver.Version // e.g. srcv["stable"]["example-operator.v1.0.0"] = 1.0.0
156+
157+
// the "high-water channel" struct functions as a freely-rising indicator of the "most stable" channel head, so we can use that
158+
// later as the package's defaultChannel attribute
159+
type highwaterChannel struct {
160+
archetype channelArchetype
161+
kind streamType
162+
version semver.Version
163+
name string
164+
}
165+
166+
// entryTuple represents a channel entry with its associated metadata
167+
type entryTuple struct {
168+
arch channelArchetype
169+
kind streamType
170+
parent string
171+
name string
172+
version semver.Version
173+
index int
174+
}
175+
176+
func buildBundleList(t SemverTemplateData) map[string]string {
62177
dict := make(map[string]string)
63178
for _, bl := range []semverTemplateChannelBundles{t.Candidate, t.Fast, t.Stable} {
64179
for _, b := range bl.Bundles {
@@ -70,13 +185,13 @@ func buildBundleList(t semverTemplate) map[string]string {
70185
return dict
71186
}
72187

73-
func readFile(reader io.Reader) (*semverTemplate, error) {
188+
func readFile(reader io.Reader) (*SemverTemplateData, error) {
74189
data, err := io.ReadAll(reader)
75190
if err != nil {
76191
return nil, err
77192
}
78193

79-
sv := semverTemplate{}
194+
sv := SemverTemplateData{}
80195
if err := yaml.UnmarshalStrict(data, &sv); err != nil {
81196
return nil, err
82197
}
@@ -115,7 +230,7 @@ func readFile(reader io.Reader) (*semverTemplate, error) {
115230
return &sv, nil
116231
}
117232

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

121236
bdm, err := sv.getVersionsFromChannel(sv.Candidate.Bundles, bundleDict, cfg)
@@ -148,7 +263,7 @@ func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.Declarati
148263
return &versions, nil
149264
}
150265

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

154269
// we iterate over the channel bundles from the template, to:
@@ -210,7 +325,7 @@ func (sv *semverTemplate) getVersionsFromChannel(semverBundles []semverTemplateB
210325
// - 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.
211326
// 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
212327

213-
func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
328+
func (sv *SemverTemplateData) generateChannels(semverChannels *bundleVersions) []declcfg.Channel {
214329
outChannels := []declcfg.Channel{}
215330

216331
// sort the channel archetypes in ascending order so we can traverse the bundles in order of
@@ -287,7 +402,7 @@ func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []dec
287402
return outChannels
288403
}
289404

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

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

0 commit comments

Comments
 (0)