diff --git a/alpha/action/migrations/001_v2.go b/alpha/action/migrations/001_v2.go new file mode 100644 index 000000000..c1913db27 --- /dev/null +++ b/alpha/action/migrations/001_v2.go @@ -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, + } +} diff --git a/alpha/action/migrations/migrations.go b/alpha/action/migrations/migrations.go index 22ff86b74..32f02d09b 100644 --- a/alpha/action/migrations/migrations.go +++ b/alpha/action/migrations/migrations.go @@ -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) { diff --git a/alpha/declcfg/declcfg.go b/alpha/declcfg/declcfg.go index 7797baa49..9cd7151fb 100644 --- a/alpha/declcfg/declcfg.go +++ b/alpha/declcfg/declcfg.go @@ -6,13 +6,14 @@ 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 ( @@ -20,12 +21,29 @@ const ( 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...) + } diff --git a/alpha/declcfg/declcfg_to_model.go b/alpha/declcfg/declcfg_to_model.go index 2657efb16..9e2d27386 100644 --- a/alpha/declcfg/declcfg_to_model.go +++ b/alpha/declcfg/declcfg_to_model.go @@ -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) } } } diff --git a/alpha/declcfg/load.go b/alpha/declcfg/load.go index f811b3145..8f14aca68 100644 --- a/alpha/declcfg/load.go +++ b/alpha/declcfg/load.go @@ -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 { diff --git a/alpha/declcfg/util.go b/alpha/declcfg/util.go new file mode 100644 index 000000000..ce65d584c --- /dev/null +++ b/alpha/declcfg/util.go @@ -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 +} diff --git a/alpha/declcfg/write.go b/alpha/declcfg/write.go index 9856c2e1e..d5efdd58c 100644 --- a/alpha/declcfg/write.go +++ b/alpha/declcfg/write.go @@ -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 } diff --git a/annotations.txt b/annotations.txt new file mode 100644 index 000000000..ff799b5e4 --- /dev/null +++ b/annotations.txt @@ -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