Skip to content

Commit aa18b4b

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 92fa7be commit aa18b4b

File tree

6 files changed

+256
-72
lines changed

6 files changed

+256
-72
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
pkg.Provider = metadata.provider
69+
pkg.Maintainers = metadata.maintainers
70+
pkg.Links = metadata.links
71+
pkg.Keywords = slices.DeleteFunc(metadata.keywords, func(s string) bool {
72+
// Delete keywords that are empty strings
73+
return s == ""
74+
})
75+
}
76+
return nil
77+
}
78+
79+
func getCsvMetadata(b *declcfg.Bundle) (*property.CSVMetadata, int, error) {
80+
for i, p := range b.Properties {
81+
if p.Type != property.TypeCSVMetadata {
82+
continue
83+
}
84+
var csvMetadata property.CSVMetadata
85+
if err := json.Unmarshal(p.Value, &csvMetadata); err != nil {
86+
return nil, -1, err
87+
}
88+
return &csvMetadata, i, nil
89+
}
90+
return nil, -1, nil
91+
}
92+
93+
func compareRegistryV1Semver(a, b *semver.Version) int {
94+
if v := a.Compare(b); v != 0 {
95+
return v
96+
}
97+
aPre := semver.New(0, 0, 0, a.Metadata(), "")
98+
bPre := semver.New(0, 0, 0, b.Metadata(), "")
99+
return aPre.Compare(bPre)
100+
}
101+
102+
type promotedMetadata struct {
103+
version *semver.Version
104+
105+
displayName string
106+
shortDescription string
107+
provider v1alpha1.AppLink
108+
maintainers []v1alpha1.Maintainer
109+
links []v1alpha1.AppLink
110+
keywords []string
111+
}
112+
113+
func promotedCSVMetadata(version *semver.Version, metadata *property.CSVMetadata) promotedMetadata {
114+
return promotedMetadata{
115+
version: version,
116+
displayName: metadata.DisplayName,
117+
shortDescription: metadata.Annotations["description"],
118+
provider: metadata.Provider,
119+
maintainers: metadata.Maintainers,
120+
links: metadata.Links,
121+
keywords: metadata.Keywords,
122+
}
123+
}
124+
125+
func shortenDescription(input string) string {
126+
const maxLen = 256
127+
input = strings.TrimSpace(input)
128+
129+
// If the input is already under the limit return it.
130+
if utf8.RuneCountInString(input) <= maxLen {
131+
return input
132+
}
133+
134+
// Chop off everything after the first paragraph.
135+
if idx := strings.Index(input, "\n\n"); idx != -1 {
136+
input = strings.TrimSpace(input[:idx])
137+
}
138+
139+
// If we're _now_ under the limit, return the first paragraph.
140+
if utf8.RuneCountInString(input) <= maxLen {
141+
return input
142+
}
143+
144+
// If the first paragraph is still over the limit, we'll have to truncate.
145+
// We'll truncate at the last word boundary that still allows an ellipsis
146+
// to fit within the maximum length. But if there are no word boundaries
147+
// (veeeeery unlikely), we'll hard truncate mid-word.
148+
input = input[:maxLen-3]
149+
if truncatedIdx := strings.LastIndexFunc(input, unicode.IsSpace); truncatedIdx != -1 {
150+
return input[:truncatedIdx] + "..."
151+
}
152+
return input + "..."
153+
}

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/declcfg/declcfg.go

Lines changed: 8 additions & 6 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
)
@@ -36,12 +38,12 @@ type Package struct {
3638
Schema string `json:"schema"`
3739
Name string `json:"name"`
3840

39-
//DisplayName string `json:"displayName,omitempty"`
40-
//ShortDescription string `json:"shortDescription,omitempty"`
41-
//Provider v1alpha1.AppLink `json:"provider,omitempty"`
42-
//Maintainers []v1alpha1.Maintainer `json:"maintainers,omitempty"`
43-
//Links []v1alpha1.AppLink `json:"links,omitempty"`
44-
//Keywords []string `json:"keywords,omitempty"`
41+
DisplayName string `json:"displayName,omitempty"`
42+
ShortDescription string `json:"shortDescription,omitempty"`
43+
Provider v1alpha1.AppLink `json:"provider,omitempty"`
44+
Maintainers []v1alpha1.Maintainer `json:"maintainers,omitempty"`
45+
Links []v1alpha1.AppLink `json:"links,omitempty"`
46+
Keywords []string `json:"keywords,omitempty"`
4547

4648
DefaultChannel string `json:"defaultChannel"`
4749
Description string `json:"description,omitempty"`

alpha/declcfg/declcfg_to_model.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
3232
Description: p.Description,
3333
Channels: map[string]*model.Channel{},
3434

35-
//DisplayName: p.DisplayName,
36-
//ShortDescription: p.ShortDescription,
37-
//Provider: p.Provider,
38-
//Maintainers: p.Maintainers,
39-
//Links: p.Links,
40-
//Keywords: p.Keywords,
35+
DisplayName: p.DisplayName,
36+
ShortDescription: p.ShortDescription,
37+
Provider: p.Provider,
38+
Maintainers: p.Maintainers,
39+
Links: p.Links,
40+
Keywords: p.Keywords,
4141
}
4242
if p.Icon != nil {
4343
mpkg.Icon = &model.Icon{

alpha/model/model.go

Lines changed: 27 additions & 25 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

@@ -52,12 +54,12 @@ type Package struct {
5254
Channels map[string]*Channel
5355
Deprecation *Deprecation
5456

55-
//DisplayName string
56-
//ShortDescription string
57-
//Provider v1alpha1.AppLink
58-
//Maintainers []v1alpha1.Maintainer
59-
//Links []v1alpha1.AppLink
60-
//Keywords []string
57+
DisplayName string
58+
ShortDescription string
59+
Provider v1alpha1.AppLink
60+
Maintainers []v1alpha1.Maintainer
61+
Links []v1alpha1.AppLink
62+
Keywords []string
6163
}
6264

6365
func (m *Package) Validate() error {
@@ -67,25 +69,25 @@ func (m *Package) Validate() error {
6769
result.subErrors = append(result.subErrors, errors.New("package name must not be empty"))
6870
}
6971

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

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

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 == "" {
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)