Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions alpha/declcfg/declcfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"

"github.com/blang/semver/v4"
"golang.org/x/text/cases"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
Expand Down Expand Up @@ -206,3 +207,71 @@ func (destination *DeclarativeConfig) Merge(src *DeclarativeConfig) {
destination.Others = append(destination.Others, src.Others...)
destination.Deprecations = append(destination.Deprecations, src.Deprecations...)
}

type CompositeVersion struct {
Version semver.Version
Release semver.Version
}

func (cv *CompositeVersion) Compare(other *CompositeVersion) int {
if cv.Version.NE(other.Version) {
return cv.Version.Compare(other.Version)
}
hasrelease := len(cv.Release.Pre) > 0
otherhasrelease := len(other.Release.Pre) > 0
if hasrelease && !otherhasrelease {
return 1
}
if !hasrelease && otherhasrelease {
return -1
}
return cv.Release.Compare(other.Release)
}

// order by version, then
// release, if present
func (b *Bundle) Compare(other *Bundle) int {
if b.Name == other.Name {
return 0
}
acv, err := b.CompositeVersion()
if err != nil {
return 0
}
otherCv, err := other.CompositeVersion()
if err != nil {
return 0
}
return acv.Compare(otherCv)
}

func (b *Bundle) CompositeVersion() (*CompositeVersion, error) {
props, err := property.Parse(b.Properties)
if err != nil {
return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
}
if len(props.Packages) != 1 {
return nil, fmt.Errorf("bundle %q must have exactly 1 \"olm.package\" property, found %v", b.Name, len(props.Packages))
}
v, err := semver.Parse(props.Packages[0].Version)
if err != nil {
return nil, fmt.Errorf("bundle %q has invalid version %q: %v", b.Name, props.Packages[0].Version, err)
}

var r semver.Version
if props.Packages[0].Release != "" {
r, err = semver.Parse(fmt.Sprintf("0.0.0-%s", props.Packages[0].Release))
if err != nil {
return nil, fmt.Errorf("error parsing bundle %q release version %q: %v", b.Name, props.Packages[0].Release, err)
}
// only need to check for build metadata since we are using explicit zero major, minor, and patch versions above
if len(r.Build) != 0 {
return nil, fmt.Errorf("bundle %q release version %q cannot contain build metadata", b.Name, props.Packages[0].Release)
}
}

return &CompositeVersion{
Version: v,
Release: r,
}, nil
}
14 changes: 14 additions & 0 deletions alpha/declcfg/declcfg_to_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
return nil, fmt.Errorf("error parsing bundle %q version %q: %v", b.Name, rawVersion, err)
}

// Parse release version from the package property.
var relver semver.Version
if props.Packages[0].Release != "" {
relver, err = semver.Parse(fmt.Sprintf("0.0.0-%s", props.Packages[0].Release))
if err != nil {
return nil, fmt.Errorf("error parsing bundle %q release version %q: %v", b.Name, props.Packages[0].Release, err)
}
// only need to check for build metadata since we are using explicit zero major, minor, and patch versions above
if len(relver.Build) != 0 {
return nil, fmt.Errorf("bundle %q release version %q cannot contain build metadata", b.Name, props.Packages[0].Release)
}
}

