Skip to content

Commit 06f9b26

Browse files
committed
fbc: promote package-level metdata from olm.csv.metadata to olm.package blob
Signed-off-by: Joe Lanford <[email protected]>
1 parent bc1f8c2 commit 06f9b26

File tree

7 files changed

+267
-40
lines changed

7 files changed

+267
-40
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package migrations
2+
3+
import (
4+
"encoding/json"
5+
"slices"
6+
"strings"
7+
"unicode"
8+
"unicode/utf8"
9+
10+
"github.com/Masterminds/semver/v3"
11+
12+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
13+
14+
"github.com/operator-framework/operator-registry/alpha/declcfg"
15+
"github.com/operator-framework/operator-registry/alpha/property"
16+
)
17+
18+
func promotePackageMetadata(cfg *declcfg.DeclarativeConfig) error {
19+
metadataByPackage := map[string]promotedMetadata{}
20+
for i := range cfg.Bundles {
21+
b := &cfg.Bundles[i]
22+
23+
csvMetadata, csvMetadataIdx, err := getCsvMetadata(b)
24+
if err != nil {
25+
return err
26+
}
27+
28+
// Skip objects that have no olm.csv.metadata property
29+
if csvMetadata == nil {
30+
continue
31+
}
32+
33+
// Keep track of the metadata from the highest versioned bundle from each package.
34+
cur, ok := metadataByPackage[b.Package]
35+
if !ok || compareRegistryV1Semver(cur.version, b.Version) < 0 {
36+
metadataByPackage[b.Package] = promotedCSVMetadata(b.Version, csvMetadata)
37+
}
38+
39+
// Delete the package-level metadata from the olm.csv.metadata object and
40+
// update the bundle properties to use the new slimmed-down revision of it.
41+
csvMetadata.DisplayName = ""
42+
delete(csvMetadata.Annotations, "description")
43+
csvMetadata.Provider = v1alpha1.AppLink{}
44+
csvMetadata.Maintainers = nil
45+
csvMetadata.Links = nil
46+
csvMetadata.Keywords = nil
47+
48+
newCSVMetadata, err := json.Marshal(csvMetadata)
49+
if err != nil {
50+
return err
51+
}
52+
b.Properties[csvMetadataIdx] = property.Property{
53+
Type: property.TypeCSVMetadata,
54+
Value: newCSVMetadata,
55+
}
56+
}
57+
58+
// Update each olm.package object to include the metadata we extracted from
59+
// bundles in the first loop.
60+
for i := range cfg.Packages {
61+
pkg := &cfg.Packages[i]
62+
metadata, ok := metadataByPackage[pkg.Name]
63+
if !ok {
64+
continue
65+
}
66+
pkg.DisplayName = metadata.displayName
67+
pkg.ShortDescription = shortenDescription(metadata.shortDescription)
68+
if metadata.provider.Name != "" || metadata.provider.URL != "" {
69+
pkg.Provider = &metadata.provider
70+
}
71+
pkg.Maintainers = metadata.maintainers
72+
pkg.Links = metadata.links
73+
pkg.Keywords = slices.DeleteFunc(metadata.keywords, func(s string) bool {
74+
// Delete keywords that are empty strings
75+
return s == ""
76+
})
77+
}
78+
return nil
79+
}
80+
81+
func getCsvMetadata(b *declcfg.Bundle) (*property.CSVMetadata, int, error) {
82+
for i, p := range b.Properties {
83+
if p.Type != property.TypeCSVMetadata {
84+
continue
85+
}
86+
var csvMetadata property.CSVMetadata
87+
if err := json.Unmarshal(p.Value, &csvMetadata); err != nil {
88+
return nil, -1, err
89+
}
90+
return &csvMetadata, i, nil
91+
}
92+
return nil, -1, nil
93+
}
94+
95+
func compareRegistryV1Semver(a, b *semver.Version) int {
96+
if v := a.Compare(b); v != 0 {
97+
return v
98+
}
99+
aPre := semver.New(0, 0, 0, a.Metadata(), "")
100+
bPre := semver.New(0, 0, 0, b.Metadata(), "")
101+
return aPre.Compare(bPre)
102+
}
103+
104+
type promotedMetadata struct {
105+
version *semver.Version
106+
107+
displayName string
108+
shortDescription string
109+
provider v1alpha1.AppLink
110+
maintainers []v1alpha1.Maintainer
111+
links []v1alpha1.AppLink
112+
keywords []string
113+
}
114+
115+
func promotedCSVMetadata(version *semver.Version, metadata *property.CSVMetadata) promotedMetadata {
116+
return promotedMetadata{
117+
version: version,
118+
displayName: metadata.DisplayName,
119+
shortDescription: metadata.Annotations["description"],
120+
provider: metadata.Provider,
121+
maintainers: metadata.Maintainers,
122+
links: metadata.Links,
123+
keywords: metadata.Keywords,
124+
}
125+
}
126+
127+
func shortenDescription(input string) string {
128+
const maxLen = 256
129+
input = strings.TrimSpace(input)
130+
131+
// If the input is already under the limit return it.
132+
if utf8.RuneCountInString(input) <= maxLen {
133+
return input
134+
}
135+
136+
// Chop off everything after the first paragraph.
137+
if idx := strings.Index(input, "\n\n"); idx != -1 {
138+
input = strings.TrimSpace(input[:idx])
139+
}
140+
141+
// If we're _now_ under the limit, return the first paragraph.
142+
if utf8.RuneCountInString(input) <= maxLen {
143+
return input
144+
}
145+
146+
// If the first paragraph is still over the limit, we'll have to truncate.
147+
// We'll truncate at the last word boundary that still allows an ellipsis
148+
// to fit within the maximum length. But if there are no word boundaries
149+
// (veeeeery unlikely), we'll hard truncate mid-word.
150+
input = input[:maxLen-3]
151+
if truncatedIdx := strings.LastIndexFunc(input, unicode.IsSpace); truncatedIdx != -1 {
152+
return input[:truncatedIdx] + "..."
153+
}
154+
return input + "..."
155+
}

