Skip to content

wip: olm.package.v2, olm.bundle.v2, olm.package.icon #1520

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
348 changes: 348 additions & 0 deletions alpha/action/migrations/001_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
package migrations

import (
"encoding/json"
"fmt"
"strings"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"

"github.com/operator-framework/api/pkg/operators/v1alpha1"

"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
)

func v2(cfg *declcfg.DeclarativeConfig) error {
channelHeads, err := getChannelHeads(cfg)
if err != nil {
return fmt.Errorf("could not get channel heads: %w", err)
}

for _, pkgV1 := range cfg.Packages {
cfg.PackageV2s = append(cfg.PackageV2s, packageToPackageV2(pkgV1))
if pkgIconV2 := packageToPackageIconV2(pkgV1); pkgIconV2 != nil {
cfg.PackageV2Icons = append(cfg.PackageV2Icons, *pkgIconV2)
}
pkgMetaV2, err := packageToPackageMetadataV2(pkgV1, channelHeads[channelKey{Package: pkgV1.Name, Channel: pkgV1.DefaultChannel}])
if err != nil {
return fmt.Errorf("could not get package metadata: %w", err)
}
cfg.PackageV2Metadatas = append(cfg.PackageV2Metadatas, *pkgMetaV2)
}
cfg.Packages = nil
cfg.Bundles = nil
return nil

//
//
//uniqueBundles := map[string]*model.Bundle{}
//m, err := declcfg.ConvertToModel(*cfg)
//if err != nil {
// return err
//}
//cfg.Packages = nil
//cfg.Bundles = nil
//
//for _, pkg := range m {
// head, err := pkg.DefaultChannel.Head()
// if err != nil {
// return err
// }
//
// pkgV2 := packageToPackageV2(pkg)
//
// if head.PropertiesP == nil || len(head.PropertiesP.CSVMetadatas) == 0 {
// return fmt.Errorf("no CSV metadata defined for package %s", pkg.Name)
// }
// csvMetadata := head.PropertiesP.CSVMetadatas[0]
//
// packageAnnotations := map[string]string{
// "operators.openshift.io/capabilities": csvMetadata.Annotations["capabilities"],
// "operators.openshift.io/categories": csvMetadata.Annotations["categories"],
// "olm.operatorframework.io/olmv0-compatibility-default-channel": pkg.DefaultChannel.Name,
//
// // TODO: these really probably belong at the bundle metadata level
// "operators.openshift.io/infrastructure-features": csvMetadata.Annotations["operators.openshift.io/infrastructure-features"],
// "operators.openshift.io/valid-subscription": csvMetadata.Annotations["operators.openshift.io/valid-subscription"],
// }
//
// v2p := declcfg.PackageV2{
// Schema: declcfg.SchemaPackageV2,
// Package: pkg.Name,
// }
// cfg.PackageV2s = append(cfg.PackageV2s, v2p)
//
// v2pkgMeta := declcfg.PackageV2Metadata{
// Schema: declcfg.SchemaPackageV2,
// Package: pkg.Name,
//
// DisplayName: csvMetadata.DisplayName,
// ShortDescription: csvMetadata.Annotations["description"],
// LongDescription: csvMetadata.Description,
// Keywords: csvMetadata.Keywords,
// Links: csvMetadata.Links,
// Provider: csvMetadata.Provider,
// Maintainers: csvMetadata.Maintainers,
// Annotations: packageAnnotations,
// }
// cfg.PackageV2Metadatas = append(cfg.PackageV2Metadatas, v2pkgMeta)
//
// if pkg.Icon != nil {
// v2i := declcfg.PackageV2Icon{
// Schema: "olm.package.icon",
// Package: pkg.Name,
// Data: pkg.Icon.Data,
// MediaType: pkg.Icon.MediaType,
// }
// cfg.PackageV2Icons = append(cfg.PackageV2Icons, v2i)
// }
//
// for _, ch := range pkg.Channels {
// for _, b := range ch.Bundles {
// uniqueBundles[b.Name] = b
// }
// }
//}
//
//bundleRenames := make(map[string]string, len(uniqueBundles))
//bundleVersions := make(map[string]semver.Version, len(uniqueBundles))
//for _, b := range uniqueBundles {
// newName := fmt.Sprintf("%s.v%s", b.Package.Name, b.Version)
// bundleRenames[b.Name] = newName
// bundleVersions[b.Name] = b.Version
//
// v2b := declcfg.BundleV2{
// Schema: declcfg.SchemaBundleV2,
// Package: b.Package.Name,
// Name: newName,
//
// Version: b.Version.String(),
// Release: 0,
// Reference: fmt.Sprintf("docker://%s", b.Image),
//
// Constraints: map[string][]json.RawMessage{},
// Properties: map[string][]json.RawMessage{},
// }
// for _, p := range b.Properties {
// if p.Type == property.TypePackage || p.Type == property.TypeBundleObject {
// continue
// }
// if isContraint(p) {
// v2b.Constraints[p.Type] = append(v2b.Constraints[p.Type], p.Value)
// } else {
// v2b.Properties[p.Type] = append(v2b.Properties[p.Type], p.Value)
// }
// }
// cfg.BundleV2s = append(cfg.BundleV2s, v2b)
//
// if len(b.RelatedImages) > 0 {
// refs := sets.New[string]()
// for _, ri := range b.RelatedImages {
// if ri.Image != "" {
// refs.Insert(fmt.Sprintf("docker://%s", ri.Image))
// }
// }
// if refs.Len() > 0 {
// cfg.BundleV2RelatedReferences = append(cfg.BundleV2RelatedReferences, declcfg.BundleV2RelatedReferences{
// Schema: declcfg.SchemaBundleV2RelatedReferences,
// Package: b.Package.Name,
// Name: newName,
// References: sets.List(refs),
// })
// }
// }
//
// if b.PropertiesP != nil && len(b.PropertiesP.CSVMetadatas) > 0 {
// csvMetadata := b.PropertiesP.CSVMetadatas[0]
//
// desiredAnnotations := map[string]string{
// "createdAt": "operators.openshift.io/creationTimestamp",
// "repository": "operators.openshift.io/repository",
// "support": "operators.openshift.io/support",
// "containerImage": "operators.openshift.io/image",
// }
//
// bundleAnnotations := map[string]string{}
// for fromKey, toKey := range desiredAnnotations {
// if value, ok := csvMetadata.Annotations[fromKey]; ok {
// bundleAnnotations[toKey] = value
// }
// }
// if len(bundleAnnotations) > 0 {
// v2BundleMetadata := declcfg.BundleV2Metadata{
// Schema: declcfg.SchemaBundleV2Metadata,
// Package: b.Package.Name,
// Name: newName,
// Annotations: bundleAnnotations,
// }
// cfg.BundleV2Metadatas = append(cfg.BundleV2Metadatas, v2BundleMetadata)
// }
// }
//}
//
//}
//cfg.Channels = nil
//
//for depIdx := range cfg.Deprecations {
// for entryIdx := range cfg.Deprecations[depIdx].Entries {
// e := &cfg.Deprecations[depIdx].Entries[entryIdx]
// switch e.Reference.Schema {
// case declcfg.SchemaPackage:
// e.Reference.Schema = declcfg.SchemaPackageV2
// case declcfg.SchemaBundle:
// e.Reference.Schema = declcfg.SchemaBundleV2
// }
// }
//}
//return nil
}

