-
Notifications
You must be signed in to change notification settings - Fork 260
detect template type from input, but respect explicit specification #1844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
| Schema string `json:"schema"` | ||
| Entries []*declcfg.Meta `json:"entries"` | ||
| type basicTemplate struct { | ||
| 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) | ||
| // New creates a new basic template instance | ||
| func New(renderBundle template.BundleRenderer) template.Template { | ||
| return &basicTemplate{ | ||
| renderBundle: renderBundle, | ||
| } | ||
| } | ||
|
|
||
| return bt, nil | ||
| // RenderBundle implements the template.Template interface | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given that we call |
||
| return New(renderBundle) | ||
| } | ||
|
|
||
| // Schema implements the template.TemplateFactory interface | ||
| func (f *Factory) Schema() string { | ||
| return schema | ||
| } | ||
|
|
||
| type BasicTemplateData struct { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it need to be public? |
||
| 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 { | ||
|
|
@@ -101,7 +137,7 @@ func FromReader(r io.Reader) (*BasicTemplate, error) { | |
| return nil, err | ||
| } | ||
|
|
||
| bt := &BasicTemplate{ | ||
| bt := &BasicTemplateData{ | ||
| Schema: schema, | ||
| Entries: entries, | ||
| } | ||
|
|
||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we return here an interface instead? |
||
| 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() | ||
| } | ||
| 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 | ||
| } | ||
grokspawn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given that we are in |
||
| Image string `json:"image,omitempty"` | ||
| } | ||
|
|
||
| type semverTemplateChannelBundles struct { | ||
| Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"` | ||
| } | ||
|
|
||
| type SemverTemplateData struct { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this need to be public? It is a bit strange that a caller get access to a type, that has member fields of a private type. |
||
| 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 | ||
| } | ||
|
|
||
| // New creates a new semver template instance | ||
| func New(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) | ||
| } | ||
|
|
@@ -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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same question as for the basic factory. |
||
| return New(renderBundle) | ||
| } | ||
|
|
||
| // Schema implements the template.TemplateFactory interface | ||
| func (f *Factory) Schema() string { | ||
| return schema | ||
| } | ||
|
|
||
| const schema string = "olm.semver" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you move it at the top of the file? |
||
|
|
||
| func init() { | ||
| template.GetTemplateRegistry().Register(&Factory{}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same question as for basic 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} | ||
|
Comment on lines
+129
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you move it to the top of the file? |
||
|
|
||
| // 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 | ||
grokspawn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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 { | ||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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) | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this init funciton is called only when we import
alpha/template/basicpackage. Hence, only then we are going to register this Factory.IMHO, we should register it at the other end - within
alpha/template/registry.go- in that way all available factories will be registered always.