channelDefinedEntries[b.Package] = channelDefinedEntries[b.Package].Delete(b.Name)
found := false
for _, mch := range mpkg.Channels {
Expand All @@ -147,6 +160,7 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
mb.Objects = b.Objects
mb.PropertiesP = props
mb.Version = ver
mb.Release = relver
}
}
if !found {
Expand Down
64 changes: 64 additions & 0 deletions alpha/declcfg/declcfg_to_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,70 @@ func TestConvertToModel(t *testing.T) {
},
},
},
{
name: "Error/InvalidReleaseVersion",
assertion: hasError(`error parsing bundle "foo.v0.1.0" release version "!!!": Invalid character(s) found in prerelease "!!!"`),
cfg: DeclarativeConfig{
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})},
Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
b.Properties = []property.Property{
property.MustBuildPackageRelease("foo", "0.1.0", "!!!"),
}
})},
},
},
{
name: "Error/InvalidBundleNormalizedName",
assertion: hasError(`invalid index:
└── invalid package "foo":
└── invalid channel "alpha":
└── invalid bundle "foo.v0.1.0-alpha.1.0.0":
└── name "foo.v0.1.0-alpha.1.0.0" does not match normalized name "foo-v0.1.0-alpha.1.0.0"`),
cfg: DeclarativeConfig{
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0-alpha.1.0.0"})},
Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
b.Properties = []property.Property{
property.MustBuildPackageRelease("foo", "0.1.0", "alpha.1.0.0"),
}
b.Name = "foo.v0.1.0-alpha.1.0.0"
})},
},
},
{
name: "Success/ValidBundleReleaseVersion",
assertion: require.NoError,
cfg: DeclarativeConfig{
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo-v0.1.0-alpha.1.0.0"})},
Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
b.Properties = []property.Property{
property.MustBuildPackageRelease("foo", "0.1.0", "alpha.1.0.0"),
}
b.Name = "foo-v0.1.0-alpha.1.0.0"
})},
},
},
{
name: "Error/BundleReleaseWithBuildMetadata",
assertion: hasError(`invalid index:
└── invalid package "foo":
└── invalid channel "alpha":
└── invalid bundle "foo.v0.1.0+alpha.1.0.0-0.0.1":
├── name "foo.v0.1.0+alpha.1.0.0-0.0.1" does not match normalized name "foo-v0.1.0+alpha.1.0.0-0.0.1"
└── cannot use build metadata in version with a release version`),
cfg: DeclarativeConfig{
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0+alpha.1.0.0-0.0.1"})},
Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
b.Properties = []property.Property{
property.MustBuildPackageRelease("foo", "0.1.0+alpha.1.0.0", "0.0.1"),
}
b.Name = "foo.v0.1.0+alpha.1.0.0-0.0.1"
})},
},
},
}