func isContraint(p property.Property) bool {
switch p.Type {
case property.TypeConstraint, property.TypeGVKRequired, property.TypePackageRequired:
return true
}
return false
}

type bundleKey struct {
Package, Bundle string
}

type channelKey struct {
Package, Channel string
}

func getChannelHeads(cfg *declcfg.DeclarativeConfig) (map[channelKey]*declcfg.Bundle, error) {
channelHeadsKeys := map[bundleKey][]channelKey{}
for _, ch := range cfg.Channels {
head, err := getChannelHead(ch)
if err != nil {
return nil, fmt.Errorf("invalid channel %q in package %q: failed to get channel head: %w", ch.Name, ch.Package, err)
}
bk := bundleKey{Package: ch.Package, Bundle: head}
ck := channelKey{Package: ch.Package, Channel: ch.Name}
channelHeadsKeys[bk] = append(channelHeadsKeys[bk], ck)
}
channelHeads := map[channelKey]*declcfg.Bundle{}
for i, b := range cfg.Bundles {
bk := bundleKey{Package: b.Package, Bundle: b.Name}
if channelKeys, ok := channelHeadsKeys[bk]; ok {
for _, ck := range channelKeys {
channelHeads[ck] = &cfg.Bundles[i]
}
}
}
return channelHeads, nil
}

func getChannelHead(ch declcfg.Channel) (string, error) {
incoming := map[string]int{}
for _, b := range ch.Entries {
if b.Replaces != "" {
incoming[b.Replaces]++
}
for _, skip := range b.Skips {
incoming[skip]++
}
}
heads := sets.New[string]()
for _, b := range ch.Entries {
if _, ok := incoming[b.Name]; !ok {
heads.Insert(b.Name)
}
}
if heads.Len() == 0 {
return "", fmt.Errorf("no channel head found in graph")
}
if len(heads) > 1 {
return "", fmt.Errorf("multiple channel heads found in graph: %s", strings.Join(sets.List(heads), ", "))
}
return heads.UnsortedList()[0], nil
}