alpha/action/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ var allMigrations = []Migration{
5656
newMigration("bundle-object-to-csv-metadata", `migrates bundles' "olm.bundle.object" to "olm.csv.metadata"`, bundleObjectToCSVMetadata),
5757
newMigration("split-icon", `split package icon out into separate "olm.icon" blob`, splitIcon),
5858
newMigration("promote-bundle-version", `promote bundle version into first-class bundle field, remove olm.package properties`, promoteBundleVersion),
59+
newMigration("promote-package-metadata", `promote package metadata from "olm.csv.metadata" properties to "olm.package" blob`, promotePackageMetadata),
5960
}
6061

6162
func NewMigrations(name string) (*Migrations, error) {

alpha/action/migrations/migrations_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ func TestMigrations(t *testing.T) {
3737
}
3838
return nil
3939
},
40-
MigrationToken("split-icon"): func(d *declcfg.DeclarativeConfig) error { return nil },
41-
MigrationToken("promote-bundle-version"): func(d *declcfg.DeclarativeConfig) error { return nil },
40+
MigrationToken("split-icon"): func(d *declcfg.DeclarativeConfig) error { return nil },
41+
MigrationToken("promote-bundle-version"): func(d *declcfg.DeclarativeConfig) error { return nil },
42+
MigrationToken("promote-package-metadata"): func(d *declcfg.DeclarativeConfig) error { return nil },
4243
}
4344

4445
tests := []struct {

alpha/declcfg/declcfg.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
utilerrors "k8s.io/apimachinery/pkg/util/errors"
1212
"k8s.io/apimachinery/pkg/util/sets"
1313

14+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
15+
1416
"github.com/operator-framework/operator-registry/alpha/property"
1517
prettyunmarshaler "github.com/operator-framework/operator-registry/pkg/prettyunmarshaler"
1618
)
@@ -33,9 +35,15 @@ type DeclarativeConfig struct {
3335
}
3436

