diff --git a/alpha/action/migrations/001_v2.go b/alpha/action/migrations/001_v2.go new file mode 100644 index 000000000..09cdc4b1e --- /dev/null +++ b/alpha/action/migrations/001_v2.go @@ -0,0 +1,178 @@ +package migrations + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + + "github.com/Masterminds/semver/v3" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + fbcv2 "github.com/operator-framework/operator-registry/alpha/fbc/v2" + "github.com/operator-framework/operator-registry/alpha/property" +) + +func v2(cfg *declcfg.DeclarativeConfig) error { + slices.DeleteFunc(cfg.Packages, func(p declcfg.Package) bool { + pkgV2 := fbcv2.Package{ + Schema: "olm.package.v2", + Package: p.Name, + ShortDescription: ellipsesDescription(p.Description), + LongDescription: p.Description, + } + cfg.PackagesV2 = append(cfg.PackagesV2, pkgV2) + + if p.Icon != nil { + iconV2 := fbcv2.Icon{ + Schema: "olm.icon.v2", + Package: p.Name, + MediaType: p.Icon.MediaType, + Data: p.Icon.Data, + } + cfg.IconsV2 = append(cfg.IconsV2, iconV2) + } + return true + }) + + nameToVersion := map[string]semver.Version{} + var errs []error + slices.DeleteFunc(cfg.Bundles, func(b declcfg.Bundle) bool { + var ( + vers *semver.Version + err error + + properties = map[string]json.RawMessage{} + constraints = map[string]json.RawMessage{} + + rawListProperties = map[string][]json.RawMessage{} + rawListConstraints = map[string][]json.RawMessage{} + ) + + for _, p := range b.Properties { + switch p.Type { + case property.TypePackage: + var pkgProp property.Package + if err := json.Unmarshal(p.Value, &pkgProp); err != nil { + errs = append(errs, fmt.Errorf("could not migrate bundle %q: %v", b.Name, err)) + return true + } + vers, err = semver.NewVersion(pkgProp.Version) + if err != nil { + errs = append(errs, fmt.Errorf("could not migrate bundle %q: %v", b.Name, err)) + return true + } + case property.TypeCSVMetadata: + properties[p.Type] = p.Value + case property.TypeGVK, property.TypeBundleObject: + rawListProperties[p.Type] = append(rawListProperties[p.Type], p.Value) + case property.TypeGVKRequired, property.TypePackageRequired, property.TypeConstraint: + rawListConstraints[p.Type] = append(rawListConstraints[p.Type], p.Value) + default: + errs = append(errs, fmt.Errorf("could not migrate bundle %q: unknown property type %q cannot be translated to v2 properties format", b.Name, p.Type)) + return true + } + } + for k, v := range rawListProperties { + properties[k], _ = json.Marshal(v) + } + + for k, v := range rawListConstraints { + constraints[k], _ = json.Marshal(v) + } + + nameToVersion[b.Name] = *vers + + relatedURIs := sets.New[string]() + for _, r := range b.RelatedImages { + relatedURIs.Insert(fmt.Sprintf("docker://%s", r.Image)) + } + bundleV2 := fbcv2.Bundle{ + Schema: "olm.bundle.v2", + Package: b.Package, + Name: fmt.Sprintf("%s-%s-%d", b.Package, vers, 1), + Version: *vers, + Release: 1, + URI: fmt.Sprintf("docker://%s", b.Image), + RelatedURIs: sets.List(relatedURIs), + Properties: properties, + Constraints: constraints, + } + cfg.BundlesV2 = append(cfg.BundlesV2, bundleV2) + return true + }) + if len(errs) > 0 { + return fmt.Errorf("error migrating bundles: %v", errs) + } + + slices.DeleteFunc(cfg.Channels, func(c declcfg.Channel) bool { + entries := []fbcv2.ChannelEntry{} + for _, e := range c.Entries { + var upgradesFromBuilder strings.Builder + if e.Replaces != "" { + upgradesFromBuilder.WriteString(fmt.Sprintf("%s", nameToVersion[e.Replaces])) + } + for _, s := range e.Skips { + upgradesFromBuilder.WriteString(fmt.Sprintf(" %s", nameToVersion[s])) + } + if e.SkipRange != "" { + upgradesFromBuilder.WriteString(fmt.Sprintf(" %s", e.SkipRange)) + } + upgradesFromStr := upgradesFromBuilder.String() + + // If the original channel entry has no upgrade edges, make + // sure to explicitly configure it to an impossible range + // equivalent to "no upgrades". + if upgradesFromStr == "" { + upgradesFromStr = fmt.Sprintf("<0.0.0 >0.0.0") + } + upgradesFrom, err := semver.NewConstraint(upgradesFromStr) + if err != nil { + errs = append(errs, fmt.Errorf("count not migrate channel %q: %v", c.Name, err)) + return true + } + + entries = append(entries, fbcv2.ChannelEntry{ + Version: nameToVersion[e.Name], + UpgradesFrom: *upgradesFrom, + }) + } + + rawListProperties := map[string][]json.RawMessage{} + for _, p := range c.Properties { + rawListProperties[p.Type] = append(rawListProperties[p.Type], p.Value) + } + + properties := map[string]json.RawMessage{} + for k, v := range rawListProperties { + properties[k], _ = json.Marshal(v) + } + + channelV2 := fbcv2.Channel{ + Schema: "olm.channel.v2", + Package: c.Package, + Name: c.Name, + Entries: entries, + Properties: properties, + } + cfg.ChannelsV2 = append(cfg.ChannelsV2, channelV2) + return true + }) + if len(errs) > 0 { + return fmt.Errorf("error migrating channels: %v", errs) + } + + return nil +} + +func ellipsesDescription(s string) string { + if len(s) <= 60 { + return s + } + lastSpace := strings.LastIndex(s[:57], " ") + if lastSpace == -1 { + lastSpace = 57 + } + return s[:lastSpace] + "..." +} diff --git a/alpha/action/migrations/migrations.go b/alpha/action/migrations/migrations.go index 22ff86b74..d149af944 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", "migrates olm.package, olm.channel, and olm.bundle to their equivalent v2 APIs", v2), } func NewMigrations(name string) (*Migrations, error) { diff --git a/alpha/declcfg/declcfg.go b/alpha/declcfg/declcfg.go index 7797baa49..0da21950d 100644 --- a/alpha/declcfg/declcfg.go +++ b/alpha/declcfg/declcfg.go @@ -6,13 +6,13 @@ 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" + fbcv2 "github.com/operator-framework/operator-registry/alpha/fbc/v2" "github.com/operator-framework/operator-registry/alpha/property" + prettyunmarshaler "github.com/operator-framework/operator-registry/pkg/prettyunmarshaler" ) const ( @@ -28,6 +28,11 @@ type DeclarativeConfig struct { Bundles []Bundle Deprecations []Deprecation Others []Meta + + PackagesV2 []fbcv2.Package + ChannelsV2 []fbcv2.Channel + BundlesV2 []fbcv2.Bundle + IconsV2 []fbcv2.Icon } type Package struct { @@ -201,6 +206,10 @@ func extractUniqueMetaKeys(blobMap map[string]any, m *Meta) error { } func (destination *DeclarativeConfig) Merge(src *DeclarativeConfig) { + destination.PackagesV2 = append(destination.PackagesV2, src.PackagesV2...) + destination.ChannelsV2 = append(destination.ChannelsV2, src.ChannelsV2...) + destination.BundlesV2 = append(destination.BundlesV2, src.BundlesV2...) + destination.Packages = append(destination.Packages, src.Packages...) destination.Channels = append(destination.Channels, src.Channels...) destination.Bundles = append(destination.Bundles, src.Bundles...) diff --git a/alpha/declcfg/write.go b/alpha/declcfg/write.go index 9856c2e1e..c2b4145a3 100644 --- a/alpha/declcfg/write.go +++ b/alpha/declcfg/write.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/yaml" + fbcv2 "github.com/operator-framework/operator-registry/alpha/fbc/v2" "github.com/operator-framework/operator-registry/alpha/property" ) @@ -395,8 +396,27 @@ type encoder interface { } func writeToEncoder(cfg DeclarativeConfig, enc encoder) error { - pkgNames := sets.NewString() + pkgV2Names := sets.NewString() + packagesV2ByName := map[string][]fbcv2.Package{} + for _, p := range cfg.PackagesV2 { + pkgName := p.Package + pkgV2Names.Insert(pkgName) + packagesV2ByName[pkgName] = append(packagesV2ByName[pkgName], p) + } + channelsV2ByPackage := map[string][]fbcv2.Channel{} + for _, c := range cfg.ChannelsV2 { + pkgName := c.Package + pkgV2Names.Insert(pkgName) + channelsV2ByPackage[pkgName] = append(channelsV2ByPackage[pkgName], c) + } + bundlesV2ByPackage := map[string][]fbcv2.Bundle{} + for _, b := range cfg.BundlesV2 { + pkgName := b.Package + pkgV2Names.Insert(pkgName) + bundlesV2ByPackage[pkgName] = append(bundlesV2ByPackage[pkgName], b) + } + pkgNames := sets.NewString() packagesByName := map[string][]Package{} for _, p := range cfg.Packages { pkgName := p.Name @@ -428,6 +448,38 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error { deprecationsByPackage[pkgName] = append(deprecationsByPackage[pkgName], d) } + for _, pName := range pkgV2Names.List() { + if len(pName) == 0 { + continue + } + pkgs := packagesV2ByName[pName] + for _, p := range pkgs { + if err := enc.Encode(p); err != nil { + return err + } + } + + channels := channelsV2ByPackage[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 { + return err + } + } + + bundles := bundlesV2ByPackage[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 + } + } + } + for _, pName := range pkgNames.List() { if len(pName) == 0 { continue @@ -499,6 +551,15 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error { type WriteFunc func(config DeclarativeConfig, w io.Writer) error func WriteFS(cfg DeclarativeConfig, rootDir string, writeFunc WriteFunc, fileExt string) error { + channelsV2ByPackage := map[string][]fbcv2.Channel{} + for _, c := range cfg.ChannelsV2 { + channelsV2ByPackage[c.Package] = append(channelsV2ByPackage[c.Package], c) + } + bundlesV2ByPackage := map[string][]fbcv2.Bundle{} + for _, b := range cfg.BundlesV2 { + bundlesV2ByPackage[b.Package] = append(bundlesV2ByPackage[b.Package], b) + } + channelsByPackage := map[string][]Channel{} for _, c := range cfg.Channels { channelsByPackage[c.Package] = append(channelsByPackage[c.Package], c) @@ -512,6 +573,22 @@ func WriteFS(cfg DeclarativeConfig, rootDir string, writeFunc WriteFunc, fileExt return err } + for _, p := range cfg.PackagesV2 { + fcfg := DeclarativeConfig{ + PackagesV2: []fbcv2.Package{p}, + ChannelsV2: channelsV2ByPackage[p.Package], + BundlesV2: bundlesV2ByPackage[p.Package], + } + pkgDir := filepath.Join(rootDir, p.Package) + if err := os.MkdirAll(pkgDir, 0777); err != nil { + return err + } + filename := filepath.Join(pkgDir, fmt.Sprintf("catalog.v2%s", fileExt)) + if err := writeFile(fcfg, filename, writeFunc); err != nil { + return err + } + } + for _, p := range cfg.Packages { fcfg := DeclarativeConfig{ Packages: []Package{p}, diff --git a/alpha/fbc/v2/bundle.go b/alpha/fbc/v2/bundle.go new file mode 100644 index 000000000..99c74dae6 --- /dev/null +++ b/alpha/fbc/v2/bundle.go @@ -0,0 +1,51 @@ +package v2 + +import ( + "encoding/json" + + "github.com/Masterminds/semver/v3" +) + +type Bundle struct { + // Schema is the FBC schema version of the bundle. Always "olm.bundle.v2". + Schema string `json:"schema"` + + // Package is the name of the package to which this bundle belongs. + Package string `json:"package"` + + // Name is the name of the bundle. It is required to be in the format of + // "package-version-release", and mnust be unique within a catalog. + Name string `json:"name"` + + // Version is the version of the software packaged in this bundle. + Version semver.Version `json:"version"` + + // Release is the number of times the Version of this bundle has been + // released. + Release uint32 `json:"release"` + + // URI is the location of the bundle. + URI string `json:"uri"` + + // RelatedURIs is a list of related URIs that are associated with the + // bundle. URIs should be included here if they are referenced or used by + // the bundle. + RelatedURIs []string `json:"relatedURIs,omitempty"` + + // Annotations is a map of string keys to string values. Annotations are + // used to store simple arbitrary metadata about the bundle. + Annotations map[string]string `json:"annotations,omitempty"` + + // Properties is a map of string keys to arbitrary JSON-encoded values. + // Properties are used to store complex metadata about the bundle. A + // property's "type" key is used to determine how to interpret the + // JSON-encoded value. Unrecognized properties MUST be ignored. + Properties map[string]json.RawMessage `json:"properties,omitempty"` + + // Constraints is a map of string keys to arbitrary JSON-encoded values. + // Constraints are used to store complex constraints that the bundle + // requires. A constraint's "type" key is used to determine how to + // interpret the JSON-encoded value. Unrecognized constraints MUST be + // treated as unsatisfiable. + Constraints map[string]json.RawMessage `json:"constraints,omitempty"` +} diff --git a/alpha/fbc/v2/channel.go b/alpha/fbc/v2/channel.go new file mode 100644 index 000000000..29a9ef500 --- /dev/null +++ b/alpha/fbc/v2/channel.go @@ -0,0 +1,82 @@ +package v2 + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" +) + +type Channel struct { + // Schema is the FBC schema version of the channel. Always "olm.channel.v2". + Schema string `json:"schema"` + + // Package is the name of the package to which this channel belongs. + Package string `json:"package"` + + // Name is the name of the channel. It must be unique within a package. + Name string `json:"name"` + + // Entries is a list of ChannelEntry objects that describe the bundles in + // the channel. + Entries []ChannelEntry `json:"entries"` + + // Annotations is a map of string keys to string values. Annotations are + // used to store simple arbitrary metadata about the channel. + Annotations map[string]string `json:"annotations,omitempty"` + + // Properties is a map of string keys to arbitrary JSON-encoded values. + // Properties are used to store complex metadata. A property's "type" key + // is used to determine how to interpret the JSON-encoded value. + Properties map[string]json.RawMessage `json:"properties,omitempty"` +} + +type ChannelEntry struct { + // Version is the version of the bundle to be included in the channel. + Version semver.Version `json:"version"` + + // UpgradesFrom is an optional constraint that specifies the range of + // versions that can be upgraded to Version in the channel. If not + // specified, semver semantics apply. That is to say the constraint + // will be " >=X.0.0 =%s <%s", minVersionFrom(eu.Version), eu.Version) + } + + cf, err := semver.NewConstraint(eu.UpgradesFrom) + if err != nil { + return err + } + c.UpgradesFrom = *cf + return nil +} + +func minVersionFrom(v semver.Version) semver.Version { + // 1.2.3 -> 1.0.0 + // 1.0.0 -> 1.0.0 + // 0.2.3 -> 0.2.0 + // 0.2.0 -> 0.2.0 + // 0.0.3 -> 0.0.3 + if v.Major() == 0 { + if v.Minor() == 0 { + return v + } + return *semver.New(0, v.Minor(), 0, "", "") + } + return *semver.New(v.Major(), 0, 0, "", "") +} diff --git a/alpha/fbc/v2/icon.go b/alpha/fbc/v2/icon.go new file mode 100644 index 000000000..03f92d699 --- /dev/null +++ b/alpha/fbc/v2/icon.go @@ -0,0 +1,15 @@ +package v2 + +type Icon struct { + // Schema is the FBC schema version of the icon. Always "olm.icon.v2". + Schema string `json:"schema"` + + // Package is the name of the package to which this icon belongs. + Package string `json:"package"` + + // MediaType is the media type of the icon. + MediaType string `json:"mediaType"` + + // Data is the icon data + Data []byte `json:"data"` +} diff --git a/alpha/fbc/v2/package.go b/alpha/fbc/v2/package.go new file mode 100644 index 000000000..f6b737767 --- /dev/null +++ b/alpha/fbc/v2/package.go @@ -0,0 +1,29 @@ +package v2 + +import ( + "encoding/json" +) + +type Package struct { + // Schema is the FBC schema version of the package. Always "olm.package.v2". + Schema string `json:"schema"` + + // Package is the name of this package. + Package string `json:"package"` + + // ShortDescription is a short description of the package. + ShortDescription string `json:"shortDescription,omitempty"` + + // LongDescription is a long description of the package. + LongDescription string `json:"longDescription,omitempty"` + + // Annotations is a map of string keys to string values. Annotations are + // used to store simple arbitrary metadata about the package. + Annotations map[string]string `json:"annotations,omitempty"` + + // Properties is a map of string keys to arbitrary JSON-encoded values. + // Properties are used to store complex metadata about the package. A + // property's "type" key is used to determine how to interpret the + // JSON-encoded value. Unrecognized properties MUST be ignored. + Properties map[string]json.RawMessage `json:"properties,omitempty"` +} diff --git a/go.mod b/go.mod index 89a8e61c0..923bae756 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/operator-framework/operator-registry go 1.22.5 require ( + github.com/Masterminds/semver/v3 v3.3.0 github.com/akrylysov/pogreb v0.10.2 github.com/blang/semver/v4 v4.0.0 github.com/containerd/containerd v1.7.22 diff --git a/go.sum b/go.sum index 77cb76c7e..a5fc08d03 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=