From aad3ea090ded80161dc72e4008cb518691ae061f Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 4 Dec 2025 11:41:21 +0100 Subject: [PATCH 1/3] Reorder functions from top-to-bottom reading --- cmd/generate-catalog/generate.go | 208 +++++++++++++++---------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/cmd/generate-catalog/generate.go b/cmd/generate-catalog/generate.go index 65769aef..b6618407 100644 --- a/cmd/generate-catalog/generate.go +++ b/cmd/generate-catalog/generate.go @@ -130,6 +130,88 @@ func readInputFile(filename string) (Configuration, error) { }, nil } +// getAllVersions extracts all operator versions from the input images. +func getAllVersions(images []BundleImage) []*semver.Version { + versions := make([]*semver.Version, 0, len(images)) + for _, img := range images { + versions = append(versions, img.Version) + } + return versions +} + +// validateVersionsAreSorted checks that the operator versions are sorted in ascending order and that there are no duplicates. +// The sorted order is important for the correct functioning of the rest of the program. +func validateVersionsAreSorted(versions []*semver.Version) error { + for i := 0; i < len(versions)-1; i++ { + currentVersion := versions[i] + nextVersion := versions[i+1] + if currentVersion.GreaterThanEqual(nextVersion) { + return fmt.Errorf("versions are not sorted in ascending order: %s is not less than %s", currentVersion, nextVersion) + } + } + return nil +} + +func hasGapInVersions(versions []*semver.Version) error { + for i := 0; i < len(versions)-1; i++ { + var expectedNextVersion *semver.Version + currentVersion := versions[i] + nextVersion := versions[i+1] + + if currentVersion.Major() != nextVersion.Major() { + expectedNextVersion = semver.New(currentVersion.Major()+1, 0, 0, "", "") + } + if currentVersion.Major() == nextVersion.Major() && currentVersion.Minor() != nextVersion.Minor() { + expectedNextVersion = semver.New(currentVersion.Major(), currentVersion.Minor()+1, 0, "", "") + } + if currentVersion.Major() == nextVersion.Major() && currentVersion.Minor() == nextVersion.Minor() { + expectedNextVersion = semver.New(currentVersion.Major(), currentVersion.Minor(), currentVersion.Patch()+1, "", "") + } + + if expectedNextVersion.Major() != nextVersion.Major() || expectedNextVersion.Minor() != nextVersion.Minor() || expectedNextVersion.Patch() != nextVersion.Patch() { + return fmt.Errorf("unexpected version sequence [%s, %s]: %s should be followed by %s", currentVersion, nextVersion, currentVersion, expectedNextVersion) + } + } + + return nil +} + +// validateImageReferences checks that all images in the input bundle have valid container image references with a digest. +func validateImageReferences(images []BundleImage) error { + for _, img := range images { + if err := validateImageReference(img.Image); err != nil { + return err + } + } + return nil +} + +// validateImageReference checks that the given image reference string is a valid container image reference and includes a registry, repository and digest. +// Also check that tag is not present. See tag related issue: https://redhat-internal.slack.com/archives/C031USXS2FJ/p1755792504667849?thread_ts=1755622785.895239&cid=C031USXS2FJ +func validateImageReference(imageRef string) error { + ref, err := reference.Parse(imageRef) + if err != nil { + return fmt.Errorf("cannot parse string as container image reference %s: %w", imageRef, err) + } + + canonical, ok := ref.(reference.Canonical) + if !ok || canonical.Digest() == "" { + return fmt.Errorf("image reference %s does not include a digest", imageRef) + } + if canonical.Digest().Algorithm() != digest.SHA256 { + return fmt.Errorf("image reference %s digest algorithm is not sha256", imageRef) + } + + if reference.Domain(canonical) == "" { + return fmt.Errorf("image reference %s needs the registry to be explicitly defined", imageRef) + } + if tagged, ok := ref.(reference.Tagged); ok && tagged.Tag() != "" { + return fmt.Errorf("image reference %s should not contain a tag", imageRef) + } + + return nil +} + // generatePackageWithIcon creates a new "olm.package" object with an operator icon. func generatePackageWithIcon() (Package, error) { data, err := os.ReadFile(iconFile) @@ -179,6 +261,17 @@ func generateEmptyChannels(versions []*semver.Version) []Channel { return channels } +// assignChannels assigns channels to the appropriate channel lineages based on their Y-Stream versions. +func assignChannels(lineages []ChannelLineage, channels []Channel) { + for _, ch := range channels { + for i := range lineages { + if versionBelongsToChannelLineage(ch.yStreamVersion, lineages[i]) { + lineages[i].YStreamChannels = append(lineages[i].YStreamChannels, ch) + } + } + } +} + // generateChannelEntries creates channel entries for each version, setting the appropriate `replaces` and `skipRange` fields. func generateChannelEntries(versions []*semver.Version) []ChannelEntry { channelEntries := make([]ChannelEntry, 0) @@ -202,17 +295,6 @@ func generateChannelEntries(versions []*semver.Version) []ChannelEntry { return channelEntries } -// assignChannels assigns channels to the appropriate channel lineages based on their Y-Stream versions. -func assignChannels(lineages []ChannelLineage, channels []Channel) { - for _, ch := range channels { - for i := range lineages { - if versionBelongsToChannelLineage(ch.yStreamVersion, lineages[i]) { - lineages[i].YStreamChannels = append(lineages[i].YStreamChannels, ch) - } - } - } -} - // assignChannelEntries assigns channel entries to the appropriate channels within each lineage. func assignChannelEntries(lineages []ChannelLineage, entries []ChannelEntry) { for _, entry := range entries { @@ -229,6 +311,17 @@ func assignChannelEntries(lineages []ChannelLineage, entries []ChannelEntry) { } } +func versionBelongsToChannelLineage(version *semver.Version, lineage ChannelLineage) bool { + return lineage.FromVersion.LessThanEqual(version) && lineage.UntilVersion.GreaterThan(version) +} + +func channelShouldHaveEntry(channel Channel, entry ChannelEntry) bool { + lesserX := entry.version.Major() < channel.yStreamVersion.Major() + sameXVersion := entry.version.Major() == channel.yStreamVersion.Major() + belongsToYStream := entry.version.Minor() <= channel.yStreamVersion.Minor() + return lesserX || (sameXVersion && belongsToYStream) +} + // flattenChannels flattens channels from multiple ChannelLineages into a single slice. func flattenChannels(lineages []ChannelLineage) []Channel { var channels []Channel @@ -248,13 +341,6 @@ func clearReplacesForStartingEntries(channels []Channel) { } } -func channelShouldHaveEntry(channel Channel, entry ChannelEntry) bool { - lesserX := entry.version.Major() < channel.yStreamVersion.Major() - sameXVersion := entry.version.Major() == channel.yStreamVersion.Major() - belongsToYStream := entry.version.Minor() <= channel.yStreamVersion.Minor() - return lesserX || (sameXVersion && belongsToYStream) -} - // generateDeprecations creates an object with a list of deprecations based on the provided versions. func generateDeprecations(versions []*semver.Version, channels []Channel, oldestSupportedVersion *semver.Version) Deprecations { var deprecations []DeprecationEntry @@ -309,89 +395,3 @@ func writeToFile(filename string, ct CatalogTemplate) error { return nil } - -// getAllVersions extracts all operator versions from the input images. -func getAllVersions(images []BundleImage) []*semver.Version { - versions := make([]*semver.Version, 0, len(images)) - for _, img := range images { - versions = append(versions, img.Version) - } - return versions -} - -// validateVersionsAreSorted checks that the operator versions are sorted in ascending order and that there are no duplicates. -// The sorted order is important for the correct functioning of the rest of the program. -func validateVersionsAreSorted(versions []*semver.Version) error { - for i := 0; i < len(versions)-1; i++ { - currentVersion := versions[i] - nextVersion := versions[i+1] - if currentVersion.GreaterThanEqual(nextVersion) { - return fmt.Errorf("versions are not sorted in ascending order: %s is not less than %s", currentVersion, nextVersion) - } - } - return nil -} - -func hasGapInVersions(versions []*semver.Version) error { - for i := 0; i < len(versions)-1; i++ { - var expectedNextVersion *semver.Version - currentVersion := versions[i] - nextVersion := versions[i+1] - - if currentVersion.Major() != nextVersion.Major() { - expectedNextVersion = semver.New(currentVersion.Major()+1, 0, 0, "", "") - } - if currentVersion.Major() == nextVersion.Major() && currentVersion.Minor() != nextVersion.Minor() { - expectedNextVersion = semver.New(currentVersion.Major(), currentVersion.Minor()+1, 0, "", "") - } - if currentVersion.Major() == nextVersion.Major() && currentVersion.Minor() == nextVersion.Minor() { - expectedNextVersion = semver.New(currentVersion.Major(), currentVersion.Minor(), currentVersion.Patch()+1, "", "") - } - - if expectedNextVersion.Major() != nextVersion.Major() || expectedNextVersion.Minor() != nextVersion.Minor() || expectedNextVersion.Patch() != nextVersion.Patch() { - return fmt.Errorf("unexpected version sequence [%s, %s]: %s should be followed by %s", currentVersion, nextVersion, currentVersion, expectedNextVersion) - } - } - - return nil -} - -// validateImageReferences checks that all images in the input bundle have valid container image references with a digest. -func validateImageReferences(images []BundleImage) error { - for _, img := range images { - if err := validateImageReference(img.Image); err != nil { - return err - } - } - return nil -} - -// validateImageReference checks that the given image reference string is a valid container image reference and includes a registry, repository and digest. -// Also check that tag is not present. See tag related issue: https://redhat-internal.slack.com/archives/C031USXS2FJ/p1755792504667849?thread_ts=1755622785.895239&cid=C031USXS2FJ -func validateImageReference(imageRef string) error { - ref, err := reference.Parse(imageRef) - if err != nil { - return fmt.Errorf("cannot parse string as container image reference %s: %w", imageRef, err) - } - - canonical, ok := ref.(reference.Canonical) - if !ok || canonical.Digest() == "" { - return fmt.Errorf("image reference %s does not include a digest", imageRef) - } - if canonical.Digest().Algorithm() != digest.SHA256 { - return fmt.Errorf("image reference %s digest algorithm is not sha256", imageRef) - } - - if reference.Domain(canonical) == "" { - return fmt.Errorf("image reference %s needs the registry to be explicitly defined", imageRef) - } - if tagged, ok := ref.(reference.Tagged); ok && tagged.Tag() != "" { - return fmt.Errorf("image reference %s should not contain a tag", imageRef) - } - - return nil -} - -func versionBelongsToChannelLineage(version *semver.Version, lineage ChannelLineage) bool { - return lineage.FromVersion.LessThanEqual(version) && lineage.UntilVersion.GreaterThan(version) -} From 4546e10ffe58b31d1abedd46970375b1ea463d40 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 4 Dec 2025 11:58:43 +0100 Subject: [PATCH 2/3] Reorder types.go and update comments --- cmd/generate-catalog/types.go | 78 +++++++++++++++++------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/cmd/generate-catalog/types.go b/cmd/generate-catalog/types.go index 08439858..b11a77f7 100644 --- a/cmd/generate-catalog/types.go +++ b/cmd/generate-catalog/types.go @@ -15,7 +15,7 @@ const ( olmBundleSchema = "olm.bundle" ) -// Describes format of the input file for catalog template generation. +// Input describes format of the input file for catalog template generation. // It contains: // - OldestSupportedVersion - the oldest supported version of the operator. All versions < OldestSupportedVersion are marked as deprecated. // - Images - a list of bundle images with their versions. @@ -29,7 +29,7 @@ type InputBundleImage struct { Version string `yaml:"version"` } -// Describes domain logic configuration for the catalog template generation. +// Configuration describes domain logic configuration for the catalog template generation. type Configuration struct { OldestSupportedVersion *semver.Version Images []BundleImage @@ -41,7 +41,8 @@ type BundleImage struct { Version *semver.Version } -// Describes catalog template structure which is used to generate the catalog YAML file. +// CatalogTemplate describes catalog template structure which is used to generate the catalog YAML file. +// It has to contain entries with schema equal to: "olm.package", "olm.channel", "olm.deprecations" or "olm.bundle". // See OLM catalog template documentation for more details: https://olm.operatorframework.io/docs/reference/catalog-templates/ type CatalogTemplate struct { Schema string `yaml:"schema"` @@ -69,13 +70,6 @@ type Icon struct { MediaType string `yaml:"mediatype"` } -type ChannelLineage struct { - MainChannel Channel // The main channel (e.g., "stable" or "latest") which contains all versions associated with this channel lineage (e.g., stable: 4.0.x, 4.1.x, etc.) - YStreamChannels []Channel - FromVersion *semver.Version // Inclusive lower bound of versions associated with this channel lineage. - UntilVersion *semver.Version // Exclusive upper bound of versions associated with this channel lineage. -} - type Channel struct { Schema string `yaml:"schema"` Name string `yaml:"name"` @@ -112,8 +106,6 @@ type BundleEntry struct { Image string `yaml:"image"` } -// Create base catalog template block. -// It has to contain objects with schema equal to: "olm.package", "olm.channel", "olm.deprecations" or "olm.bundle". func newCatalogTemplate() CatalogTemplate { return CatalogTemplate{ Schema: olmTemplateSchema, @@ -138,7 +130,7 @@ func (c *CatalogTemplate) addPackage(pkg Package) { c.Entries = append(c.Entries, CatalogEntry(pkg)) } -// addChannels adds a list of "olm.channel" objects to the base catalog. +// addChannels adds a slice of "olm.channel" objects to the base catalog. func (c *CatalogTemplate) addChannels(channels []Channel) { for _, channel := range channels { c.Entries = append(c.Entries, CatalogEntry(channel)) @@ -157,22 +149,8 @@ func (c *CatalogTemplate) addBundles(bundles []BundleEntry) { } } -// Create a new ChannelLineage structure which groups channels together (e.g., all "stable" channels). -func newChannelLineage(name string, from, until *semver.Version) ChannelLineage { - mainChannel := Channel{ - Schema: olmChannelSchema, - Name: name, - Package: rhacsOperator, - } - return ChannelLineage{ - MainChannel: mainChannel, - FromVersion: from, - UntilVersion: until, - } -} - -// Create a new "olm.channel" object. -// it will be represented in YAML like this: +// newChannel creates a new "olm.channel" object. +// It will be represented in YAML like this: // | - schema: olm.channel // | name: rhacs-3.64 // | package: rhacs-operator @@ -187,9 +165,13 @@ func newChannel(version *semver.Version) Channel { } } +func makeYStreamVersion(v *semver.Version) *semver.Version { + return semver.New(v.Major(), v.Minor(), 0, "", "") +} + // newChannelEntry creates an object to be added to Channel entries list. // Channel entries effectively form the upgrade graph within the channel telling OLM from which versions it's allowed to upgrade to a particular one. -// it will be represented in YAML like this: +// It will be represented in YAML like this: // | - name: rhacs-operator.v // | replaces: rhacs-operator.v // | skipRange: '>= < ' @@ -212,7 +194,7 @@ func (e *ChannelEntry) setSkipRange(skipRangeFrom, skipRangeTo *semver.Version) e.SkipRange = fmt.Sprintf(">= %s < %s", skipRangeFrom, skipRangeTo) } -// Create a new "olm.deprecations" object which should be added to the catalog base. +// newDeprecations creates a new "olm.deprecations" object which should be added to the catalog base. // It will be represented in YAML like this: // | - schema: olm.deprecations // | package: rhacs-operator @@ -226,8 +208,8 @@ func newDeprecations(entries []DeprecationEntry) Deprecations { } } -// Create a new channel DeprecationEntry reference object which should be added to Deprecation reference list. -// it will be represented in YAML like this: +// newChannelDeprecationEntry creates a new channel DeprecationEntry reference object which should be added to Deprecation reference list. +// It will be represented in YAML like this: // | - reference: // | schema: olm.channel // | name: @@ -243,8 +225,8 @@ func newChannelDeprecationEntry(name string, message string) DeprecationEntry { } } -// Create a new bundle DeprecationEntry reference object which should be added to Deprecation reference list. -// it will be represented in YAML like this: +// newBundleDeprecationEntry creates a new bundle DeprecationEntry reference object which should be added to Deprecation reference list. +// It will be represented in YAML like this: // | - reference: // | schema: olm.bundle // | name: rhacs-operator.v @@ -260,8 +242,8 @@ func newBundleDeprecationEntry(version *semver.Version, message string) Deprecat } } -// Create a new "olm.bundle" object which should be added to the catalog base. -// it will be represented in YAML like this: +// newBundleEntry creates a new "olm.bundle" object which should be added to the catalog base. +// It will be represented in YAML like this: // | - image: // | schema: olm.bundle func newBundleEntry(image string) BundleEntry { @@ -275,6 +257,24 @@ func generateBundleName(version *semver.Version) string { return fmt.Sprintf("%s.v%s", rhacsOperator, version) } -func makeYStreamVersion(v *semver.Version) *semver.Version { - return semver.New(v.Major(), v.Minor(), 0, "", "") +// ChannelLineage is a helper struct for the generation time. +// It groups channels together. There's a main one, it's the most complete including all versions in this lineage, and there are Y-Stream channels that are subsets of the main one. +type ChannelLineage struct { + MainChannel Channel // The main channel (e.g., "stable" or "latest") which contains all versions associated with this channel lineage (e.g., stable: 4.0.x, 4.1.x, etc.) + YStreamChannels []Channel + FromVersion *semver.Version // Inclusive lower bound of versions associated with this channel lineage. + UntilVersion *semver.Version // Exclusive upper bound of versions associated with this channel lineage. +} + +func newChannelLineage(name string, from, until *semver.Version) ChannelLineage { + mainChannel := Channel{ + Schema: olmChannelSchema, + Name: name, + Package: rhacsOperator, + } + return ChannelLineage{ + MainChannel: mainChannel, + FromVersion: from, + UntilVersion: until, + } } From 95ded281ac40bd913521081609d0a7e31ea4cae1 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 4 Dec 2025 11:59:51 +0100 Subject: [PATCH 3/3] Unexport `ChannelLineage` because we don't serialize it or anything like that. --- cmd/generate-catalog/generate.go | 10 +++++----- cmd/generate-catalog/generate_test.go | 6 +++--- cmd/generate-catalog/types.go | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/generate-catalog/generate.go b/cmd/generate-catalog/generate.go index b6618407..e0a91fec 100644 --- a/cmd/generate-catalog/generate.go +++ b/cmd/generate-catalog/generate.go @@ -228,7 +228,7 @@ func generatePackageWithIcon() (Package, error) { func generateChannels(versions []*semver.Version) []Channel { latestLineage := newChannelLineage(latestChannelName, latestChannelFromVersion, latestChannelUntilVersion) stableLineage := newChannelLineage(stableChannelName, stableChannelFromVersion, stableChannelUntilVersion) - lineages := []ChannelLineage{latestLineage, stableLineage} + lineages := []channelLineage{latestLineage, stableLineage} emptyChannels := generateEmptyChannels(versions) assignChannels(lineages, emptyChannels) @@ -262,7 +262,7 @@ func generateEmptyChannels(versions []*semver.Version) []Channel { } // assignChannels assigns channels to the appropriate channel lineages based on their Y-Stream versions. -func assignChannels(lineages []ChannelLineage, channels []Channel) { +func assignChannels(lineages []channelLineage, channels []Channel) { for _, ch := range channels { for i := range lineages { if versionBelongsToChannelLineage(ch.yStreamVersion, lineages[i]) { @@ -296,7 +296,7 @@ func generateChannelEntries(versions []*semver.Version) []ChannelEntry { } // assignChannelEntries assigns channel entries to the appropriate channels within each lineage. -func assignChannelEntries(lineages []ChannelLineage, entries []ChannelEntry) { +func assignChannelEntries(lineages []channelLineage, entries []ChannelEntry) { for _, entry := range entries { for i := range lineages { if versionBelongsToChannelLineage(entry.version, lineages[i]) { @@ -311,7 +311,7 @@ func assignChannelEntries(lineages []ChannelLineage, entries []ChannelEntry) { } } -func versionBelongsToChannelLineage(version *semver.Version, lineage ChannelLineage) bool { +func versionBelongsToChannelLineage(version *semver.Version, lineage channelLineage) bool { return lineage.FromVersion.LessThanEqual(version) && lineage.UntilVersion.GreaterThan(version) } @@ -323,7 +323,7 @@ func channelShouldHaveEntry(channel Channel, entry ChannelEntry) bool { } // flattenChannels flattens channels from multiple ChannelLineages into a single slice. -func flattenChannels(lineages []ChannelLineage) []Channel { +func flattenChannels(lineages []channelLineage) []Channel { var channels []Channel for _, lineage := range lineages { channels = append(channels, lineage.YStreamChannels...) diff --git a/cmd/generate-catalog/generate_test.go b/cmd/generate-catalog/generate_test.go index d35362cb..4f2c104e 100644 --- a/cmd/generate-catalog/generate_test.go +++ b/cmd/generate-catalog/generate_test.go @@ -443,7 +443,7 @@ func TestGenerateBundles(t *testing.T) { func TestAssignChannels(t *testing.T) { latestLineage := newChannelLineage("latest", semver.MustParse("3.62.0"), semver.MustParse("4.0.0")) stableLineage := newChannelLineage("stable", semver.MustParse("4.0.0"), semver.MustParse("9999.0.0")) - lineages := []ChannelLineage{latestLineage, stableLineage} + lineages := []channelLineage{latestLineage, stableLineage} channels := []Channel{ {Name: "rhacs-3.62", yStreamVersion: semver.MustParse("3.62.0")}, @@ -487,7 +487,7 @@ func TestAssignChannelEntries(t *testing.T) { {Name: "rhacs-operator.v5.0.1", Replaces: "rhacs-operator.v5.0.0", version: semver.MustParse("5.0.1")}, } - lineages := []ChannelLineage{latestLineage, stableLineage} + lineages := []channelLineage{latestLineage, stableLineage} assignChannelEntries(lineages, entries) latestLineage = lineages[0] @@ -564,7 +564,7 @@ func TestFlattenChannels(t *testing.T) { {Name: "rhacs-4.1"}, } - lineages := []ChannelLineage{latestLineage, stableLineage} + lineages := []channelLineage{latestLineage, stableLineage} channels := flattenChannels(lineages) // Should have 5 channels total: rhacs-3.62, latest, rhacs-4.0, rhacs-4.1, stable diff --git a/cmd/generate-catalog/types.go b/cmd/generate-catalog/types.go index b11a77f7..81048cf6 100644 --- a/cmd/generate-catalog/types.go +++ b/cmd/generate-catalog/types.go @@ -257,22 +257,22 @@ func generateBundleName(version *semver.Version) string { return fmt.Sprintf("%s.v%s", rhacsOperator, version) } -// ChannelLineage is a helper struct for the generation time. +// channelLineage is a helper struct for the generation time. // It groups channels together. There's a main one, it's the most complete including all versions in this lineage, and there are Y-Stream channels that are subsets of the main one. -type ChannelLineage struct { +type channelLineage struct { MainChannel Channel // The main channel (e.g., "stable" or "latest") which contains all versions associated with this channel lineage (e.g., stable: 4.0.x, 4.1.x, etc.) YStreamChannels []Channel FromVersion *semver.Version // Inclusive lower bound of versions associated with this channel lineage. UntilVersion *semver.Version // Exclusive upper bound of versions associated with this channel lineage. } -func newChannelLineage(name string, from, until *semver.Version) ChannelLineage { +func newChannelLineage(name string, from, until *semver.Version) channelLineage { mainChannel := Channel{ Schema: olmChannelSchema, Name: name, Package: rhacsOperator, } - return ChannelLineage{ + return channelLineage{ MainChannel: mainChannel, FromVersion: from, UntilVersion: until,