diff --git a/alpha/declcfg/write.go b/alpha/declcfg/write.go index 3a66829ef..c2d869905 100644 --- a/alpha/declcfg/write.go +++ b/alpha/declcfg/write.go @@ -95,7 +95,7 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer) // build increasing-version-ordered bundle names, so we can meaningfully iterate over a range orderedBundles := []string{} - for n, _ := range versionMap { + for n := range versionMap { orderedBundles = append(orderedBundles, n) } sort.Slice(orderedBundles, func(i, j int) bool { @@ -153,7 +153,7 @@ func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer) out.Write([]byte("graph LR\n")) pkgNames := []string{} - for pname, _ := range pkgs { + for pname := range pkgs { pkgNames = append(pkgNames, pname) } sort.Slice(pkgNames, func(i, j int) bool { diff --git a/alpha/template/semver/README.md b/alpha/template/semver/README.md index 2bab90c1c..ace33746a 100644 --- a/alpha/template/semver/README.md +++ b/alpha/template/semver/README.md @@ -2,7 +2,7 @@ Since a `catalog template` is identified as an input schema which may be processed to generate a valid FBC, we can define a `semver template` as a schema which uses channel conventions to facilitate the auto-generation of channels along `semver` delimiters. -[**DISCLAIMER:** since version build metadata [MUST be ignored when determining version precedence](https://semver.org) when using semver, it cannot be used in any bundle included in the `semver template` and will result in a fatal error.] +[**DISCLAIMER:** since version build metadata [MUST be ignored when determining version precedence](https://semver.org) when using semver, rendering the template will result in an error if two bundles differ only by the build metadata.] ### Schema Goals The `semver template` must have: @@ -85,7 +85,7 @@ Note that if the command is called without a file argument and nothing passed in the command will hang indefinitely. Either a file argument or file information passed in on standard input is required by the command. -With the template attribute `GenerateMajorChannels: true` resulting major channels from the command are (skipping the rendered bundle image output): +With the template attribute `GenerateMajorChannels: true` resulting major channels from the command are (filtering out `olm.bundle` content): ```yaml --- defaultChannel: stable-v1 @@ -93,59 +93,73 @@ name: testoperator schema: olm.package --- entries: -- name: testoperator.v0.1.0 -- name: testoperator.v0.1.1 -- name: testoperator.v0.1.2 -- name: testoperator.v0.1.3 - skips: - - testoperator.v0.1.0 - - testoperator.v0.1.1 - - testoperator.v0.1.2 -- name: testoperator.v0.2.0 -- name: testoperator.v0.2.1 -- name: testoperator.v0.2.2 - replaces: testoperator.v0.1.3 - skips: - - testoperator.v0.2.0 - - testoperator.v0.2.1 -- name: testoperator.v0.3.0 - replaces: testoperator.v0.2.2 + - name: testoperator.v0.1.0 + - name: testoperator.v0.1.1 + - name: testoperator.v0.1.2 + - name: testoperator.v0.1.3 + skips: + - testoperator.v0.1.0 + - testoperator.v0.1.1 + - testoperator.v0.1.2 + - name: testoperator.v0.2.0 + - name: testoperator.v0.2.1 + - name: testoperator.v0.2.2 + replaces: testoperator.v0.1.3 + skips: + - testoperator.v0.1.0 + - testoperator.v0.1.1 + - testoperator.v0.1.2 + - testoperator.v0.2.0 + - testoperator.v0.2.1 + - name: testoperator.v0.3.0 + replaces: testoperator.v0.2.2 + skips: + - testoperator.v0.1.0 + - testoperator.v0.1.1 + - testoperator.v0.1.2 + - testoperator.v0.1.3 + - testoperator.v0.2.0 + - testoperator.v0.2.1 name: candidate-v0 package: testoperator schema: olm.channel --- entries: -- name: testoperator.v1.0.0 -- name: testoperator.v1.0.1 - skips: - - testoperator.v1.0.0 -- name: testoperator.v1.1.0 - replaces: testoperator.v1.0.1 + - name: testoperator.v1.0.0 + - name: testoperator.v1.0.1 + skips: + - testoperator.v1.0.0 + - name: testoperator.v1.1.0 + replaces: testoperator.v1.0.1 + skips: + - testoperator.v1.0.0 name: candidate-v1 package: testoperator schema: olm.channel --- entries: -- name: testoperator.v0.2.1 -- name: testoperator.v0.2.2 - skips: - - testoperator.v0.2.1 -- name: testoperator.v0.3.0 - replaces: testoperator.v0.2.2 + - name: testoperator.v0.2.1 + - name: testoperator.v0.2.2 + skips: + - testoperator.v0.2.1 + - name: testoperator.v0.3.0 + replaces: testoperator.v0.2.2 + skips: + - testoperator.v0.2.1 name: fast-v0 package: testoperator schema: olm.channel --- entries: -- name: testoperator.v1.0.1 -- name: testoperator.v1.1.0 - replaces: testoperator.v1.0.1 + - name: testoperator.v1.0.1 + - name: testoperator.v1.1.0 + replaces: testoperator.v1.0.1 name: fast-v1 package: testoperator schema: olm.channel --- entries: -- name: testoperator.v1.0.1 + - name: testoperator.v1.0.1 name: stable-v1 package: testoperator schema: olm.channel @@ -180,6 +194,9 @@ entries: - name: testoperator.v0.2.2 replaces: testoperator.v0.1.3 skips: + - testoperator.v0.1.0 + - testoperator.v0.1.1 + - testoperator.v0.1.2 - testoperator.v0.2.0 - testoperator.v0.2.1 name: candidate-v0.2 @@ -189,6 +206,13 @@ schema: olm.channel entries: - name: testoperator.v0.3.0 replaces: testoperator.v0.2.2 + skips: + - testoperator.v0.1.0 + - testoperator.v0.1.1 + - testoperator.v0.1.2 + - testoperator.v0.1.3 + - testoperator.v0.2.0 + - testoperator.v0.2.1 name: candidate-v0.3 package: testoperator schema: olm.channel @@ -205,6 +229,8 @@ schema: olm.channel entries: - name: testoperator.v1.1.0 replaces: testoperator.v1.0.1 + skips: + - testoperator.v1.0.0 name: candidate-v1.1 package: testoperator schema: olm.channel @@ -221,6 +247,8 @@ schema: olm.channel entries: - name: testoperator.v0.3.0 replaces: testoperator.v0.2.2 + skips: + - testoperator.v0.2.1 name: fast-v0.3 package: testoperator schema: olm.channel @@ -243,9 +271,8 @@ entries: name: stable-v1.0 package: testoperator schema: olm.channel - ``` -Here, a channel is generated for each template channel which differs by minor version, and each channel has a `replaces` edge from the predecessor channel to the next-lesser minor bundle version. Please note that at no time do we transgress across major-version boundaries with the channels, to be consistent with [the semver convention](https://semver.org/) for major versions, where the purpose is to make incompatible API changes. +Here, a channel is generated for each template channel which differs by minor version, each channel has a `replaces` edge from the highest version entry in the predecessor channel, and the highest version entry in each channel also has a skips list composed of all lower version entries within the same minor (Y). Please note that at no time do we transgress across major-version boundaries with the channels, to be consistent with [the semver convention](https://semver.org/) for major versions, where the purpose is to make incompatible API changes. ### DEMOS diff --git a/alpha/template/semver/major-version-demo.gif b/alpha/template/semver/major-version-demo.gif old mode 100755 new mode 100644 index 244ecd6db..21e570771 Binary files a/alpha/template/semver/major-version-demo.gif and b/alpha/template/semver/major-version-demo.gif differ diff --git a/alpha/template/semver/minor-version-demo.gif b/alpha/template/semver/minor-version-demo.gif old mode 100755 new mode 100644 index af0fea0b3..c3838b62c Binary files a/alpha/template/semver/minor-version-demo.gif and b/alpha/template/semver/minor-version-demo.gif differ diff --git a/alpha/template/semver/semver.go b/alpha/template/semver/semver.go index 601047d26..ea3269a11 100644 --- a/alpha/template/semver/semver.go +++ b/alpha/template/semver/semver.go @@ -2,7 +2,6 @@ package semver import ( "context" - "encoding/json" "fmt" "io" "sort" @@ -15,74 +14,14 @@ import ( "github.com/operator-framework/operator-registry/alpha/action" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/alpha/property" - "github.com/operator-framework/operator-registry/pkg/image" ) -// data passed into this module externally -type Template struct { - Data io.Reader - Registry image.Registry -} - -// IO structs -- BEGIN -type semverTemplateBundleEntry struct { - Image string `json:"image,omitempty"` -} - -type candidateBundles struct { - Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"` -} -type fastBundles struct { - Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"` -} -type stableBundles struct { - Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"` -} - -type semverTemplate struct { - Schema string `json:"schema"` - GenerateMajorChannels bool `json:"generateMajorChannels,omitempty"` - GenerateMinorChannels bool `json:"generateMinorChannels,omitempty"` - AvoidSkipPatch bool `json:"avoidSkipPatch,omitempty"` - Candidate candidateBundles `json:"candidate,omitempty"` - Fast fastBundles `json:"fast,omitempty"` - Stable stableBundles `json:"stable,omitempty"` - - pkg string `json:"-"` // the derived package name - defaultChannel string `json:"-"` // detected "most stable" channel head -} - -// IO structs -- END - -// channel "kinds", restricted in this iteration to just these -const ( - candidateChannelName string = "candidate" - fastChannelName string = "fast" - stableChannelName string = "stable" -) - -// mapping channel name --> stability, where higher values indicate greater stability -var channelPriorities = map[string]int{candidateChannelName: 0, fastChannelName: 1, stableChannelName: 2} - -// sorting capability for a slice according to the assigned channelPriorities -type byChannelPriority []string - -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] } - -// map of channels : bundles : bundle-version -// channels --> bundles --> version -type semverRenderedChannelVersions map[string]map[string]semver.Version // e.g. d["stable-v1"]["example-operator/v1.0.0"] = 1.0.0 - func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error) { var out declcfg.DeclarativeConfig sv, err := readFile(t.Data) if err != nil { - return nil, fmt.Errorf("semver-render: unable to read file: %v", err) + return nil, fmt.Errorf("render: unable to read file: %v", err) } var cfgs []declcfg.DeclarativeConfig @@ -92,7 +31,7 @@ func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error buildBundleList(&sv.Fast.Bundles, &bundleDict) buildBundleList(&sv.Stable.Bundles, &bundleDict) - for b, _ := range bundleDict { + for b := range bundleDict { r := action.Render{ AllowedRefMask: action.RefBundleImage, Refs: []string{b}, @@ -107,12 +46,12 @@ func (t Template) Render(ctx context.Context) (*declcfg.DeclarativeConfig, error out = *combineConfigs(cfgs) if len(out.Bundles) == 0 { - return nil, fmt.Errorf("semver-render: no bundles specified or no bundles could be rendered") + return nil, fmt.Errorf("render: no bundles specified or no bundles could be rendered") } channelBundleVersions, err := sv.getVersionsFromStandardChannels(&out) if err != nil { - return nil, fmt.Errorf("semver-render: unable to post-process bundle info: %v", err) + return nil, fmt.Errorf("render: unable to post-process bundle info: %v", err) } channels := sv.generateChannels(channelBundleVersions) @@ -136,23 +75,22 @@ func readFile(reader io.Reader) (*semverTemplate, error) { return nil, err } - // default behavior is to generate only minor channels and to use skips over replaces + // default behavior is to generate only minor channels sv := semverTemplate{ GenerateMajorChannels: false, GenerateMinorChannels: true, - AvoidSkipPatch: false, } - if err := yaml.Unmarshal(data, &sv, func(decoder *json.Decoder) *json.Decoder { - decoder.DisallowUnknownFields() - return decoder - }); err != nil { + if err := yaml.UnmarshalStrict(data, &sv); err != nil { return nil, err } + if sv.Schema != schema { + return nil, fmt.Errorf("readFile: input file has unknown schema, should be %q", schema) + } return &sv, nil } -func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.DeclarativeConfig) (*semverRenderedChannelVersions, error) { - versions := semverRenderedChannelVersions{} +func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.DeclarativeConfig) (*bundleVersions, error) { + versions := bundleVersions{} bdm, err := sv.getVersionsFromChannel(sv.Candidate.Bundles, cfg) if err != nil { @@ -161,7 +99,7 @@ func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.Declarati if err = validateVersions(&bdm); err != nil { return nil, err } - versions[candidateChannelName] = bdm + versions[candidateChannelArchetype] = bdm bdm, err = sv.getVersionsFromChannel(sv.Fast.Bundles, cfg) if err != nil { @@ -170,7 +108,7 @@ func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.Declarati if err = validateVersions(&bdm); err != nil { return nil, err } - versions[fastChannelName] = bdm + versions[fastChannelArchetype] = bdm bdm, err = sv.getVersionsFromChannel(sv.Stable.Bundles, cfg) if err != nil { @@ -179,7 +117,7 @@ func (sv *semverTemplate) getVersionsFromStandardChannels(cfg *declcfg.Declarati if err = validateVersions(&bdm); err != nil { return nil, err } - versions[stableChannelName] = bdm + versions[stableChannelArchetype] = bdm return &versions, nil } @@ -240,42 +178,31 @@ func (sv *semverTemplate) getVersionsFromChannel(semverBundles []semverTemplateB return entries, nil } -// 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 { - kind string - version semver.Version - name string -} - -func (h *highwaterChannel) gt(ih *highwaterChannel) bool { - return (channelPriorities[h.kind] > channelPriorities[ih.kind]) || (h.version.GT(ih.version)) -} - // generates an unlinked channel for each channel as per the input template config (major || minor), then link up the edges of the set of channels so that: +// - for minor version increase, the new edge replaces the previous // - (for major channels) iterating to a new minor version channel (traversing between Y-streams) creates a 'replaces' edge between the predecessor and successor bundles -// - within the same minor version (Y-stream), the head of the channel should have a 'skips' encompassing all lesser minor versions of the bundle enumerated in the template. +// - 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 *semverRenderedChannelVersions) []declcfg.Channel { + +func (sv *semverTemplate) generateChannels(semverChannels *bundleVersions) []declcfg.Channel { outChannels := []declcfg.Channel{} - // sort the channelkinds in ascending order so we can traverse the bundles in order of + // sort the channel archetypes in ascending order so we can traverse the bundles in order of // their source channel's priority - var keysByPriority []string - for k, _ := range channelPriorities { - keysByPriority = append(keysByPriority, k) + var archetypesByPriority []channelArchetype + for k := range channelPriorities { + archetypesByPriority = append(archetypesByPriority, k) } - sort.Sort(byChannelPriority(keysByPriority)) + sort.Sort(byChannelPriority(archetypesByPriority)) // set to the least-priority channel - hwc := highwaterChannel{kind: keysByPriority[0], version: semver.Version{Major: 0, Minor: 0}} + hwc := highwaterChannel{archetype: archetypesByPriority[0], version: semver.Version{Major: 0, Minor: 0}} - // mapping the generated channel name to the original semver name (i.e. channel kind), so we can do generated-channel-name --> original-semver-name --> version mapping, later - channelMapping := map[string]string{} - - for _, k := range keysByPriority { - bundles := (*semverChannels)[k] + unlinkedChannels := make(map[string]*declcfg.Channel) + unassociatedEdges := []entryTuple{} + for _, archetype := range archetypesByPriority { + bundles := (*semverChannels)[archetype] // skip channel if empty if len(bundles) == 0 { continue @@ -290,116 +217,128 @@ func (sv *semverTemplate) generateChannels(semverChannels *semverRenderedChannel return bundles[bundleNamesByVersion[i]].LT(bundles[bundleNamesByVersion[j]]) }) - majors := map[string]*declcfg.Channel{} - minors := map[string]*declcfg.Channel{} - - for _, b := range bundleNamesByVersion { + // for each bundle (by version): + // for each of Major/Minor setting (since they're independent) + // retrieve the existing channel object, or create a channel (by criteria major/minor) if one doesn't exist + // add a new edge entry based on the bundle name + // save the channel name --> channel archetype mapping + // test the channel object for 'more stable' than previous best + for _, bundleName := range bundleNamesByVersion { + // a dodge to avoid duplicating channel processing body; accumulate a map of the channels which need creating from the bundle + // we need to associate by kind so we can partition the resulting entries + channelNameKeys := make(map[streamType]string) if sv.GenerateMajorChannels { - testChannelName := channelNameFromMajor(k, bundles[b]) - ch, ok := majors[testChannelName] - if !ok { - ch = newChannel(sv.pkg, testChannelName) - majors[testChannelName] = ch - } - ch.Entries = append(ch.Entries, declcfg.ChannelEntry{Name: b}) - - channelMapping[testChannelName] = k - - hwcCandidate := highwaterChannel{kind: k, version: bundles[b], name: testChannelName} - if hwcCandidate.gt(&hwc) { - hwc = hwcCandidate - } + channelNameKeys[majorStreamType] = channelNameFromMajor(archetype, bundles[bundleName]) } if sv.GenerateMinorChannels { - testChannelName := channelNameFromMinor(k, bundles[b]) - ch, ok := minors[testChannelName] + channelNameKeys[minorStreamType] = channelNameFromMinor(archetype, bundles[bundleName]) + } + + for cKey, cName := range channelNameKeys { + ch, ok := unlinkedChannels[cName] if !ok { - ch = newChannel(sv.pkg, testChannelName) - minors[testChannelName] = ch - } - ch.Entries = append(ch.Entries, declcfg.ChannelEntry{Name: b}) + ch = newChannel(sv.pkg, cName) - channelMapping[testChannelName] = k + unlinkedChannels[cName] = ch - hwcCandidate := highwaterChannel{kind: k, version: bundles[b], name: testChannelName} - if hwcCandidate.gt(&hwc) { - hwc = hwcCandidate + hwcCandidate := highwaterChannel{archetype: archetype, version: bundles[bundleName], name: cName} + if hwcCandidate.gt(&hwc) { + hwc = hwcCandidate + } } + ch.Entries = append(ch.Entries, declcfg.ChannelEntry{Name: bundleName}) + unassociatedEdges = append(unassociatedEdges, entryTuple{arch: archetype, kind: cKey, parent: cName, name: bundleName, version: bundles[bundleName], index: len(ch.Entries) - 1}) } } - - outChannels = append(outChannels, sv.linkChannels(majors, sv.pkg, semverChannels, &channelMapping)...) - outChannels = append(outChannels, sv.linkChannels(minors, sv.pkg, semverChannels, &channelMapping)...) } // save off the name of the high-water-mark channel for the default for this package sv.defaultChannel = hwc.name + outChannels = append(outChannels, sv.linkChannels(unlinkedChannels, unassociatedEdges)...) + return outChannels } -// all channels that come to linkChannels MUST have the same prefix. This adds replaces edges of minor versions of the largest major version. -func (sv *semverTemplate) linkChannels(unlinkedChannels map[string]*declcfg.Channel, pkg string, semverChannels *semverRenderedChannelVersions, channelMapping *map[string]string) []declcfg.Channel { +func (sv *semverTemplate) linkChannels(unlinkedChannels map[string]*declcfg.Channel, entries []entryTuple) []declcfg.Channel { channels := []declcfg.Channel{} - for channelName, channel := range unlinkedChannels { - // sort the channel entries in ascending order, according to the corresponding bundle versions for the channelName stored in the semverRenderedChannelVersions - // convenience function, to make this more clear - versionLookup := func(generatedChannelName string, channelEdgeIndex int) semver.Version { - channelKind := (*channelMapping)[generatedChannelName] - bundleVersions := (*semverChannels)[channelKind] - bundleName := channel.Entries[channelEdgeIndex].Name - return bundleVersions[bundleName] + // sort to force partitioning by archetype --> kind --> semver + sort.Slice(entries, func(i, j int) bool { + if channelPriorities[entries[i].arch] != channelPriorities[entries[j].arch] { + return channelPriorities[entries[i].arch] < channelPriorities[entries[j].arch] } - - sort.Slice(channel.Entries, func(i, j int) bool { - return versionLookup(channelName, i).LT(versionLookup(channelName, j)) - }) - - // link up the edges according to config - if sv.AvoidSkipPatch { - for i := 1; i < len(channel.Entries); i++ { - channel.Entries[i] = declcfg.ChannelEntry{ - Name: channel.Entries[i].Name, - Replaces: channel.Entries[i-1].Name, - } + if streamTypePriorities[entries[i].kind] != streamTypePriorities[entries[j].kind] { + return streamTypePriorities[entries[i].kind] < streamTypePriorities[entries[j].kind] + } + return entries[i].version.LT(entries[j].version) + }) + + prevZMax := "" + var curSkips sets.String = sets.NewString() + + for index := 1; index < len(entries); index++ { + prevTuple := entries[index-1] + curTuple := entries[index] + prevX := getMajorVersion(prevTuple.version) + prevY := getMinorVersion(prevTuple.version) + curX := getMajorVersion(curTuple.version) + curY := getMinorVersion(curTuple.version) + + archChange := curTuple.arch != prevTuple.arch + kindChange := curTuple.kind != prevTuple.kind + xChange := !prevX.EQ(curX) + yChange := !prevY.EQ(curY) + + if archChange || kindChange || xChange || yChange { + // if we passed any kind of change besides Z, then we need to set skips/replaces for previous max-Z + prevChannel := unlinkedChannels[prevTuple.parent] + finalEntry := &prevChannel.Entries[prevTuple.index] + finalEntry.Replaces = prevZMax + // don't include replaces in skips list, but they are accumulated in discrete cycles (and maybe useful for later channels) so remove here + if curSkips.Has(finalEntry.Replaces) { + finalEntry.Skips = curSkips.Difference(sets.NewString(finalEntry.Replaces)).List() + } else { + finalEntry.Skips = curSkips.List() } + } + + if archChange || kindChange || xChange { + // we don't maintain skips/replaces over these transitions + curSkips = sets.NewString() + prevZMax = "" } else { - curIndex := len(channel.Entries) - 1 - curMinor := getMinorVersion((*semverChannels)[(*channelMapping)[channelName]][channel.Entries[curIndex].Name]) - curSkips := sets.NewString() - for i := len(channel.Entries) - 2; i >= 0; i-- { - thisName := channel.Entries[i].Name - thisMinor := getMinorVersion((*semverChannels)[(*channelMapping)[channelName]][thisName]) - if thisMinor.EQ(curMinor) { - channel.Entries[i] = declcfg.ChannelEntry{Name: thisName} - curSkips = curSkips.Insert(thisName) - } else { - channel.Entries[curIndex] = declcfg.ChannelEntry{ - Name: channel.Entries[curIndex].Name, - Replaces: thisName, - Skips: curSkips.List(), - } - curSkips = sets.NewString() - curIndex = i - curMinor = thisMinor - } - } - channel.Entries[curIndex] = declcfg.ChannelEntry{ - Name: channel.Entries[curIndex].Name, - Skips: curSkips.List(), + if yChange { + prevZMax = prevTuple.name } + curSkips.Insert(prevTuple.name) } - channels = append(channels, *channel) } + + // last entry accumulation + lastTuple := entries[len(entries)-1] + prevChannel := unlinkedChannels[lastTuple.parent] + finalEntry := &prevChannel.Entries[lastTuple.index] + finalEntry.Replaces = prevZMax + // don't include replaces in skips list, but they are accumulated in discrete cycles (and maybe useful for later channels) so remove here + if curSkips.Has(finalEntry.Replaces) { + finalEntry.Skips = curSkips.Difference(sets.NewString(finalEntry.Replaces)).List() + } else { + finalEntry.Skips = curSkips.List() + } + + for _, ch := range unlinkedChannels { + channels = append(channels, *ch) + } + return channels } -func channelNameFromMinor(prefix string, version semver.Version) string { +func channelNameFromMinor(prefix channelArchetype, version semver.Version) string { return fmt.Sprintf("%s-v%d.%d", prefix, version.Major, version.Minor) } -func channelNameFromMajor(prefix string, version semver.Version) string { +func channelNameFromMajor(prefix channelArchetype, version semver.Version) string { return fmt.Sprintf("%s-v%d", prefix, version.Major) } @@ -438,6 +377,12 @@ func getMinorVersion(v semver.Version) semver.Version { } } +func getMajorVersion(v semver.Version) semver.Version { + return semver.Version{ + Major: v.Major, + } +} + func withoutBuildMetadataConflict(versions *map[string]semver.Version) error { errs := []error{} diff --git a/alpha/template/semver/semver_test.go b/alpha/template/semver/semver_test.go index d712578c0..6eec13260 100644 --- a/alpha/template/semver/semver_test.go +++ b/alpha/template/semver/semver_test.go @@ -12,31 +12,25 @@ import ( ) func TestLinkChannels(t *testing.T) { - // type semverRenderedChannelVersions map[string]map[string]semver.Version // e.g. d["stable-v1"]["example-operator/v1.0.0"] = 1.0.0 - channelOperatorVersions := semverRenderedChannelVersions{ - "stable": { - "a-v0.1.0": semver.MustParse("0.1.0"), - "a-v0.1.1": semver.MustParse("0.1.1"), - "a-v1.1.0": semver.MustParse("1.1.0"), - "a-v1.2.1": semver.MustParse("1.2.1"), - "a-v1.3.1": semver.MustParse("1.3.1"), - "a-v2.1.0": semver.MustParse("2.1.0"), - "a-v2.1.1": semver.MustParse("2.1.1"), - "a-v2.3.1": semver.MustParse("2.3.1"), - "a-v2.3.2": semver.MustParse("2.3.2"), - }, - } - // map[string]string - channelNameToKind := map[string]string{ - "stable-v0": "stable", - "stable-v1": "stable", - "stable-v2": "stable", - "stable-v0.1": "stable", - "stable-v1.1": "stable", - "stable-v1.2": "stable", - "stable-v1.3": "stable", - "stable-v2.1": "stable", - "stable-v2.3": "stable", + // type entryTuple struct { + // arch channelArchetype + // kind streamType + // name string + // parent string + // index int + // version semver.Version + // } + + majorChannelEntries := []entryTuple{ + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v0.1.0", parent: "stable-v0", index: 0, version: semver.MustParse("0.1.0")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v0.1.1", parent: "stable-v0", index: 1, version: semver.MustParse("0.1.1")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v1.1.0", parent: "stable-v1", index: 0, version: semver.MustParse("1.1.0")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v1.2.1", parent: "stable-v1", index: 1, version: semver.MustParse("1.2.1")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v1.3.1", parent: "stable-v1", index: 2, version: semver.MustParse("1.3.1")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v2.1.0", parent: "stable-v2", index: 0, version: semver.MustParse("2.1.0")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v2.1.1", parent: "stable-v2", index: 1, version: semver.MustParse("2.1.1")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v2.3.1", parent: "stable-v2", index: 2, version: semver.MustParse("2.3.1")}, + {arch: stableChannelArchetype, kind: majorStreamType, name: "a-v2.3.2", parent: "stable-v2", index: 3, version: semver.MustParse("2.3.2")}, } majorGeneratedUnlinkedChannels := map[string]*declcfg.Channel{ @@ -72,120 +66,16 @@ func TestLinkChannels(t *testing.T) { }, } - minorGeneratedUnlinkedChannels := map[string]*declcfg.Channel{ - "stable-v0.1": { - Schema: "olm.channel", - Name: "stable-v0.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v0.1.0"}, - {Name: "a-v0.1.1"}, - }, - }, - "stable-v1.1": { - Schema: "olm.channel", - Name: "stable-v1.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.1.0"}, - }, - }, - "stable-v1.2": { - Schema: "olm.channel", - Name: "stable-v1.2", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.2.1"}, - }, - }, - "stable-v1.3": { - Schema: "olm.channel", - Name: "stable-v1.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.3.1"}, - }, - }, - "stable-v2.1": { - Schema: "olm.channel", - Name: "stable-v2.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.1.0"}, - {Name: "a-v2.1.1"}, - }, - }, - "stable-v2.3": { - Schema: "olm.channel", - Name: "stable-v2.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.3.1"}, - {Name: "a-v2.3.2"}, - }, - }, - "stable-v3.1": { - Schema: "olm.channel", - Name: "stable-v3.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v3.1.0"}, - {Name: "a-v3.1.1"}, - }, - }, - } - tests := []struct { name string unlinkedChannels map[string]*declcfg.Channel - avoidSkipPatch bool generateMinorChannels bool generateMajorChannels bool out []declcfg.Channel }{ { - name: "NoSkipPatch/No edges between successive major channels", - unlinkedChannels: majorGeneratedUnlinkedChannels, - avoidSkipPatch: true, - generateMinorChannels: false, - generateMajorChannels: true, - out: []declcfg.Channel{ - { - Schema: "olm.channel", - Name: "stable-v0", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v0.1.0", Replaces: ""}, - {Name: "a-v0.1.1", Replaces: "a-v0.1.0"}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.1.0", Replaces: ""}, - {Name: "a-v1.2.1", Replaces: "a-v1.1.0"}, - {Name: "a-v1.3.1", Replaces: "a-v1.2.1"}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v2", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.1.0", Replaces: ""}, - {Name: "a-v2.1.1", Replaces: "a-v2.1.0"}, - {Name: "a-v2.3.1", Replaces: "a-v2.1.1"}, - {Name: "a-v2.3.2", Replaces: "a-v2.3.1"}, - }, - }, - }, - }, - { - name: "SkipPatch/No edges between successive major channels", + name: "No edges between successive major channels", unlinkedChannels: majorGeneratedUnlinkedChannels, - avoidSkipPatch: false, generateMinorChannels: false, generateMajorChannels: true, out: []declcfg.Channel{ @@ -205,7 +95,7 @@ func TestLinkChannels(t *testing.T) { Entries: []declcfg.ChannelEntry{ {Name: "a-v1.1.0", Replaces: "", Skips: []string{}}, {Name: "a-v1.2.1", Replaces: "a-v1.1.0", Skips: []string{}}, - {Name: "a-v1.3.1", Replaces: "a-v1.2.1", Skips: []string{}}, + {Name: "a-v1.3.1", Replaces: "a-v1.2.1", Skips: []string{"a-v1.1.0"}}, }, }, { @@ -216,145 +106,7 @@ func TestLinkChannels(t *testing.T) { {Name: "a-v2.1.0", Replaces: ""}, {Name: "a-v2.1.1", Replaces: "", Skips: []string{"a-v2.1.0"}}, {Name: "a-v2.3.1", Replaces: ""}, - {Name: "a-v2.3.2", Replaces: "a-v2.1.1", Skips: []string{"a-v2.3.1"}}, - }, - }, - }, - }, - { - name: "NoSkipPatch/No edges between minor channels", - unlinkedChannels: minorGeneratedUnlinkedChannels, - avoidSkipPatch: true, - generateMinorChannels: true, - generateMajorChannels: false, - out: []declcfg.Channel{ - { - Schema: "olm.channel", - Name: "stable-v0.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v0.1.0", Replaces: ""}, - {Name: "a-v0.1.1", Replaces: "a-v0.1.0"}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.1.0", Replaces: ""}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.2", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.2.1", Replaces: ""}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.3.1", Replaces: ""}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v2.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.1.0", Replaces: ""}, - {Name: "a-v2.1.1", Replaces: "a-v2.1.0"}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v2.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.3.1", Replaces: ""}, - {Name: "a-v2.3.2", Replaces: "a-v2.3.1"}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v3.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v3.1.0", Replaces: ""}, - {Name: "a-v3.1.1", Replaces: "a-v3.1.0"}, - }, - }, - }, - }, - { - name: "SkipPatch/No edges between minor channels", - unlinkedChannels: minorGeneratedUnlinkedChannels, - avoidSkipPatch: false, - generateMinorChannels: true, - generateMajorChannels: false, - out: []declcfg.Channel{ - { - Schema: "olm.channel", - Name: "stable-v0.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v0.1.0", Replaces: ""}, - {Name: "a-v0.1.1", Replaces: "", Skips: []string{"a-v0.1.0"}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.1.0", Replaces: "", Skips: []string{}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.2", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.2.1", Replaces: "", Skips: []string{}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v1.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.3.1", Replaces: "", Skips: []string{}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v2.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.1.0", Replaces: ""}, - {Name: "a-v2.1.1", Replaces: "", Skips: []string{"a-v2.1.0"}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v2.3", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v2.3.1", Replaces: ""}, - {Name: "a-v2.3.2", Replaces: "", Skips: []string{"a-v2.3.1"}}, - }, - }, - { - Schema: "olm.channel", - Name: "stable-v3.1", - Package: "a", - Entries: []declcfg.ChannelEntry{ - {Name: "a-v3.1.0", Replaces: ""}, - {Name: "a-v3.1.1", Replaces: "", Skips: []string{"a-v3.1.0"}}, + {Name: "a-v2.3.2", Replaces: "a-v2.1.1", Skips: []string{"a-v2.1.0", "a-v2.3.1"}}, }, }, }, @@ -363,20 +115,15 @@ func TestLinkChannels(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // map[string]*declcfg.Channel - unlinkedChannels := map[string]*declcfg.Channel{} - for c, e := range tt.unlinkedChannels { - unlinkedChannels[c] = e - } - sv := &semverTemplate{AvoidSkipPatch: tt.avoidSkipPatch, GenerateMajorChannels: tt.generateMajorChannels, GenerateMinorChannels: tt.generateMinorChannels} - require.ElementsMatch(t, tt.out, sv.linkChannels(unlinkedChannels, "a", &channelOperatorVersions, &channelNameToKind)) + sv := &semverTemplate{pkg: "a", GenerateMajorChannels: tt.generateMajorChannels, GenerateMinorChannels: tt.generateMinorChannels} + require.ElementsMatch(t, tt.out, sv.linkChannels(tt.unlinkedChannels, majorChannelEntries)) }) } } func TestGenerateChannels(t *testing.T) { - // type semverRenderedChannelVersions map[string]map[string]semver.Version // e.g. d["stable-v1"]["example-operator/v1.0.0"] = 1.0.0 - channelOperatorVersions := semverRenderedChannelVersions{ + // type bundleVersions map[string]map[string]semver.Version // e.g. d["stable"]["example-operator.v1.0.0"] = 1.0.0 + channelOperatorVersions := bundleVersions{ "stable": { "a-v0.1.0": semver.MustParse("0.1.0"), "a-v0.1.1": semver.MustParse("0.1.1"), @@ -399,14 +146,12 @@ func TestGenerateChannels(t *testing.T) { tests := []struct { name string - avoidSkipPatch bool generateMinorChannels bool generateMajorChannels bool out []declcfg.Channel }{ { - name: "SkipPatch/No edges between minor channels", - avoidSkipPatch: false, + name: "Edges between minor channels", generateMinorChannels: true, generateMajorChannels: false, out: []declcfg.Channel{ @@ -432,7 +177,7 @@ func TestGenerateChannels(t *testing.T) { Name: "stable-v1.2", Package: "a", Entries: []declcfg.ChannelEntry{ - {Name: "a-v1.2.1", Replaces: "", Skips: []string{}}, + {Name: "a-v1.2.1", Replaces: "a-v1.1.0", Skips: []string{}}, }, }, { @@ -442,7 +187,7 @@ func TestGenerateChannels(t *testing.T) { Entries: []declcfg.ChannelEntry{ {Name: "a-v1.3.1-alpha", Replaces: ""}, {Name: "a-v1.3.1-beta", Replaces: ""}, - {Name: "a-v1.3.1", Replaces: "", Skips: []string{"a-v1.3.1-alpha", "a-v1.3.1-beta"}}, + {Name: "a-v1.3.1", Replaces: "a-v1.2.1", Skips: []string{"a-v1.1.0", "a-v1.3.1-alpha", "a-v1.3.1-beta"}}, }, }, { @@ -452,7 +197,7 @@ func TestGenerateChannels(t *testing.T) { Entries: []declcfg.ChannelEntry{ {Name: "a-v1.4.1-beta1", Replaces: ""}, {Name: "a-v1.4.1-beta2", Replaces: ""}, - {Name: "a-v1.4.1", Replaces: "", Skips: []string{"a-v1.4.1-beta1", "a-v1.4.1-beta2"}}, + {Name: "a-v1.4.1", Replaces: "a-v1.3.1", Skips: []string{"a-v1.1.0", "a-v1.2.1", "a-v1.3.1-alpha", "a-v1.3.1-beta", "a-v1.4.1-beta1", "a-v1.4.1-beta2"}}, }, }, { @@ -470,7 +215,7 @@ func TestGenerateChannels(t *testing.T) { Package: "a", Entries: []declcfg.ChannelEntry{ {Name: "a-v2.3.1", Replaces: ""}, - {Name: "a-v2.3.2", Replaces: "", Skips: []string{"a-v2.3.1"}}, + {Name: "a-v2.3.2", Replaces: "a-v2.1.1", Skips: []string{"a-v2.1.0", "a-v2.3.1"}}, }, }, { @@ -488,7 +233,7 @@ func TestGenerateChannels(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sv := &semverTemplate{AvoidSkipPatch: tt.avoidSkipPatch, GenerateMajorChannels: tt.generateMajorChannels, GenerateMinorChannels: tt.generateMinorChannels, pkg: "a"} + sv := &semverTemplate{GenerateMajorChannels: tt.generateMajorChannels, GenerateMinorChannels: tt.generateMinorChannels, pkg: "a"} require.ElementsMatch(t, tt.out, sv.generateChannels(&channelOperatorVersions)) }) } @@ -498,13 +243,13 @@ func TestGetVersionsFromStandardChannel(t *testing.T) { tests := []struct { name string sv semverTemplate - outVersions semverRenderedChannelVersions + outVersions bundleVersions dc declcfg.DeclarativeConfig }{ { name: "sunny day case", sv: semverTemplate{ - Stable: stableBundles{ + Stable: semverTemplateChannelBundles{ []semverTemplateBundleEntry{ {Image: "repo/origin/a-v0.1.0"}, {Image: "repo/origin/a-v0.1.1"}, @@ -519,7 +264,7 @@ func TestGetVersionsFromStandardChannel(t *testing.T) { }, }, }, - outVersions: semverRenderedChannelVersions{ + outVersions: bundleVersions{ "candidate": map[string]semver.Version{}, "fast": map[string]semver.Version{}, "stable": { @@ -571,7 +316,7 @@ func TestGetVersionsFromStandardChannel(t *testing.T) { func TestBailOnVersionBuildMetadata(t *testing.T) { sv := semverTemplate{ - Stable: stableBundles{ + Stable: semverTemplateChannelBundles{ []semverTemplateBundleEntry{ {Image: "repo/origin/a-v0.1.0"}, {Image: "repo/origin/a-v0.1.1"}, diff --git a/alpha/template/semver/types.go b/alpha/template/semver/types.go new file mode 100644 index 000000000..de948f372 --- /dev/null +++ b/alpha/template/semver/types.go @@ -0,0 +1,96 @@ +package semver + +import ( + "fmt" + "io" + + "github.com/blang/semver/v4" + "github.com/operator-framework/operator-registry/pkg/image" +) + +// data passed into this module externally +type Template struct { + Data io.Reader + Registry image.Registry +} + +// IO structs -- BEGIN +type semverTemplateBundleEntry struct { + Image string `json:"image,omitempty"` +} + +type semverTemplateChannelBundles struct { + Bundles []semverTemplateBundleEntry `json:"bundles,omitempty"` +} + +type semverTemplate struct { + Schema string `json:"schema"` + GenerateMajorChannels bool `json:"generateMajorChannels,omitempty"` + GenerateMinorChannels bool `json:"generateMinorChannels,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 + +const schema string = "olm.semver" + +// 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 minorStreamType streamType = "minor" +const majorStreamType streamType = "major" + +var streamTypePriorities = map[streamType]int{minorStreamType: 0, majorStreamType: 1} + +// 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 + version semver.Version + name string +} + +func (h *highwaterChannel) gt(ih *highwaterChannel) bool { + return (channelPriorities[h.archetype] > channelPriorities[ih.archetype]) || (h.version.GT(ih.version)) +} + +type entryTuple struct { + arch channelArchetype + kind streamType + name string + parent string + index int + version semver.Version +} + +func (t entryTuple) String() string { + return fmt.Sprintf("{ arch: %q, kind: %q, name: %q, parent: %q, index: %d, version: %v }", t.arch, t.kind, t.name, t.parent, t.index, t.version.String()) +}