3537
type Package struct {
36-
Schema string `json:"schema"`
37-
Name string `json:"name"`
38-
DefaultChannel string `json:"defaultChannel"`
38+
Schema string `json:"schema"`
39+
Name string `json:"name"`
40+
DisplayName string `json:"displayName,omitempty"`
41+
ShortDescription string `json:"shortDescription,omitempty"`
42+
Provider *v1alpha1.AppLink `json:"provider,omitempty"`
43+
Maintainers []v1alpha1.Maintainer `json:"maintainers,omitempty"`
44+
Links []v1alpha1.AppLink `json:"links,omitempty"`
45+
Keywords []string `json:"keywords,omitempty"`
46+
DefaultChannel string `json:"defaultChannel"`
3947

4048
// Deprecated: It is no longer recommended to embed an icon in the package.
4149
// Instead, use separate a Icon item alongside the Package.

alpha/declcfg/declcfg_to_model.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
3131
Name: p.Name,
3232
Description: p.Description,
3333
Channels: map[string]*model.Channel{},
34+
35+
DisplayName: p.DisplayName,
36+
ShortDescription: p.ShortDescription,
37+
Provider: p.Provider,
38+
Maintainers: p.Maintainers,
39+
Links: p.Links,
40+
Keywords: p.Keywords,
3441
}
3542
if p.Icon != nil {
3643
mpkg.Icon = &model.Icon{

alpha/model/model.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"golang.org/x/exp/maps"
1515
"k8s.io/apimachinery/pkg/util/sets"
1616

17+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
18+
1719
"github.com/operator-framework/operator-registry/alpha/property"
1820
)
1921

@@ -50,6 +52,13 @@ type Package struct {
5052
DefaultChannel *Channel
5153
Channels map[string]*Channel
5254
Deprecation *Deprecation
55+
56+
DisplayName string
57+
ShortDescription string
58+
Provider *v1alpha1.AppLink
59+
Maintainers []v1alpha1.Maintainer
60+
Links []v1alpha1.AppLink
61+
Keywords []string
5362
}
5463

5564
func (m *Package) Validate() error {
@@ -59,6 +68,26 @@ func (m *Package) Validate() error {
5968
result.subErrors = append(result.subErrors, errors.New("package name must not be empty"))
6069
}
6170

71+
if len(m.ShortDescription) > 256 {
72+
result.subErrors = append(result.subErrors, fmt.Errorf("short description must not be more than 128 characters, found %d characters", len(m.ShortDescription)))
73+
}
74+
75+
for i, maintainer := range m.Maintainers {
76+
if maintainer.Name == "" && maintainer.Email == "" {
77+
result.subErrors = append(result.subErrors, fmt.Errorf("maintainer at index %d must not be empty", i))
78+
}
79+
}
80+
for i, link := range m.Links {
81+
if link.Name == "" && link.URL == "" {
82+
result.subErrors = append(result.subErrors, fmt.Errorf("link at index %d must not be empty", i))
83+
}
84+
}
85+
for i, keyword := range m.Keywords {
86+
if keyword == "" {
87+
result.subErrors = append(result.subErrors, fmt.Errorf("keyword at index %d must not be empty", i))
88+
}
89+
}
90+
6291
if err := m.Icon.Validate(); err != nil {
6392
result.subErrors = append(result.subErrors, err)
6493
}

pkg/api/model_to_api.go

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,7 @@ func ConvertModelBundleToAPIBundle(b model.Bundle) (*Bundle, error) {
2323

2424
csvJSON := b.CsvJSON
2525
if csvJSON == "" && len(props.CSVMetadatas) == 1 {
26-
var icons []v1alpha1.Icon
27-
if b.Package.Icon != nil {
28-
icons = []v1alpha1.Icon{{
29-
Data: base64.StdEncoding.EncodeToString(b.Package.Icon.Data),
30-
MediaType: b.Package.Icon.MediaType,
31-
}}
32-
}
33-
csv := csvMetadataToCsv(props.CSVMetadatas[0])
34-
csv.Name = b.Name
35-
csv.Spec.Icon = icons
36-
csv.Spec.InstallStrategy = v1alpha1.NamedInstallStrategy{
37-
// This stub is required to avoid a panic in OLM's package server that results in
38-
// attemptint to write to a nil map.
39-
StrategyName: "deployment",
40-
}
41-
csv.Spec.Version = version.OperatorVersion{Version: b.Version}
42-
csv.Spec.RelatedImages = convertModelRelatedImagesToCSVRelatedImages(b.RelatedImages)
43-
if csv.Spec.Description == "" {
44-
csv.Spec.Description = b.Package.Description
45-
}
26+
csv := newSyntheticCSV(b)
4627
csvData, err := json.Marshal(csv)
4728
if err != nil {
4829
return nil, err
@@ -101,29 +82,74 @@ func parseProperties(in []property.Property) (*property.Properties, error) {
10182
return props, nil
10283
}
10384

104-
func csvMetadataToCsv(m property.CSVMetadata) v1alpha1.ClusterServiceVersion {
85+
func newSyntheticCSV(b model.Bundle) v1alpha1.ClusterServiceVersion {
86+
pkg := b.Package
87+
csvMetadata := b.PropertiesP.CSVMetadatas[0]
88+
89+
var icons []v1alpha1.Icon
90+
if pkg.Icon != nil {
91+
icons = []v1alpha1.Icon{{
92+
Data: base64.StdEncoding.EncodeToString(pkg.Icon.Data),
93+
MediaType: pkg.Icon.MediaType,
94+
}}
95+
}
96+
97+
// Copy package-level metadata into CSV if fields are unset in CSV
98+
if csvMetadata.DisplayName == "" {
99+
csvMetadata.DisplayName = pkg.DisplayName
100+
}
101+
if _, ok := csvMetadata.Annotations["description"]; !ok {
102+
csvMetadata.Annotations["description"] = pkg.ShortDescription
103+
}
104+
if csvMetadata.Description == "" {
105+
csvMetadata.Description = pkg.Description
106+
}
107+
if csvMetadata.Provider.Name == "" && csvMetadata.Provider.URL == "" && pkg.Provider != nil {
108+
csvMetadata.Provider = *pkg.Provider
109+
}
110+
if csvMetadata.Maintainers == nil {
111+
csvMetadata.Maintainers = pkg.Maintainers
112+
}
113+
if csvMetadata.Links == nil {
114+
csvMetadata.Links = pkg.Links
115+
}
116+
if csvMetadata.Keywords == nil {
117+
csvMetadata.Keywords = pkg.Keywords
118+
}
119+
120+
// Return syntheticly generated CSV
105121
return v1alpha1.ClusterServiceVersion{
106122
TypeMeta: metav1.TypeMeta{
107123
Kind: operators.ClusterServiceVersionKind,
108124
APIVersion: v1alpha1.ClusterServiceVersionAPIVersion,
109125
},
110126
ObjectMeta: metav1.ObjectMeta{
111-
Annotations: m.Annotations,
112-
Labels: m.Labels,
127+
Name: b.Name,
128+
Annotations: csvMetadata.Annotations,
129+
Labels: csvMetadata.Labels,
113130
},
114131
Spec: v1alpha1.ClusterServiceVersionSpec{
115-
APIServiceDefinitions: m.APIServiceDefinitions,
116-
CustomResourceDefinitions: m.CustomResourceDefinitions,
117-
Description: m.Description,
118-
DisplayName: m.DisplayName,
119-
InstallModes: m.InstallModes,
120-
Keywords: m.Keywords,
121-
Links: m.Links,
122-
Maintainers: m.Maintainers,
123-
Maturity: m.Maturity,
124-
MinKubeVersion: m.MinKubeVersion,
125-
NativeAPIs: m.NativeAPIs,
126-
Provider: m.Provider,
132+
APIServiceDefinitions: csvMetadata.APIServiceDefinitions,
133+
CustomResourceDefinitions: csvMetadata.CustomResourceDefinitions,
134+
Description: csvMetadata.Description,
135+
DisplayName: csvMetadata.DisplayName,
136+
InstallModes: csvMetadata.InstallModes,
137+
Keywords: csvMetadata.Keywords,
138+
Links: csvMetadata.Links,
139+
Maintainers: csvMetadata.Maintainers,
140+
Maturity: csvMetadata.Maturity,
141+
MinKubeVersion: csvMetadata.MinKubeVersion,
142+
NativeAPIs: csvMetadata.NativeAPIs,
143+
Provider: csvMetadata.Provider,
144+
145+
Icon: icons,
146+
InstallStrategy: v1alpha1.NamedInstallStrategy{
147+
// This stub is required to avoid a panic in OLM's package server that results in
148+
// attemptint to write to a nil map.
149+
StrategyName: "deployment",
150+
},
151+
Version: version.OperatorVersion{Version: b.Version},
152+
RelatedImages: convertModelRelatedImagesToCSVRelatedImages(b.RelatedImages),
127153
},
128154
}
129155
}

0 commit comments

Comments
 (0)