for _, s := range specs {
Expand Down
95 changes: 73 additions & 22 deletions alpha/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package model
import (
"errors"
"fmt"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -52,75 +53,75 @@ type Package struct {
Deprecation *Deprecation
}

func (m *Package) Validate() error {
result := newValidationError(fmt.Sprintf("invalid package %q", m.Name))
func (p *Package) Validate() error {
result := newValidationError(fmt.Sprintf("invalid package %q", p.Name))

if m.Name == "" {
if p.Name == "" {
result.subErrors = append(result.subErrors, errors.New("package name must not be empty"))
}

if err := m.Icon.Validate(); err != nil {
if err := p.Icon.Validate(); err != nil {
result.subErrors = append(result.subErrors, err)
}

if m.DefaultChannel == nil {
if p.DefaultChannel == nil {
result.subErrors = append(result.subErrors, fmt.Errorf("default channel must be set"))
}

if len(m.Channels) == 0 {
if len(p.Channels) == 0 {
result.subErrors = append(result.subErrors, fmt.Errorf("package must contain at least one channel"))
}

foundDefault := false
for name, ch := range m.Channels {
for name, ch := range p.Channels {
if name != ch.Name {
result.subErrors = append(result.subErrors, fmt.Errorf("channel key %q does not match channel name %q", name, ch.Name))
}
if err := ch.Validate(); err != nil {
result.subErrors = append(result.subErrors, err)
}
if ch == m.DefaultChannel {
if ch == p.DefaultChannel {
foundDefault = true
}
if ch.Package != m {
if ch.Package != p {
result.subErrors = append(result.subErrors, fmt.Errorf("channel %q not correctly linked to parent package", ch.Name))
}
}

if err := m.validateUniqueBundleVersions(); err != nil {
if err := p.validateUniqueBundleVersions(); err != nil {
result.subErrors = append(result.subErrors, err)
}

if m.DefaultChannel != nil && !foundDefault {
result.subErrors = append(result.subErrors, fmt.Errorf("default channel %q not found in channels list", m.DefaultChannel.Name))
if p.DefaultChannel != nil && !foundDefault {
result.subErrors = append(result.subErrors, fmt.Errorf("default channel %q not found in channels list", p.DefaultChannel.Name))
}

if err := m.Deprecation.Validate(); err != nil {
if err := p.Deprecation.Validate(); err != nil {
result.subErrors = append(result.subErrors, fmt.Errorf("invalid deprecation: %v", err))
}

return result.orNil()
}

func (m *Package) validateUniqueBundleVersions() error {
versionsMap := map[string]semver.Version{}
func (p *Package) validateUniqueBundleVersions() error {
versionsMap := map[string]string{}
bundlesWithVersion := map[string]sets.Set[string]{}
for _, ch := range m.Channels {
for _, ch := range p.Channels {
for _, b := range ch.Bundles {
versionsMap[b.Version.String()] = b.Version
if bundlesWithVersion[b.Version.String()] == nil {
bundlesWithVersion[b.Version.String()] = sets.New[string]()
versionsMap[b.VersionString()] = b.VersionString()
if bundlesWithVersion[b.VersionString()] == nil {
bundlesWithVersion[b.VersionString()] = sets.New[string]()
}
bundlesWithVersion[b.Version.String()].Insert(b.Name)
bundlesWithVersion[b.VersionString()].Insert(b.Name)
}
}

versionsSlice := maps.Values(versionsMap)
semver.Sort(versionsSlice)
slices.Sort(versionsSlice)

var errs []error
for _, v := range versionsSlice {
bundles := sets.List(bundlesWithVersion[v.String()])
bundles := sets.List(bundlesWithVersion[v])
if len(bundles) > 1 {
errs = append(errs, fmt.Errorf("{%s: [%s]}", v, strings.Join(bundles, ", ")))
}
Expand Down Expand Up @@ -331,6 +332,49 @@ type Bundle struct {
// These fields are used to compare bundles in a diff.
PropertiesP *property.Properties
Version semver.Version
Release semver.Version
}

func (b *Bundle) VersionString() string {
if len(b.Release.Pre) > 0 {
pres := []string{}
for _, pre := range b.Release.Pre {
pres = append(pres, pre.String())
}
relString := strings.Join(pres, ".")
return strings.Join([]string{b.Version.String(), relString}, "-")
}
return b.Version.String()
}

func (b *Bundle) normalizeName() string {
// if the bundle has release versioning, then the name must include this in standard form:
// <package-name>-v<version>-<release version>
// if no release versioning exists, then just return the bundle name
if len(b.Release.Pre) > 0 {
return strings.Join([]string{b.Package.Name, "v" + b.VersionString()}, "-")
}
return b.Name
}

// order by version, then
// release, if present
func (b *Bundle) Compare(other *Bundle) int {
if b.Name == other.Name {
return 0
}
if b.Version.NE(other.Version) {
return b.Version.Compare(other.Version)
}
bhasrelease := len(b.Release.Pre) > 0
otherhasrelease := len(other.Release.Pre) > 0
if bhasrelease && !otherhasrelease {
return 1
}
if !bhasrelease && otherhasrelease {
return -1
}
return b.Release.Compare(other.Release)
}

func (b *Bundle) Validate() error {
Expand All @@ -339,6 +383,9 @@ func (b *Bundle) Validate() error {
if b.Name == "" {
result.subErrors = append(result.subErrors, errors.New("name must be set"))
}
if b.Name != b.normalizeName() {
result.subErrors = append(result.subErrors, fmt.Errorf("name %q does not match normalized name %q", b.Name, b.normalizeName()))
}
if b.Channel == nil {
result.subErrors = append(result.subErrors, errors.New("channel must be set"))
}
Expand Down Expand Up @@ -379,6 +426,10 @@ func (b *Bundle) Validate() error {
result.subErrors = append(result.subErrors, fmt.Errorf("invalid deprecation: %v", err))
}

if len(b.Version.Build) > 0 && len(b.Release.Pre) > 0 {
result.subErrors = append(result.subErrors, fmt.Errorf("cannot use build metadata in version with a release version"))
}

return result.orNil()
}

Expand Down
Loading