func packageToPackageV2(in declcfg.Package) declcfg.PackageV2 {
return declcfg.PackageV2{
Schema: declcfg.SchemaPackageV2,
Package: in.Name,
}
}
func packageToPackageMetadataV2(in declcfg.Package, defaultChannelHead *declcfg.Bundle) (*declcfg.PackageV2Metadata, error) {
var csvMetadata *property.CSVMetadata
if defaultChannelHead.CsvJSON != "" {
var csvMetaTry property.CSVMetadata
if err := json.Unmarshal([]byte(defaultChannelHead.CsvJSON), &csvMetaTry); err != nil {
return nil, fmt.Errorf("failed to unmarshal default channel head CSV: %w", err)
}
csvMetadata = &csvMetaTry
} else {
for _, p := range defaultChannelHead.Properties {
var csvMetaTry property.CSVMetadata
if p.Type == property.TypeCSVMetadata {
if err := json.Unmarshal(p.Value, &csvMetaTry); err != nil {
return nil, fmt.Errorf("failed to unmarshal default channel head property %q: %w", p.Type, err)
}
csvMetadata = &csvMetaTry
break
}
if p.Type == property.TypeBundleObject {
var pom v1.PartialObjectMetadata
if err := json.Unmarshal(p.Value, &pom); err != nil {
return nil, fmt.Errorf("failed to unmarshal default channel head property %q to determine object kind: %w", p.Type, err)
}
if pom.Kind != "ClusterServiceVersion" {
continue
}
var csv v1alpha1.ClusterServiceVersion
if err := json.Unmarshal(p.Value, &csv); err != nil {
return nil, fmt.Errorf("failed to unmarshal default channel head property %q as CSV: %w", p.Type, err)
}
csvMetadataProp := property.MustBuildCSVMetadata(csv)
if err := json.Unmarshal(csvMetadataProp.Value, &csvMetaTry); err != nil {
return nil, fmt.Errorf("failed to unmarshal default channel head property %q as CSV Metadata property: %w", p.Type, err)
}
csvMetadata = &csvMetaTry
}
}
}
if csvMetadata == nil {
return nil, nil
}

packageAnnotations := map[string]string{
"operators.openshift.io/capabilities": csvMetadata.Annotations["capabilities"],
"operators.openshift.io/categories": csvMetadata.Annotations["categories"],
"olm.operatorframework.io/olmv0-compatibility-default-channel": in.DefaultChannel,

// TODO: these really probably belong at the bundle metadata level
"operators.openshift.io/infrastructure-features": csvMetadata.Annotations["operators.openshift.io/infrastructure-features"],
"operators.openshift.io/valid-subscription": csvMetadata.Annotations["operators.openshift.io/valid-subscription"],
}

return &declcfg.PackageV2Metadata{
Schema: declcfg.SchemaPackageV2Metadata,
Package: in.Name,

DisplayName: csvMetadata.DisplayName,
ShortDescription: csvMetadata.Annotations["description"],
LongDescription: csvMetadata.Description,
Keywords: csvMetadata.Keywords,
Links: csvMetadata.Links,
Provider: csvMetadata.Provider,
Maintainers: csvMetadata.Maintainers,
Annotations: packageAnnotations,
}, nil
}
func packageToPackageIconV2(in declcfg.Package) *declcfg.PackageV2Icon {
if in.Icon == nil {
return nil
}
return &declcfg.PackageV2Icon{
Schema: declcfg.SchemaPackageV2Icon,
Package: in.Name,
Data: in.Icon.Data,
MediaType: in.Icon.MediaType,
}
}
1 change: 1 addition & 0 deletions alpha/action/migrations/migrations.go
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ type Migrations struct {
var allMigrations = []Migration{
newMigration(NoMigrations, "do nothing", func(_ *declcfg.DeclarativeConfig) error { return nil }),
newMigration("bundle-object-to-csv-metadata", `migrates bundles' "olm.bundle.object" to "olm.csv.metadata"`, bundleObjectToCSVMetadata),
newMigration("v2", `migrate catalog to olm.package.v2, olm.bundle.v2, and olm.package.icon`, v2),
}

func NewMigrations(name string) (*Migrations, error) {
110 changes: 104 additions & 6 deletions alpha/declcfg/declcfg.go
Original file line number Diff line number Diff line change
@@ -6,26 +6,44 @@ import (
"errors"
"fmt"

prettyunmarshaler "github.com/operator-framework/operator-registry/pkg/prettyunmarshaler"

"golang.org/x/text/cases"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"

"github.com/operator-framework/api/pkg/operators/v1alpha1"

"github.com/operator-framework/operator-registry/alpha/property"
"github.com/operator-framework/operator-registry/pkg/prettyunmarshaler"
)

const (
SchemaPackage = "olm.package"
SchemaChannel = "olm.channel"
SchemaBundle = "olm.bundle"
SchemaDeprecation = "olm.deprecations"

SchemaPackageV2 = "olm.package.v2"
SchemaPackageV2Metadata = "olm.package.v2.metadata"
SchemaPackageV2Icon = "olm.package.v2.icon"
SchemaChannelV2 = "olm.channel.v2"
SchemaBundleV2 = "olm.bundle.v2"
SchemaBundleV2Metadata = "olm.bundle.v2.metadata"
SchemaBundleV2RelatedReferences = "olm.bundle.v2.relatedReferences"
)

type DeclarativeConfig struct {
Packages []Package
Channels []Channel
Bundles []Bundle
Packages []Package
Channels []Channel
Bundles []Bundle

PackageV2s []PackageV2
PackageV2Icons []PackageV2Icon
PackageV2Metadatas []PackageV2Metadata
ChannelV2s []ChannelV2
BundleV2s []BundleV2
BundleV2Metadatas []BundleV2Metadata
BundleV2RelatedReferences []BundleV2RelatedReferences

Deprecations []Deprecation
Others []Meta
}
@@ -92,6 +110,75 @@ type RelatedImage struct {
Image string `json:"image"`
}

type PackageV2 struct {
Schema string `json:"schema"`
Package string `json:"package"`
}

type PackageV2Icon struct {
Schema string `json:"schema"`
Package string `json:"package"`
Data []byte `json:"data"`
MediaType string `json:"mediaType"`
}

type PackageV2Metadata struct {
Schema string `json:"schema"`
Package string `json:"package"`

Annotations map[string]string `json:"annotations,omitempty"`
DisplayName string `json:"displayName,omitempty"`
ShortDescription string `json:"shortDescription,omitempty"`
LongDescription string `json:"longDescription,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Links []v1alpha1.AppLink `json:"links,omitempty"`
Provider v1alpha1.AppLink `json:"provider,omitempty"`
Maintainers []v1alpha1.Maintainer `json:"maintainers,omitempty"`
}

type ChannelV2 struct {
Schema string `json:"schema"`
Package string `json:"package"`
Name string `json:"name"`

Edges []UpgradeEdgeV2 `json:"edges"`
}

type UpgradeEdgeV2 struct {
Version string `json:"version"`
UpgradesFrom string `json:"upgradesFrom"`
}

type BundleV2 struct {
Schema string `json:"schema"`
Package string `json:"package"`
Name string `json:"name"`

Version string `json:"version"`
Release uint32 `json:"release"`

Reference string `json:"ref"`

Properties map[string][]json.RawMessage `json:"properties,omitempty"`
Constraints map[string][]json.RawMessage `json:"constraints,omitempty"`
}

type BundleV2Metadata struct {
Schema string `json:"schema"`
Package string `json:"package"`
Name string `json:"name"`

Annotations map[string]string `json:"annotations,omitempty"`
}

type BundleV2RelatedReferences struct {
Schema string `json:"schema"`
Package string `json:"package"`
Name string `json:"name"`

References []string `json:"references,omitempty"`
}

type Deprecation struct {
Schema string `json:"schema"`
Package string `json:"package"`
@@ -204,6 +291,17 @@ func (destination *DeclarativeConfig) Merge(src *DeclarativeConfig) {
destination.Packages = append(destination.Packages, src.Packages...)
destination.Channels = append(destination.Channels, src.Channels...)
destination.Bundles = append(destination.Bundles, src.Bundles...)
destination.Others = append(destination.Others, src.Others...)

destination.PackageV2s = append(destination.PackageV2s, src.PackageV2s...)
destination.PackageV2Icons = append(destination.PackageV2Icons, src.PackageV2Icons...)
destination.PackageV2Metadatas = append(destination.PackageV2Metadatas, src.PackageV2Metadatas...)
destination.ChannelV2s = append(destination.ChannelV2s, src.ChannelV2s...)
destination.BundleV2s = append(destination.BundleV2s, src.BundleV2s...)
destination.BundleV2Metadatas = append(destination.BundleV2Metadatas, src.BundleV2Metadatas...)
destination.BundleV2RelatedReferences = append(destination.BundleV2RelatedReferences, src.BundleV2RelatedReferences...)

destination.Deprecations = append(destination.Deprecations, src.Deprecations...)

destination.Others = append(destination.Others, src.Others...)

}
121 changes: 117 additions & 4 deletions alpha/declcfg/declcfg_to_model.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package declcfg

import (
"fmt"
"strings"

"github.com/blang/semver/v4"
"k8s.io/apimachinery/pkg/util/sets"
@@ -42,6 +43,44 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
mpkgs[p.Name] = mpkg
}

for _, p := range cfg.PackageV2s {
if p.Package == "" {
return nil, fmt.Errorf("config contains package with no name")
}

if _, ok := mpkgs[p.Package]; ok {
return nil, fmt.Errorf("duplicate package %q", p.Package)
}

if errs := validation.IsDNS1123Label(p.Package); len(errs) > 0 {
return nil, fmt.Errorf("invalid package name %q: %v", p.Package, errs)
}

mpkg := &model.Package{
Name: p.Package,
Channels: map[string]*model.Channel{},
}
mpkgs[p.Package] = mpkg
}
for _, pi := range cfg.PackageV2Icons {
mpkg, ok := mpkgs[pi.Package]
if !ok {
return nil, fmt.Errorf("found package icon for non-existent package %q", pi.Package)
}
mpkg.Icon = &model.Icon{
Data: pi.Data,
MediaType: pi.MediaType,
}
}
for _, pm := range cfg.PackageV2Metadatas {
mpkg, ok := mpkgs[pm.Package]
if !ok {
return nil, fmt.Errorf("found package metadata for non-existent package %q", pm.Package)
}
mpkg.Description = pm.ShortDescription
defaultChannels[pm.Package] = pm.Annotations["olm.operatorframework.io/olmv0-compatibility-default-channel"]
}

channelDefinedEntries := map[string]sets.Set[string]{}
for _, c := range cfg.Channels {
mpkg, ok := mpkgs[c.Package]
@@ -154,6 +193,81 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
}
}

for _, b := range cfg.BundleV2s {
if b.Package == "" {
return nil, fmt.Errorf("package name must be set for bundle %q", b.Name)
}

ver, err := semver.Parse(b.Version)
if err != nil {
return nil, fmt.Errorf("error parsing bundle %q version %q: %v", b.Name, b.Version, err)
}

expectedName := fmt.Sprintf("%s.v%s", b.Package, b.Version)
if b.Release > 0 {
expectedName += fmt.Sprintf("__%d", b.Release)
}
if b.Name != expectedName {
return nil, fmt.Errorf("bundle name %q does not match expected name %q", b.Name, expectedName)
}
mpkg, ok := mpkgs[b.Package]
if !ok {
return nil, fmt.Errorf("unknown package %q for bundle %q", b.Package, b.Name)
}

bundles, ok := packageBundles[b.Package]
if !ok {
bundles = sets.Set[string]{}
}
if bundles.Has(b.Name) {
return nil, fmt.Errorf("package %q has duplicate bundle %q", b.Package, b.Name)
}
bundles.Insert(b.Name)
packageBundles[b.Package] = bundles

channelDefinedEntries[b.Package] = channelDefinedEntries[b.Package].Delete(b.Name)
found := false
for _, mch := range mpkg.Channels {
if mb, ok := mch.Bundles[b.Name]; ok {
found = true
if !strings.HasPrefix(b.Reference, "docker://") {
return nil, fmt.Errorf("bundle %q reference %q must be a docker reference (with prefix 'docker://')", b.Name, b.Reference)
}
mb.Image = strings.TrimPrefix(b.Reference, "docker://")
for k, values := range b.Properties {
for _, v := range values {
mb.Properties = append(mb.Properties, property.Property{
Type: k,
Value: v,
})
}
}
mb.Properties = append(mb.Properties, property.MustBuildPackage(b.Package, b.Name))
mb.Version = ver
}
}
if !found {
return nil, fmt.Errorf("package %q, bundle %q not found in any channel entries", b.Package, b.Name)
}
}
for _, bRefs := range cfg.BundleV2RelatedReferences {
mpkg, ok := mpkgs[bRefs.Package]
if !ok {
return nil, fmt.Errorf("unknown package %q for bundle related references %q", bRefs.Package, bRefs.Name)
}
for _, ch := range mpkg.Channels {
for _, b := range ch.Bundles {
if b.Name == bRefs.Name {
for _, ref := range bRefs.References {
if strings.HasPrefix(ref, "docker://") {
b.RelatedImages = append(b.RelatedImages, model.RelatedImage{Image: strings.TrimPrefix(ref, "docker://")})
}
}
}
}
}
}

for pkg, entries := range channelDefinedEntries {
if entries.Len() > 0 {
return nil, fmt.Errorf("no olm.bundle blobs found in package %q for olm.channel entries %s", pkg, sets.List[string](entries))
@@ -210,7 +324,7 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
references.Insert(entry.Reference)

switch entry.Reference.Schema {
case SchemaBundle:
case SchemaBundle, SchemaBundleV2:
if !packageBundles[deprecation.Package].Has(entry.Reference.Name) {
return nil, fmt.Errorf("cannot deprecate bundle %q for package %q: bundle not found", entry.Reference.Name, deprecation.Package)
}
@@ -226,14 +340,13 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
}
ch.Deprecation = &model.Deprecation{Message: entry.Message}

case SchemaPackage:
case SchemaPackage, SchemaPackageV2:
if entry.Reference.Name != "" {
return nil, fmt.Errorf("package name must be empty for deprecated package %q (specified %q)", deprecation.Package, entry.Reference.Name)
}
mpkg.Deprecation = &model.Deprecation{Message: entry.Message}

default:
return nil, fmt.Errorf("cannot deprecate object %#v referenced by entry %v for package %q: object schema unknown", entry.Reference, j, deprecation.Package)
return nil, fmt.Errorf("cannot deprecate object %#v referenced by entry %v for package %q: object schema unknown or deprecation not supported", entry.Reference, j, deprecation.Package)
}
}
}
73 changes: 68 additions & 5 deletions alpha/declcfg/load.go
Original file line number Diff line number Diff line change
@@ -273,11 +273,18 @@ func LoadSlice(metas []*Meta) (*DeclarativeConfig, error) {
type fbcBuilder struct {
cfg DeclarativeConfig

packagesMu sync.Mutex
channelsMu sync.Mutex
bundlesMu sync.Mutex
deprecationsMu sync.Mutex
othersMu sync.Mutex
packagesMu sync.Mutex
channelsMu sync.Mutex
bundlesMu sync.Mutex
packageV2sMu sync.Mutex
packageV2IconsMu sync.Mutex
packageV2MetadatasMu sync.Mutex
channelV2sMu sync.Mutex
bundleV2sMu sync.Mutex
bundleV2MetadatasMu sync.Mutex
bundleV2RelatedReferencesMu sync.Mutex
deprecationsMu sync.Mutex
othersMu sync.Mutex
}

func (c *fbcBuilder) addMeta(in *Meta) error {
@@ -309,6 +316,62 @@ func (c *fbcBuilder) addMeta(in *Meta) error {
c.bundlesMu.Lock()
c.cfg.Bundles = append(c.cfg.Bundles, b)
c.bundlesMu.Unlock()
case SchemaPackageV2:
var p PackageV2
if err := json.Unmarshal(in.Blob, &p); err != nil {
return fmt.Errorf("parse package: %v", err)
}
c.packageV2sMu.Lock()
c.cfg.PackageV2s = append(c.cfg.PackageV2s, p)
c.packageV2sMu.Unlock()
case SchemaPackageV2Icon:
var p PackageV2Icon
if err := json.Unmarshal(in.Blob, &p); err != nil {
return fmt.Errorf("parse package icon: %v", err)
}
c.packageV2IconsMu.Lock()
c.cfg.PackageV2Icons = append(c.cfg.PackageV2Icons, p)
c.packageV2IconsMu.Unlock()
case SchemaPackageV2Metadata:
var p PackageV2Metadata
if err := json.Unmarshal(in.Blob, &p); err != nil {
return fmt.Errorf("parse package metadata: %v", err)
}
c.packageV2MetadatasMu.Lock()
c.cfg.PackageV2Metadatas = append(c.cfg.PackageV2Metadatas, p)
c.packageV2MetadatasMu.Unlock()
case SchemaChannelV2:
var u ChannelV2
if err := json.Unmarshal(in.Blob, &u); err != nil {
return fmt.Errorf("parse channel v2: %v", err)
}
c.channelV2sMu.Lock()
c.cfg.ChannelV2s = append(c.cfg.ChannelV2s, u)
c.channelV2sMu.Unlock()
case SchemaBundleV2:
var b BundleV2
if err := json.Unmarshal(in.Blob, &b); err != nil {
return fmt.Errorf("parse bundle: %v", err)
}
c.bundleV2sMu.Lock()
c.cfg.BundleV2s = append(c.cfg.BundleV2s, b)
c.bundleV2sMu.Unlock()
case SchemaBundleV2Metadata:
var b BundleV2Metadata
if err := json.Unmarshal(in.Blob, &b); err != nil {
return fmt.Errorf("parse bundle metadata: %v", err)
}
c.bundleV2MetadatasMu.Lock()
c.cfg.BundleV2Metadatas = append(c.cfg.BundleV2Metadatas, b)
c.bundleV2MetadatasMu.Unlock()
case SchemaBundleV2RelatedReferences:
var b BundleV2RelatedReferences
if err := json.Unmarshal(in.Blob, &b); err != nil {
return fmt.Errorf("parse bundle related references: %v", err)
}
c.bundleV2RelatedReferencesMu.Lock()
c.cfg.BundleV2RelatedReferences = append(c.cfg.BundleV2RelatedReferences, b)
c.bundleV2RelatedReferencesMu.Unlock()
case SchemaDeprecation:
var d Deprecation
if err := json.Unmarshal(in.Blob, &d); err != nil {
124 changes: 124 additions & 0 deletions alpha/declcfg/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package declcfg

import (
"cmp"
"slices"

"k8s.io/apimachinery/pkg/util/sets"
)

func SortByPackage(cfg *DeclarativeConfig) {
slices.SortFunc(cfg.Packages, func(a, b Package) int { return cmp.Compare(a.Name, b.Name) })
slices.SortFunc(cfg.Channels, func(a, b Channel) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.Bundles, func(a, b Bundle) int { return cmp.Compare(a.Package, b.Package) })

slices.SortFunc(cfg.PackageV2s, func(a, b PackageV2) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.PackageV2Icons, func(a, b PackageV2Icon) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.PackageV2Metadatas, func(a, b PackageV2Metadata) int { return cmp.Compare(a.Package, b.Package) })

slices.SortFunc(cfg.ChannelV2s, func(a, b ChannelV2) int { return cmp.Compare(a.Package, b.Package) })

slices.SortFunc(cfg.BundleV2s, func(a, b BundleV2) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.BundleV2Metadatas, func(a, b BundleV2Metadata) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.BundleV2RelatedReferences, func(a, b BundleV2RelatedReferences) int { return cmp.Compare(a.Package, b.Package) })

slices.SortFunc(cfg.Deprecations, func(a, b Deprecation) int { return cmp.Compare(a.Package, b.Package) })
slices.SortFunc(cfg.Others, func(a, b Meta) int { return cmp.Compare(a.Package, b.Package) })
}

func OrganizeByPackage(cfg DeclarativeConfig) map[string]DeclarativeConfig {
pkgNames := sets.New[string]()
packagesByName := map[string][]Package{}
for _, p := range cfg.Packages {
pkgName := p.Name
pkgNames.Insert(pkgName)
packagesByName[pkgName] = append(packagesByName[pkgName], p)
}
channelsByPackage := map[string][]Channel{}
for _, c := range cfg.Channels {
pkgName := c.Package
pkgNames.Insert(pkgName)
channelsByPackage[pkgName] = append(channelsByPackage[pkgName], c)
}
bundlesByPackage := map[string][]Bundle{}
for _, b := range cfg.Bundles {
pkgName := b.Package
pkgNames.Insert(pkgName)
bundlesByPackage[pkgName] = append(bundlesByPackage[pkgName], b)
}
packageV2sByName := map[string][]PackageV2{}
for _, p := range cfg.PackageV2s {
pkgName := p.Package
pkgNames.Insert(pkgName)
packageV2sByName[pkgName] = append(packageV2sByName[pkgName], p)
}
packageIconsByPackage := map[string][]PackageV2Icon{}
for _, pi := range cfg.PackageV2Icons {
pkgName := pi.Package
pkgNames.Insert(pkgName)
packageIconsByPackage[pkgName] = append(packageIconsByPackage[pkgName], pi)
}
packageMetadataV2sByPackage := map[string][]PackageV2Metadata{}
for _, pm := range cfg.PackageV2Metadatas {
pkgName := pm.Package
pkgNames.Insert(pkgName)
packageMetadataV2sByPackage[pkgName] = append(packageMetadataV2sByPackage[pkgName], pm)
}
channelV2sByPackage := map[string][]ChannelV2{}
for _, u := range cfg.ChannelV2s {
pkgName := u.Package
pkgNames.Insert(pkgName)
channelV2sByPackage[pkgName] = append(channelV2sByPackage[pkgName], u)
}
bundleV2sByPackage := map[string][]BundleV2{}
for _, b := range cfg.BundleV2s {
pkgName := b.Package
pkgNames.Insert(pkgName)
bundleV2sByPackage[pkgName] = append(bundleV2sByPackage[pkgName], b)
}
bundleV2RelatedReferencesByPackage := map[string][]BundleV2RelatedReferences{}
for _, b := range cfg.BundleV2RelatedReferences {
pkgName := b.Package
pkgNames.Insert(pkgName)
bundleV2RelatedReferencesByPackage[pkgName] = append(bundleV2RelatedReferencesByPackage[pkgName], b)
}
bundleV2MetadatasByPackage := map[string][]BundleV2Metadata{}
for _, b := range cfg.BundleV2Metadatas {
pkgName := b.Package
pkgNames.Insert(pkgName)
bundleV2MetadatasByPackage[pkgName] = append(bundleV2MetadatasByPackage[pkgName], b)
}
othersByPackage := map[string][]Meta{}
for _, o := range cfg.Others {
pkgName := o.Package
pkgNames.Insert(pkgName)
othersByPackage[pkgName] = append(othersByPackage[pkgName], o)
}
deprecationsByPackage := map[string][]Deprecation{}
for _, d := range cfg.Deprecations {
pkgName := d.Package
pkgNames.Insert(pkgName)
deprecationsByPackage[pkgName] = append(deprecationsByPackage[pkgName], d)
}

fbcsByPackageName := make(map[string]DeclarativeConfig, len(pkgNames))
for _, pkgName := range sets.List(pkgNames) {
fbcsByPackageName[pkgName] = DeclarativeConfig{
Packages: packagesByName[pkgName],
Channels: channelsByPackage[pkgName],
Bundles: bundlesByPackage[pkgName],

PackageV2s: packageV2sByName[pkgName],
PackageV2Icons: packageIconsByPackage[pkgName],
PackageV2Metadatas: packageMetadataV2sByPackage[pkgName],
ChannelV2s: channelV2sByPackage[pkgName],
BundleV2s: bundleV2sByPackage[pkgName],
BundleV2RelatedReferences: bundleV2RelatedReferencesByPackage[pkgName],
BundleV2Metadatas: bundleV2MetadatasByPackage[pkgName],

Deprecations: deprecationsByPackage[pkgName],
Others: othersByPackage[pkgName],
}
}
return fbcsByPackageName
}
154 changes: 45 additions & 109 deletions alpha/declcfg/write.go
Original file line number Diff line number Diff line change
@@ -2,11 +2,13 @@ package declcfg

import (
"bytes"
"cmp"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"sort"
"strings"

@@ -394,131 +396,65 @@ type encoder interface {
Encode(interface{}) error
}

func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
pkgNames := sets.NewString()

packagesByName := map[string][]Package{}
for _, p := range cfg.Packages {
pkgName := p.Name
pkgNames.Insert(pkgName)
packagesByName[pkgName] = append(packagesByName[pkgName], p)
}
channelsByPackage := map[string][]Channel{}
for _, c := range cfg.Channels {
pkgName := c.Package
pkgNames.Insert(pkgName)
channelsByPackage[pkgName] = append(channelsByPackage[pkgName], c)
}
bundlesByPackage := map[string][]Bundle{}
for _, b := range cfg.Bundles {
pkgName := b.Package
pkgNames.Insert(pkgName)
bundlesByPackage[pkgName] = append(bundlesByPackage[pkgName], b)
}
othersByPackage := map[string][]Meta{}
for _, o := range cfg.Others {
pkgName := o.Package
pkgNames.Insert(pkgName)
othersByPackage[pkgName] = append(othersByPackage[pkgName], o)
}
deprecationsByPackage := map[string][]Deprecation{}
for _, d := range cfg.Deprecations {
pkgName := d.Package
pkgNames.Insert(pkgName)
deprecationsByPackage[pkgName] = append(deprecationsByPackage[pkgName], d)
}

for _, pName := range pkgNames.List() {
if len(pName) == 0 {
continue
}
pkgs := packagesByName[pName]
for _, p := range pkgs {
if err := enc.Encode(p); err != nil {
return err
}
}

channels := channelsByPackage[pName]
sort.Slice(channels, func(i, j int) bool {
return channels[i].Name < channels[j].Name
})
for _, c := range channels {
if err := enc.Encode(c); err != nil {
func encodeAll[T any](values []T) func(encoder) error {
return func(enc encoder) error {
for _, v := range values {
if err := enc.Encode(v); err != nil {
return err
}
}
return nil
}
}

bundles := bundlesByPackage[pName]
sort.Slice(bundles, func(i, j int) bool {
return bundles[i].Name < bundles[j].Name
})
for _, b := range bundles {
if err := enc.Encode(b); err != nil {
return err
func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
byPackage := OrganizeByPackage(cfg)

for _, pkgName := range sets.List(sets.KeySet(byPackage)) {
slices.SortFunc(byPackage[pkgName].Packages, func(i, j Package) int { return cmp.Compare(i.Name, j.Name) })
slices.SortFunc(byPackage[pkgName].Channels, func(i, j Channel) int { return cmp.Compare(i.Name, j.Name) })
slices.SortFunc(byPackage[pkgName].Bundles, func(i, j Bundle) int { return cmp.Compare(i.Name, j.Name) })

slices.SortFunc(byPackage[pkgName].PackageV2s, func(i, j PackageV2) int { return cmp.Compare(i.Package, j.Package) })
slices.SortFunc(byPackage[pkgName].PackageV2Metadatas, func(i, j PackageV2Metadata) int { return cmp.Compare(i.Package, j.Package) })
slices.SortFunc(byPackage[pkgName].PackageV2Icons, func(i, j PackageV2Icon) int { return cmp.Compare(i.Package, j.Package) })
slices.SortFunc(byPackage[pkgName].ChannelV2s, func(i, j ChannelV2) int { return cmp.Compare(i.Name, j.Name) })
slices.SortFunc(byPackage[pkgName].BundleV2s, func(i, j BundleV2) int { return cmp.Compare(i.Name, j.Name) })
slices.SortFunc(byPackage[pkgName].BundleV2RelatedReferences, func(i, j BundleV2RelatedReferences) int { return cmp.Compare(i.Name, j.Name) })
slices.SortFunc(byPackage[pkgName].BundleV2Metadatas, func(i, j BundleV2Metadata) int { return cmp.Compare(i.Name, j.Name) })

slices.SortFunc(byPackage[pkgName].Deprecations, func(i, j Deprecation) int { return cmp.Compare(i.Package, j.Package) })
slices.SortFunc(byPackage[pkgName].Others, func(i, j Meta) int {
if bySchema := cmp.Compare(i.Schema, j.Schema); bySchema != 0 {
return bySchema
}
}

others := othersByPackage[pName]
sort.SliceStable(others, func(i, j int) bool {
return others[i].Schema < others[j].Schema
return cmp.Compare(i.Name, j.Name)
})
for _, o := range others {
if err := enc.Encode(o); err != nil {
return err
}
}

//
// Normally we would order the deprecations, but it really doesn't make sense since
// - there will be 0 or 1 of them for any given package
// - they have no other useful field for ordering
//
// validation is typically via conversion to a model.Model and invoking model.Package.Validate()
// It's possible that a user of the object could create a slice containing more then 1
// Deprecation object for a package, and it would bypass validation if this
// function gets called without conversion.
//
deprecations := deprecationsByPackage[pName]
for _, d := range deprecations {
if err := enc.Encode(d); err != nil {
for _, f := range []func(enc encoder) error{
encodeAll(byPackage[pkgName].Packages),
encodeAll(byPackage[pkgName].Channels),
encodeAll(byPackage[pkgName].Bundles),
encodeAll(byPackage[pkgName].PackageV2s),
encodeAll(byPackage[pkgName].PackageV2Metadatas),
encodeAll(byPackage[pkgName].PackageV2Icons),
encodeAll(byPackage[pkgName].ChannelV2s),
encodeAll(byPackage[pkgName].BundleV2s),
encodeAll(byPackage[pkgName].Deprecations),
encodeAll(byPackage[pkgName].Others),
} {
if err := f(enc); err != nil {
return err
}
}
}

for _, o := range othersByPackage[""] {
if err := enc.Encode(o); err != nil {
return err
}
}

return nil
}

type WriteFunc func(config DeclarativeConfig, w io.Writer) error

func WriteFS(cfg DeclarativeConfig, rootDir string, writeFunc WriteFunc, fileExt string) error {
channelsByPackage := map[string][]Channel{}
for _, c := range cfg.Channels {
channelsByPackage[c.Package] = append(channelsByPackage[c.Package], c)
}
bundlesByPackage := map[string][]Bundle{}
for _, b := range cfg.Bundles {
bundlesByPackage[b.Package] = append(bundlesByPackage[b.Package], b)
}

if err := os.MkdirAll(rootDir, 0777); err != nil {
return err
}

for _, p := range cfg.Packages {
fcfg := DeclarativeConfig{
Packages: []Package{p},
Channels: channelsByPackage[p.Name],
Bundles: bundlesByPackage[p.Name],
}
pkgDir := filepath.Join(rootDir, p.Name)
for pkgName, fcfg := range OrganizeByPackage(cfg) {
pkgDir := filepath.Join(rootDir, pkgName)
if err := os.MkdirAll(pkgDir, 0777); err != nil {
return err
}
71 changes: 71 additions & 0 deletions annotations.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
alm-examples
alm-examples-metadata
anaconda-cronjob-image
capabilities
categories
certified
certifiedLevel
console.openshift.io/disable-operand-delete
console.openshift.io/plugins
containerImage
createdAt
description
external.features.ocs.openshift.io/supported-platforms
external.features.ocs.openshift.io/validation
externalClusterScript
features.ocs.openshift.io/disabled
features.ocs.openshift.io/enabled
features.operators.openshift.io/cnf
features.operators.openshift.io/cni
features.operators.openshift.io/csi
features.operators.openshift.io/disconnected
features.operators.openshift.io/fips-compliant
features.operators.openshift.io/fipsmode
features.operators.openshift.io/proxy-aware
features.operators.openshift.io/tls-profiles
features.operators.openshift.io/token-auth-aws
features.operators.openshift.io/token-auth-azure
features.operators.openshift.io/token-auth-gcp
imagePullPolicy
netobserv.io/version-release
odh-etcd-image
odh-ml-pipeline-envoy-proxy-image
odh-ml-pipeline-mariadb-image
odh-ml-pipeline-moveresult-image
odh-python-runtime-image
olm.properties
olm.skipRange
olm.substitutesFor
operator.openshift.io/uninstall-message
operatorframework.io/cluster-monitoring
operatorframework.io/initialization-resource
operatorframework.io/suggested-namespace
operatorframework.io/suggested-namespace-template
operators.openshift.io/capability
operators.openshift.io/infrastructure-features
operators.openshift.io/must-gather-image
operators.openshift.io/valid-licenses
operators.openshift.io/valid-subscription
operators.operatorframework.io/builder
operators.operatorframework.io/internal-objects
operators.operatorframework.io/operator-type
operators.operatorframework.io/project_layout
ose-cli-image
ose-oauth-proxy-dashboard-image
ose-oauth-proxy-dsp-image
ose-oauth-proxy-dw-image
ose-oauth-proxy-image
prometheus-alertmanager-image
prometheus-base-image
prometheus-config-reloader-image
prometheus-operator-image
quay-version
repository
skipRange
support
target.workload.openshift.io/management
tectonic-visibility
test-images-nvrs
ubi-minimal-image
ubi9-image
vendors.odf.openshift.io/kind