From 42ea9e031f10848f7ac58cfddc8614d210fee468 Mon Sep 17 00:00:00 2001 From: Khaled Janania Date: Fri, 23 Jul 2021 15:42:14 -0400 Subject: [PATCH 01/10] Initial pass at pruning operators by version Signed-off-by: Khaled Janania --- .gitignore | 2 + cmd/opm/index/cmd.go | 1 + cmd/opm/index/pruneversion.go | 126 +++++++++++++++++++++++++++++++++ pkg/lib/indexer/indexer.go | 58 +++++++++++++++ pkg/lib/indexer/interfaces.go | 1 + pkg/lib/registry/interfaces.go | 1 + pkg/lib/registry/registry.go | 86 ++++++++++++++++++++++ pkg/registry/interface.go | 1 + pkg/sqlite/load.go | 55 ++++++++++++++ pkg/sqlite/remove.go | 39 ++++++++++ 10 files changed, 370 insertions(+) create mode 100644 cmd/opm/index/pruneversion.go diff --git a/.gitignore b/.gitignore index 0dc911b82..e16f84274 100644 --- a/.gitignore +++ b/.gitignore @@ -472,3 +472,5 @@ test/e2e/index_tmp* # don't check in the certs directory certs/* + +index_tmp_* diff --git a/cmd/opm/index/cmd.go b/cmd/opm/index/cmd.go index c555a7253..e921a0d88 100644 --- a/cmd/opm/index/cmd.go +++ b/cmd/opm/index/cmd.go @@ -31,6 +31,7 @@ func AddCommand(parent *cobra.Command) { addIndexAddCmd(cmd) cmd.AddCommand(newIndexExportCmd()) cmd.AddCommand(newIndexPruneCmd()) + cmd.AddCommand(newIndexPruneVersionCmd()) cmd.AddCommand(newIndexDeprecateTruncateCmd()) cmd.AddCommand(newIndexPruneStrandedCmd()) } diff --git a/cmd/opm/index/pruneversion.go b/cmd/opm/index/pruneversion.go new file mode 100644 index 000000000..7a302f9ff --- /dev/null +++ b/cmd/opm/index/pruneversion.go @@ -0,0 +1,126 @@ +package index + +import ( + "fmt" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/operator-framework/operator-registry/pkg/containertools" + "github.com/operator-framework/operator-registry/pkg/lib/indexer" +) + +func newIndexPruneVersionCmd() *cobra.Command { + indexCmd := &cobra.Command{ + Use: "prune-version", + Short: "prune an index of all but specified package versions", + Long: `prune an index of all but specified package versions`, + + PreRunE: func(cmd *cobra.Command, args []string) error { + if debug, _ := cmd.Flags().GetBool("debug"); debug { + logrus.SetLevel(logrus.DebugLevel) + } + return nil + }, + + RunE: runIndexPruneVersionCmdFunc, + } + + indexCmd.Flags().Bool("debug", false, "enable debug logging") + indexCmd.Flags().Bool("generate", false, "if enabled, just creates the dockerfile and saves it to local disk") + indexCmd.Flags().StringP("out-dockerfile", "d", "", "if generating the dockerfile, this flag is used to (optionally) specify a dockerfile name") + indexCmd.Flags().StringP("from-index", "f", "", "index to prune") + if err := indexCmd.MarkFlagRequired("from-index"); err != nil { + logrus.Panic("Failed to set required `from-index` flag for `index prune`") + } + indexCmd.Flags().StringSliceP("package-versions", "p", nil, "comma separated list of package and versions to keep") + if err := indexCmd.MarkFlagRequired("package-versions"); err != nil { + logrus.Panic("Failed to set required `package-versions` flag for `index pruneversion`") + } + indexCmd.Flags().StringP("binary-image", "i", "", "container image for on-image `opm` command") + indexCmd.Flags().StringP("container-tool", "c", "podman", "tool to interact with container images (save, build, etc.). One of: [docker, podman]") + indexCmd.Flags().StringP("tag", "t", "", "custom tag for container image being built") + indexCmd.Flags().Bool("permissive", false, "allow registry load errors") + + if err := indexCmd.Flags().MarkHidden("debug"); err != nil { + logrus.Panic(err.Error()) + } + + return indexCmd + +} + +func runIndexPruneVersionCmdFunc(cmd *cobra.Command, args []string) error { + generate, err := cmd.Flags().GetBool("generate") + if err != nil { + return err + } + + outDockerfile, err := cmd.Flags().GetString("out-dockerfile") + if err != nil { + return err + } + + fromIndex, err := cmd.Flags().GetString("from-index") + if err != nil { + return err + } + + packageVersions, err := cmd.Flags().GetStringSlice("package-versions") + if err != nil { + return err + } + + binaryImage, err := cmd.Flags().GetString("binary-image") + if err != nil { + return err + } + + containerTool, err := cmd.Flags().GetString("container-tool") + if err != nil { + return err + } + + if containerTool == "none" { + return fmt.Errorf("none is not a valid container-tool for index prune") + } + + tag, err := cmd.Flags().GetString("tag") + if err != nil { + return err + } + + permissive, err := cmd.Flags().GetBool("permissive") + if err != nil { + return err + } + + skipTLS, err := cmd.Flags().GetBool("skip-tls") + if err != nil { + return err + } + + logger := logrus.WithFields(logrus.Fields{"package-versions": packageVersions}) + + logger.Info("pruning the index") + + indexPruner := indexer.NewIndexPruner(containertools.NewContainerTool(containerTool, containertools.PodmanTool), logger) + + request := indexer.PruneVersionFromIndexRequest{ + Generate: generate, + FromIndex: fromIndex, + BinarySourceImage: binaryImage, + OutDockerfile: outDockerfile, + PackageVersions: packageVersions, + Tag: tag, + Permissive: permissive, + SkipTLS: skipTLS, + } + + err = indexPruner.PruneVersionFromIndex(request) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/lib/indexer/indexer.go b/pkg/lib/indexer/indexer.go index 1f8cf22f1..4a863c4d5 100644 --- a/pkg/lib/indexer/indexer.go +++ b/pkg/lib/indexer/indexer.go @@ -293,6 +293,64 @@ func (i ImageIndexer) PruneFromIndex(request PruneFromIndexRequest) error { return nil } +// PruneFromIndexRequest defines the parameters to send to the PruneFromIndex API +type PruneVersionFromIndexRequest struct { + Generate bool + Permissive bool + BinarySourceImage string + FromIndex string + OutDockerfile string + Tag string + PackageVersions []string + CaFile string + SkipTLS bool +} + +func (i ImageIndexer) PruneVersionFromIndex(request PruneVersionFromIndexRequest) error { + buildDir, outDockerfile, cleanup, err := buildContext(request.Generate, request.OutDockerfile) + defer cleanup() + if err != nil { + return err + } + + databasePath, err := i.ExtractDatabase(buildDir, request.FromIndex, request.CaFile, request.SkipTLS) + if err != nil { + return err + } + + // Run opm registry prune on the database + pruneFromRegistryReq := registry.PruneVersionFromRegistryRequest{ + PackageVersions: request.PackageVersions, + InputDatabase: databasePath, + Permissive: request.Permissive, + } + + // Prune the bundles from the registry + err = i.RegistryPruner.PruneVersionFromRegistry(pruneFromRegistryReq) + if err != nil { + return err + } + + // generate the dockerfile + dockerfile := i.DockerfileGenerator.GenerateIndexDockerfile(request.BinarySourceImage, databasePath) + err = write(dockerfile, outDockerfile, i.Logger) + if err != nil { + return err + } + + if request.Generate { + return nil + } + + // build the dockerfile + err = build(outDockerfile, request.Tag, i.CommandRunner, i.Logger) + if err != nil { + return err + } + + return nil +} + // ExtractDatabase sets a temp directory for unpacking an image func (i ImageIndexer) ExtractDatabase(buildDir, fromIndex, caFile string, skipTLS bool) (string, error) { tmpDir, err := ioutil.TempDir("./", tmpDirPrefix) diff --git a/pkg/lib/indexer/interfaces.go b/pkg/lib/indexer/interfaces.go index df9e6e47a..ba942847a 100644 --- a/pkg/lib/indexer/interfaces.go +++ b/pkg/lib/indexer/interfaces.go @@ -84,6 +84,7 @@ func NewIndexStrandedPruner(containerTool containertools.ContainerTool, logger * // IndexPruner prunes operators out of an index type IndexPruner interface { PruneFromIndex(PruneFromIndexRequest) error + PruneVersionFromIndex(PruneVersionFromIndexRequest) error } func NewIndexPruner(containerTool containertools.ContainerTool, logger *logrus.Entry) IndexPruner { diff --git a/pkg/lib/registry/interfaces.go b/pkg/lib/registry/interfaces.go index f392d16e3..b33ccc94f 100644 --- a/pkg/lib/registry/interfaces.go +++ b/pkg/lib/registry/interfaces.go @@ -39,6 +39,7 @@ func NewRegistryStrandedPruner(logger *logrus.Entry) RegistryStrandedPruner { type RegistryPruner interface { PruneFromRegistry(PruneFromRegistryRequest) error + PruneVersionFromRegistry(PruneVersionFromRegistryRequest) error } func NewRegistryPruner(logger *logrus.Entry) RegistryPruner { diff --git a/pkg/lib/registry/registry.go b/pkg/lib/registry/registry.go index 4232e07b0..3270e64de 100644 --- a/pkg/lib/registry/registry.go +++ b/pkg/lib/registry/registry.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "github.com/sirupsen/logrus" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -321,6 +322,91 @@ func (r RegistryUpdater) PruneFromRegistry(request PruneFromRegistryRequest) err return nil } +type PruneVersionFromRegistryRequest struct { + Permissive bool + InputDatabase string + PackageVersions []string +} + +func (r RegistryUpdater) PruneVersionFromRegistry(request PruneVersionFromRegistryRequest) error { + // First we'll prune the packages + operatorVerMap := make(map[string][]string) + for _, pkgVersion := range request.PackageVersions { + split := strings.Split(pkgVersion, ":") + operatorVerMap[split[0]] = append(operatorVerMap[split[0]], split[1]) + } + packageList := make([]string, 0, len(operatorVerMap)) + for operatorName := range operatorVerMap { + packageList = append(packageList, operatorName) + } + + logrus.Info(fmt.Sprintf("Keeping %s", packageList)) + + prunePackageReq := PruneFromRegistryRequest{ + Permissive: request.Permissive, + InputDatabase: request.InputDatabase, + Packages: packageList, + } + r.PruneFromRegistry(prunePackageReq) + + // Now we go delete the versions we don't want + db, err := sqlite.Open(request.InputDatabase) + if err != nil { + return err + } + defer db.Close() + + dbLoader, err := sqlite.NewSQLLiteLoader(db) + if err != nil { + return err + } + if err := dbLoader.Migrate(context.TODO()); err != nil { + return err + } + + // get all the packages + lister := sqlite.NewSQLLiteQuerierFromDb(db) + if err != nil { + return err + } + + // make it inexpensive to find packages + pkgMap := make(map[string]string) + for _, pkgVersion := range request.PackageVersions { + split := strings.Split(pkgVersion, ":") + pkgMap[split[0]] = split[1] + } + + // prune packages from registry + for operatorName, versionList := range operatorVerMap { + operatorBundleVersions := make(map[string]bool) + for _, version := range versionList { + operatorBundleVersions[version] = true + } + bundlesForPackage, err := lister.GetBundlesForPackage(context.TODO(), operatorName) + if err != nil { + return err + } + for bundleForPackage, _ := range bundlesForPackage { + // Check our map to see if the bundle we found is in the list of bundles we want to keep + if _, found := operatorBundleVersions[bundleForPackage.Version]; !found { + // if not, then we delete that bundle + remover := sqlite.NewSQLRemoverForOperatorCsvNames(dbLoader, operatorName, bundleForPackage.Version) + if err := remover.Remove(); err != nil { + err = fmt.Errorf("error deleting bundles by operator csv name from database: %s", err) + if !request.Permissive { + logrus.WithError(err).Fatal("permissive mode disabled") + return err + } + logrus.WithError(err).Warn("permissive mode enabled") + } + } + } + } + + return nil +} + type DeprecateFromRegistryRequest struct { Permissive bool InputDatabase string diff --git a/pkg/registry/interface.go b/pkg/registry/interface.go index fd7763611..ced708cdd 100644 --- a/pkg/registry/interface.go +++ b/pkg/registry/interface.go @@ -12,6 +12,7 @@ type Load interface { AddPackageChannels(manifest PackageManifest) error AddBundlePackageChannels(manifest PackageManifest, bundle *Bundle) error RemovePackage(packageName string) error + RemoveBundleByVersion(packageName string, operatorVersion string) error RemoveStrandedBundles() error DeprecateBundle(path string) error ClearNonHeadBundles() error diff --git a/pkg/sqlite/load.go b/pkg/sqlite/load.go index cfb3b7015..289bba125 100644 --- a/pkg/sqlite/load.go +++ b/pkg/sqlite/load.go @@ -924,6 +924,35 @@ func (s *sqlLoader) getCSVNames(tx *sql.Tx, packageName string) ([]string, error return csvNames, nil } +func (s *sqlLoader) getCSVName(tx *sql.Tx, packageName string, version string) (string, error) { + getID, err := tx.Prepare(`SELECT DISTINCT operatorbundle.name FROM operatorbundle + INNER JOIN channel_entry ON operatorbundle.name=channel_entry.operatorbundle_name + WHERE channel_entry.package_name=? AND operatorbundle.version=?`) + // getID, err := tx.Prepare(` + // SELECT DISTINCT channel_entry.operatorbundle_name + // FROM channel_entry + // WHERE channel_entry.package_name=?`) + + if err != nil { + return "", err + } + defer getID.Close() + rows, err := getID.Query(packageName) + + var csvName sql.NullString + if rows.Next() { + if err := rows.Scan(&csvName); err != nil { + return "", err + } + } + + if err := rows.Close(); err != nil { + return "", err + } + + return csvName.String, nil +} + func (s *sqlLoader) RemovePackage(packageName string) error { if err := func() error { tx, err := s.db.Begin() @@ -972,6 +1001,32 @@ func (s *sqlLoader) RemovePackage(packageName string) error { return s.RemoveStrandedBundles() } +func (s *sqlLoader) RemoveBundleByVersion(operatorName string, operatorVersion string) error { + if err := func() error { + tx, err := s.db.Begin() + if err != nil { + return err + } + defer func() { + tx.Rollback() + }() + csvName, err := s.getCSVName(tx, operatorName, operatorVersion) + if err != nil { + return err + } + + if err := s.rmBundle(tx, csvName); err != nil { + return err + } + + return tx.Commit() + }(); err != nil { + return err + } + + return s.RemoveStrandedBundles() +} + func (s *sqlLoader) rmBundle(tx *sql.Tx, csvName string) error { deleteBundle, err := tx.Prepare("DELETE FROM operatorbundle WHERE operatorbundle.name=?") if err != nil { diff --git a/pkg/sqlite/remove.go b/pkg/sqlite/remove.go index 1d2a2cb4d..5a5627a07 100644 --- a/pkg/sqlite/remove.go +++ b/pkg/sqlite/remove.go @@ -14,13 +14,24 @@ type SQLRemover interface { Remove() error } +type SQLOperatorCsvNamesRemover interface { + Remove() error +} + // PackageRemover removes a package from the database type PackageRemover struct { store registry.Load packages string } +type OperatorPackageVersionRemover struct { + store registry.Load + packageName string + version string +} + var _ SQLRemover = &PackageRemover{} +var _ SQLOperatorCsvNamesRemover = &OperatorPackageVersionRemover{} func NewSQLRemoverForPackages(store registry.Load, packages string) *PackageRemover { return &PackageRemover{ @@ -29,6 +40,14 @@ func NewSQLRemoverForPackages(store registry.Load, packages string) *PackageRemo } } +func NewSQLRemoverForOperatorCsvNames(store registry.Load, packageName string, version string) *OperatorPackageVersionRemover { + return &OperatorPackageVersionRemover{ + store: store, + packageName: packageName, + version: version, + } +} + func (d *PackageRemover) Remove() error { log := logrus.WithField("pkg", d.packages) @@ -47,6 +66,26 @@ func (d *PackageRemover) Remove() error { return utilerrors.NewAggregate(errs) } +func (d *OperatorPackageVersionRemover) Remove() error { + fields := logrus.Fields{ + "package": d.packageName, + "version": d.version, + } + log := logrus.WithFields(fields) + + log.Info("deleting packages") + + var errs []error + // packages := sanitizePackageList(strings.Split(d.operatorVersions, ",")) + log.Infof("operator package and version: %s %s", d.packageName, d.version) + + if err := d.store.RemoveBundleByVersion(d.packageName, d.version); err != nil { + errs = append(errs, fmt.Errorf("error removing operator bundle %s %s: %s", d.packageName, d.version, err)) + } + + return utilerrors.NewAggregate(errs) +} + // sanitizePackageList sanitizes the set of package(s) specified. It removes // duplicates and ignores empty string. func sanitizePackageList(in []string) []string { From b35975569c170e578b92ae67797c12a94bfd36a0 Mon Sep 17 00:00:00 2001 From: Khaled Janania Date: Mon, 26 Jul 2021 17:59:11 -0400 Subject: [PATCH 02/10] replacing the head for the channel if necessary Signed-off-by: Khaled Janania --- cmd/opm/index/pruneversion.go | 7 +-- pkg/lib/registry/registry.go | 83 ++++++++++++++++++++++++++++++----- pkg/registry/interface.go | 2 +- pkg/sqlite/load.go | 54 +++++++++++++++++++---- pkg/sqlite/remove.go | 24 +++++----- 5 files changed, 135 insertions(+), 35 deletions(-) diff --git a/cmd/opm/index/pruneversion.go b/cmd/opm/index/pruneversion.go index 7a302f9ff..31eb80383 100644 --- a/cmd/opm/index/pruneversion.go +++ b/cmd/opm/index/pruneversion.go @@ -12,9 +12,10 @@ import ( func newIndexPruneVersionCmd() *cobra.Command { indexCmd := &cobra.Command{ - Use: "prune-version", - Short: "prune an index of all but specified package versions", - Long: `prune an index of all but specified package versions`, + Hidden: true, + Use: "prune-version", + Short: "prune an index of all but specified package versions", + Long: `prune an index of all but specified package versions`, PreRunE: func(cmd *cobra.Command, args []string) error { if debug, _ := cmd.Flags().GetBool("debug"); debug { diff --git a/pkg/lib/registry/registry.go b/pkg/lib/registry/registry.go index 3270e64de..8eb0c1ab5 100644 --- a/pkg/lib/registry/registry.go +++ b/pkg/lib/registry/registry.go @@ -5,9 +5,13 @@ import ( "fmt" "io/ioutil" "os" + "sort" "strings" + // "github.com/blang/semver" + // "github.com/blang/semver/v4" "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" utilerrors "k8s.io/apimachinery/pkg/util/errors" "github.com/operator-framework/operator-registry/pkg/containertools" @@ -330,11 +334,19 @@ type PruneVersionFromRegistryRequest struct { func (r RegistryUpdater) PruneVersionFromRegistry(request PruneVersionFromRegistryRequest) error { // First we'll prune the packages + // Create a map of the operator and versions we want to keep operatorVerMap := make(map[string][]string) for _, pkgVersion := range request.PackageVersions { split := strings.Split(pkgVersion, ":") operatorVerMap[split[0]] = append(operatorVerMap[split[0]], split[1]) } + + // now we sort those lists of versions for later (might only contain one version each) + for _, versionList := range operatorVerMap { + sort.Slice(versionList, func(i, j int) bool { + return semver.Compare(versionList[i], versionList[j]) < 0 + }) + } packageList := make([]string, 0, len(operatorVerMap)) for operatorName := range operatorVerMap { packageList = append(packageList, operatorName) @@ -370,28 +382,31 @@ func (r RegistryUpdater) PruneVersionFromRegistry(request PruneVersionFromRegist return err } - // make it inexpensive to find packages - pkgMap := make(map[string]string) - for _, pkgVersion := range request.PackageVersions { - split := strings.Split(pkgVersion, ":") - pkgMap[split[0]] = split[1] - } - // prune packages from registry for operatorName, versionList := range operatorVerMap { operatorBundleVersions := make(map[string]bool) for _, version := range versionList { operatorBundleVersions[version] = true } - bundlesForPackage, err := lister.GetBundlesForPackage(context.TODO(), operatorName) + // bundlesForPackage, err := lister.GetBundlesForPackage(context.TODO(), operatorName) + channelEntriesForPackage, err := lister.GetChannelEntriesFromPackage(context.TODO(), operatorName) if err != nil { return err } - for bundleForPackage, _ := range bundlesForPackage { + + for _, channelEntryForPackage := range channelEntriesForPackage { + // Find the newest of the package version for this channel (otherwise we lose everything if we delete) + // the head bundle + channel := channelEntryForPackage.ChannelName + bundleToSave := findNewestVersionToSave(channelEntriesForPackage, operatorVerMap[channelEntryForPackage.PackageName], channel) + if err != nil { + return err + } + // Check our map to see if the bundle we found is in the list of bundles we want to keep - if _, found := operatorBundleVersions[bundleForPackage.Version]; !found { + if _, found := operatorBundleVersions[channelEntryForPackage.Version]; !found { // if not, then we delete that bundle - remover := sqlite.NewSQLRemoverForOperatorCsvNames(dbLoader, operatorName, bundleForPackage.Version) + remover := sqlite.NewSQLRemoverForOperatorCsvNames(dbLoader, channelEntryForPackage.BundleName, bundleToSave) if err := remover.Remove(); err != nil { err = fmt.Errorf("error deleting bundles by operator csv name from database: %s", err) if !request.Permissive { @@ -413,6 +428,52 @@ type DeprecateFromRegistryRequest struct { Bundles []string } +func findNewestVersionToSave(channelEntries []registry.ChannelEntryAnnotated, operatorVerList []string, channelName string) *string { + // filter the channel entries for the specific channel name + filteredChannelEntries := []registry.ChannelEntryAnnotated{} + for _, channelEntryForPackage := range channelEntries { + if channelEntryForPackage.ChannelName == channelName { + filteredChannelEntries = append(filteredChannelEntries, channelEntryForPackage) + } + } + + sort.Slice(filteredChannelEntries, func(i, j int) bool { + return semver.Compare(filteredChannelEntries[i].Version, filteredChannelEntries[j].Version) < 0 + }) + + // Find all the versions that the user requested that are also in this channel + filteredOperatorVerList := []string{} + // this probably could be improved + for _, operatorVer := range operatorVerList { + for _, channelEntry := range filteredChannelEntries { + if semver.Compare(operatorVer, channelEntry.Version) == 0 { + filteredOperatorVerList = append(filteredOperatorVerList, operatorVer) + } + } + } + + // if the list is empty, then we didn't find any that matched this channel + if len(filteredOperatorVerList) == 0 { + return nil + } + + // now sort it to get the highest version we want to save for this channel + sort.Slice(filteredOperatorVerList, func(i, j int) bool { + return semver.Compare(filteredOperatorVerList[i], filteredOperatorVerList[j]) < 0 + }) + + highestVersion := filteredOperatorVerList[len(filteredOperatorVerList)-1] + + for _, i := range filteredChannelEntries { + if i.Version == highestVersion { + return &i.BundleName + } + } + + // If we get here, there's no version we could find in this channel that we'd want to save + return nil +} + func (r RegistryUpdater) DeprecateFromRegistry(request DeprecateFromRegistryRequest) error { db, err := sqlite.Open(request.InputDatabase) if err != nil { diff --git a/pkg/registry/interface.go b/pkg/registry/interface.go index ced708cdd..07731b1ae 100644 --- a/pkg/registry/interface.go +++ b/pkg/registry/interface.go @@ -12,7 +12,7 @@ type Load interface { AddPackageChannels(manifest PackageManifest) error AddBundlePackageChannels(manifest PackageManifest, bundle *Bundle) error RemovePackage(packageName string) error - RemoveBundleByVersion(packageName string, operatorVersion string) error + RemoveBundle(csvToRemove string, csvToSave *string) error RemoveStrandedBundles() error DeprecateBundle(path string) error ClearNonHeadBundles() error diff --git a/pkg/sqlite/load.go b/pkg/sqlite/load.go index 289bba125..387c029f0 100644 --- a/pkg/sqlite/load.go +++ b/pkg/sqlite/load.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "strings" @@ -924,10 +925,23 @@ func (s *sqlLoader) getCSVNames(tx *sql.Tx, packageName string) ([]string, error return csvNames, nil } +type CSVNameError struct { + Err error + PackageName string + PackageVersion string +} + +var CSVNameNotFound CSVNameError + +func (e *CSVNameError) Error() string { + return fmt.Sprintf("%s: %s %s", e.Err, e.PackageName, e.PackageVersion) +} + func (s *sqlLoader) getCSVName(tx *sql.Tx, packageName string, version string) (string, error) { - getID, err := tx.Prepare(`SELECT DISTINCT operatorbundle.name FROM operatorbundle + query := `SELECT DISTINCT operatorbundle.name FROM operatorbundle INNER JOIN channel_entry ON operatorbundle.name=channel_entry.operatorbundle_name - WHERE channel_entry.package_name=? AND operatorbundle.version=?`) + WHERE channel_entry.package_name=? AND operatorbundle.version=?` + getID, err := tx.Prepare(query) // getID, err := tx.Prepare(` // SELECT DISTINCT channel_entry.operatorbundle_name // FROM channel_entry @@ -937,13 +951,21 @@ func (s *sqlLoader) getCSVName(tx *sql.Tx, packageName string, version string) ( return "", err } defer getID.Close() - rows, err := getID.Query(packageName) + rows, err := getID.Query(packageName, version) + + var errs []error + if err != nil { + errs = append(errs, fmt.Errorf("failed query: %s \nerror: %s", query, err)) + return "", utilerrors.NewAggregate(errs) + } var csvName sql.NullString if rows.Next() { if err := rows.Scan(&csvName); err != nil { return "", err } + } else { + return "", &CSVNameError{errors.New("no CSV name found"), packageName, version} } if err := rows.Close(); err != nil { @@ -1001,7 +1023,7 @@ func (s *sqlLoader) RemovePackage(packageName string) error { return s.RemoveStrandedBundles() } -func (s *sqlLoader) RemoveBundleByVersion(operatorName string, operatorVersion string) error { +func (s *sqlLoader) RemoveBundle(csvToRemove string, csvToSave *string) error { if err := func() error { tx, err := s.db.Begin() if err != nil { @@ -1010,12 +1032,15 @@ func (s *sqlLoader) RemoveBundleByVersion(operatorName string, operatorVersion s defer func() { tx.Rollback() }() - csvName, err := s.getCSVName(tx, operatorName, operatorVersion) - if err != nil { - return err + + if csvToSave != nil { + s.replaceChannelHead(tx, *csvToSave, csvToRemove) } - if err := s.rmBundle(tx, csvName); err != nil { + if err := s.rmChannelEntry(tx, csvToRemove); err != nil { + return err + } + if err := s.rmBundle(tx, csvToRemove); err != nil { return err } @@ -1027,6 +1052,19 @@ func (s *sqlLoader) RemoveBundleByVersion(operatorName string, operatorVersion s return s.RemoveStrandedBundles() } +func (s *sqlLoader) replaceChannelHead(tx *sql.Tx, newCsvName string, oldCsvName string) error { + replaceChannelHead, err := tx.Prepare("UPDATE channel set head_operatorbundle_name=? WHERE head_operatorbundle_name=?") + if err != nil { + return err + } + defer replaceChannelHead.Close() + if _, err := replaceChannelHead.Exec(newCsvName, oldCsvName); err != nil { + return err + } + + return nil +} + func (s *sqlLoader) rmBundle(tx *sql.Tx, csvName string) error { deleteBundle, err := tx.Prepare("DELETE FROM operatorbundle WHERE operatorbundle.name=?") if err != nil { diff --git a/pkg/sqlite/remove.go b/pkg/sqlite/remove.go index 5a5627a07..1cf5472da 100644 --- a/pkg/sqlite/remove.go +++ b/pkg/sqlite/remove.go @@ -26,8 +26,8 @@ type PackageRemover struct { type OperatorPackageVersionRemover struct { store registry.Load - packageName string - version string + CsvToRemove string + CsvToSave *string } var _ SQLRemover = &PackageRemover{} @@ -40,11 +40,11 @@ func NewSQLRemoverForPackages(store registry.Load, packages string) *PackageRemo } } -func NewSQLRemoverForOperatorCsvNames(store registry.Load, packageName string, version string) *OperatorPackageVersionRemover { +func NewSQLRemoverForOperatorCsvNames(store registry.Load, csvToRemove string, csvToSave *string) *OperatorPackageVersionRemover { return &OperatorPackageVersionRemover{ store: store, - packageName: packageName, - version: version, + CsvToRemove: csvToRemove, + CsvToSave: csvToSave, } } @@ -68,19 +68,19 @@ func (d *PackageRemover) Remove() error { func (d *OperatorPackageVersionRemover) Remove() error { fields := logrus.Fields{ - "package": d.packageName, - "version": d.version, + "csv": d.CsvToRemove, } log := logrus.WithFields(fields) - log.Info("deleting packages") + log.Infof("deleting package version %s", d.CsvToRemove) + if d.CsvToSave != nil { + log.Infof("replacing with %s as head in channel", *d.CsvToSave) + } var errs []error - // packages := sanitizePackageList(strings.Split(d.operatorVersions, ",")) - log.Infof("operator package and version: %s %s", d.packageName, d.version) - if err := d.store.RemoveBundleByVersion(d.packageName, d.version); err != nil { - errs = append(errs, fmt.Errorf("error removing operator bundle %s %s: %s", d.packageName, d.version, err)) + if err := d.store.RemoveBundle(d.CsvToRemove, d.CsvToSave); err != nil { + errs = append(errs, fmt.Errorf("error removing operator bundle %s: %s", d.CsvToRemove, err)) } return utilerrors.NewAggregate(errs) From 575e09eedaa791f0d471bd641eb0870d15997fef Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Thu, 22 Jul 2021 00:26:57 -0400 Subject: [PATCH 03/10] add command to generate DC dockerfile Signed-off-by: Joe Lanford Signed-off-by: Khaled Janania --- cmd/opm/alpha/cmd.go | 7 +- cmd/opm/alpha/generate/cmd.go | 104 ++++++++++++++++++++++ internal/action/generate_dockerfile.go | 63 +++++++++++++ internal/action/render.go | 3 +- pkg/containertools/dockerfilegenerator.go | 5 +- 5 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 cmd/opm/alpha/generate/cmd.go create mode 100644 internal/action/generate_dockerfile.go diff --git a/cmd/opm/alpha/cmd.go b/cmd/opm/alpha/cmd.go index 42e50e3dd..55c402b4b 100644 --- a/cmd/opm/alpha/cmd.go +++ b/cmd/opm/alpha/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle" + "github.com/operator-framework/operator-registry/cmd/opm/alpha/generate" "github.com/operator-framework/operator-registry/cmd/opm/alpha/list" ) @@ -14,6 +15,10 @@ func NewCmd() *cobra.Command { Short: "Run an alpha subcommand", } - runCmd.AddCommand(bundle.NewCmd(), list.NewCmd()) + runCmd.AddCommand( + bundle.NewCmd(), + generate.NewCmd(), + list.NewCmd(), + ) return runCmd } diff --git a/cmd/opm/alpha/generate/cmd.go b/cmd/opm/alpha/generate/cmd.go new file mode 100644 index 000000000..b41a61b49 --- /dev/null +++ b/cmd/opm/alpha/generate/cmd.go @@ -0,0 +1,104 @@ +package generate + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/operator-framework/operator-registry/internal/action" + "github.com/operator-framework/operator-registry/pkg/containertools" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{Use: "generate"} + cmd.AddCommand( + newDockerfileCmd(), + ) + return cmd +} + +func newDockerfileCmd() *cobra.Command { + var ( + baseImage string + extraLabelStrs []string + ) + cmd := &cobra.Command{ + Use: "dockerfile ", + Args: cobra.ExactArgs(1), + Short: "Generate a Dockerfile for a declarative config index", + Long: `Generate a Dockerfile for a declarative config index. + +This command creates a Dockerfile in the same directory as the +(named .Dockerfile) that can be used to build the index. If a +Dockerfile with the same name already exists, this command will fail.`, + RunE: func(_ *cobra.Command, args []string) error { + fromDir := args[0] + + extraLabels, err := parseLabels(extraLabelStrs) + if err != nil { + return err + } + + dir, indexName := filepath.Split(fromDir) + dockerfilePath := filepath.Join(dir, fmt.Sprintf("%s.Dockerfile", indexName)) + + if err := ensureNotExist(dockerfilePath); err != nil { + logrus.Fatal(err) + } + + if s, err := os.Stat(fromDir); err != nil { + return err + } else if !s.IsDir() { + return fmt.Errorf("provided root path %q is not a directory") + } + + f, err := os.OpenFile(dockerfilePath, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + logrus.Fatal(err) + } + defer f.Close() + + gen := action.GenerateDockerfile{ + BaseImage: baseImage, + FromDir: indexName, + ExtraLabels: extraLabels, + Writer: f, + } + if err := gen.Run(); err != nil { + log.Fatal(err) + } + return nil + }, + } + cmd.Flags().StringVarP(&baseImage, "binary-image", "i", containertools.DefaultBinarySourceImage, "Image in which to build catalog.") + cmd.Flags().StringSliceVarP(&extraLabelStrs, "extra-labels", "l", []string{}, "Extra labels to include in the generated Dockerfile. Labels should be of the form 'key=value'.") + return cmd +} + +func parseLabels(labelStrs []string) (map[string]string, error) { + labels := map[string]string{} + for _, l := range labelStrs { + spl := strings.SplitN(l, "=", 2) + if len(spl) != 2 { + return nil, fmt.Errorf("invalid label %q", l) + } + labels[spl[0]] = spl[1] + } + return labels, nil +} + +func ensureNotExist(path string) error { + _, err := os.Stat(path) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + return fmt.Errorf("path %q: %w", path, os.ErrExist) + } + return nil +} diff --git a/internal/action/generate_dockerfile.go b/internal/action/generate_dockerfile.go new file mode 100644 index 000000000..ac196ad62 --- /dev/null +++ b/internal/action/generate_dockerfile.go @@ -0,0 +1,63 @@ +package action + +import ( + "fmt" + "io" + "text/template" + + "github.com/operator-framework/operator-registry/pkg/containertools" +) + +type GenerateDockerfile struct { + BaseImage string + FromDir string + ExtraLabels map[string]string + Writer io.Writer +} + +func (i GenerateDockerfile) Run() error { + if err := i.validate(); err != nil { + return err + } + + t, err := template.New("dockerfile").Parse(dockerfileTmpl) + if err != nil { + // The template is hardcoded in the binary, so if + // there is a parse error, it was a programmer error. + panic(err) + } + return t.Execute(i.Writer, i) +} + +func (i GenerateDockerfile) validate() error { + if i.BaseImage == "" { + return fmt.Errorf("base image is unset") + } + if i.FromDir == "" { + return fmt.Errorf("index path is unset") + } + return nil +} + +const dockerfileTmpl = `# The base image is expected to contain +# /bin/opm (with a serve subcommand) and /bin/grpc_health_probe +FROM {{.BaseImage}} + +# Configure the entrypoint and command +ENTRYPOINT ["/bin/opm"] +CMD ["serve", "/configs"] + +# Copy declarative config root into image at /configs +ADD {{.FromDir}} /configs + +# Set DC-specific label for the location of the DC root directory +# in the image +LABEL ` + containertools.ConfigsLocationLabel + `=/configs +{{- if .ExtraLabels }} + +# Set other custom labels +{{- range $key, $value := .ExtraLabels }} +LABEL "{{ $key }}"="{{ $value }}" +{{- end }} +{{- end }} +` diff --git a/internal/action/render.go b/internal/action/render.go index 2f4293a39..d6e620790 100644 --- a/internal/action/render.go +++ b/internal/action/render.go @@ -153,8 +153,7 @@ func (r Render) imageToDeclcfg(ctx context.Context, imageRef string) (*declcfg.D if err != nil { return nil, err } - } else if configsDir, ok := labels["operators.operatorframework.io.index.configs.v1"]; ok { - // TODO(joelanford): Make a constant for above configs location label + } else if configsDir, ok := labels[containertools.ConfigsLocationLabel]; ok { if !r.AllowedRefMask.Allowed(RefDCImage) { return nil, fmt.Errorf("cannot render DC image: %w", &ErrNotAllowed{}) } diff --git a/pkg/containertools/dockerfilegenerator.go b/pkg/containertools/dockerfilegenerator.go index 4e1a21d40..f580b3168 100644 --- a/pkg/containertools/dockerfilegenerator.go +++ b/pkg/containertools/dockerfilegenerator.go @@ -8,9 +8,10 @@ import ( ) const ( - defaultBinarySourceImage = "quay.io/operator-framework/upstream-opm-builder" + DefaultBinarySourceImage = "quay.io/operator-framework/upstream-opm-builder" DefaultDbLocation = "/database/index.db" DbLocationLabel = "operators.operatorframework.io.index.database.v1" + ConfigsLocationLabel = "operators.operatorframework.io.index.configs.v1" ) // DockerfileGenerator defines functions to generate index dockerfiles @@ -36,7 +37,7 @@ func (g *IndexDockerfileGenerator) GenerateIndexDockerfile(binarySourceImage, da var dockerfile string if binarySourceImage == "" { - binarySourceImage = defaultBinarySourceImage + binarySourceImage = DefaultBinarySourceImage } g.Logger.Info("Generating dockerfile") From b774b1d42ce25d9359e5ec23aaf3541ebeee3209 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Thu, 22 Jul 2021 11:54:53 -0400 Subject: [PATCH 04/10] add generate_dockerfile_test.go, fix format string Signed-off-by: Joe Lanford Signed-off-by: Khaled Janania --- cmd/opm/alpha/generate/cmd.go | 2 +- internal/action/generate_dockerfile.go | 8 +- internal/action/generate_dockerfile_test.go | 108 ++++++++++++++++++++ 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 internal/action/generate_dockerfile_test.go diff --git a/cmd/opm/alpha/generate/cmd.go b/cmd/opm/alpha/generate/cmd.go index b41a61b49..c3380e8e9 100644 --- a/cmd/opm/alpha/generate/cmd.go +++ b/cmd/opm/alpha/generate/cmd.go @@ -54,7 +54,7 @@ Dockerfile with the same name already exists, this command will fail.`, if s, err := os.Stat(fromDir); err != nil { return err } else if !s.IsDir() { - return fmt.Errorf("provided root path %q is not a directory") + return fmt.Errorf("provided root path %q is not a directory", fromDir) } f, err := os.OpenFile(dockerfilePath, os.O_CREATE|os.O_WRONLY, 0666) diff --git a/internal/action/generate_dockerfile.go b/internal/action/generate_dockerfile.go index ac196ad62..ef2a6d871 100644 --- a/internal/action/generate_dockerfile.go +++ b/internal/action/generate_dockerfile.go @@ -10,7 +10,7 @@ import ( type GenerateDockerfile struct { BaseImage string - FromDir string + IndexDir string ExtraLabels map[string]string Writer io.Writer } @@ -33,8 +33,8 @@ func (i GenerateDockerfile) validate() error { if i.BaseImage == "" { return fmt.Errorf("base image is unset") } - if i.FromDir == "" { - return fmt.Errorf("index path is unset") + if i.IndexDir == "" { + return fmt.Errorf("index directory is unset") } return nil } @@ -48,7 +48,7 @@ ENTRYPOINT ["/bin/opm"] CMD ["serve", "/configs"] # Copy declarative config root into image at /configs -ADD {{.FromDir}} /configs +ADD {{.IndexDir}} /configs # Set DC-specific label for the location of the DC root directory # in the image diff --git a/internal/action/generate_dockerfile_test.go b/internal/action/generate_dockerfile_test.go new file mode 100644 index 000000000..6e0a79543 --- /dev/null +++ b/internal/action/generate_dockerfile_test.go @@ -0,0 +1,108 @@ +package action + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerateDockerfile(t *testing.T) { + type spec struct { + name string + gen GenerateDockerfile + expectedDockerfile string + expectedErr string + } + + specs := []spec{ + { + name: "Fail/EmptyBaseImage", + gen: GenerateDockerfile{ + IndexDir: "bar", + ExtraLabels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectedErr: "base image is unset", + }, + { + name: "Fail/EmptyFromDir", + gen: GenerateDockerfile{ + BaseImage: "foo", + ExtraLabels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectedErr: "index directory is unset", + }, + { + name: "Success/WithoutExtraLabels", + gen: GenerateDockerfile{ + BaseImage: "foo", + IndexDir: "bar", + }, + expectedDockerfile: `# The base image is expected to contain +# /bin/opm (with a serve subcommand) and /bin/grpc_health_probe +FROM foo + +# Configure the entrypoint and command +ENTRYPOINT ["/bin/opm"] +CMD ["serve", "/configs"] + +# Copy declarative config root into image at /configs +ADD bar /configs + +# Set DC-specific label for the location of the DC root directory +# in the image +LABEL operators.operatorframework.io.index.configs.v1=/configs +`, + }, + { + name: "Success/WithExtraLabels", + gen: GenerateDockerfile{ + BaseImage: "foo", + IndexDir: "bar", + ExtraLabels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectedDockerfile: `# The base image is expected to contain +# /bin/opm (with a serve subcommand) and /bin/grpc_health_probe +FROM foo + +# Configure the entrypoint and command +ENTRYPOINT ["/bin/opm"] +CMD ["serve", "/configs"] + +# Copy declarative config root into image at /configs +ADD bar /configs + +# Set DC-specific label for the location of the DC root directory +# in the image +LABEL operators.operatorframework.io.index.configs.v1=/configs + +# Set other custom labels +LABEL "key1"="value1" +LABEL "key2"="value2" +`, + }, + } + + for _, s := range specs { + t.Run(s.name, func(t *testing.T) { + buf := bytes.Buffer{} + s.gen.Writer = &buf + err := s.gen.Run() + if s.expectedErr != "" { + require.EqualError(t, err, s.expectedErr) + } else { + require.NoError(t, err) + require.Equal(t, s.expectedDockerfile, buf.String()) + } + }) + } +} From 76a0b2fcf7b9940b0e625fe90707391db24c4f69 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Thu, 22 Jul 2021 11:59:08 -0400 Subject: [PATCH 05/10] add note explaining duplicate label keys Signed-off-by: Joe Lanford Signed-off-by: Khaled Janania --- cmd/opm/alpha/generate/cmd.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/opm/alpha/generate/cmd.go b/cmd/opm/alpha/generate/cmd.go index c3380e8e9..18f61c615 100644 --- a/cmd/opm/alpha/generate/cmd.go +++ b/cmd/opm/alpha/generate/cmd.go @@ -35,7 +35,11 @@ func newDockerfileCmd() *cobra.Command { This command creates a Dockerfile in the same directory as the (named .Dockerfile) that can be used to build the index. If a -Dockerfile with the same name already exists, this command will fail.`, +Dockerfile with the same name already exists, this command will fail. + +When specifying extra labels, note that if duplicate keys exist, only the last +value of each duplicate key will be added to the generated Dockerfile. +`, RunE: func(_ *cobra.Command, args []string) error { fromDir := args[0] @@ -65,7 +69,7 @@ Dockerfile with the same name already exists, this command will fail.`, gen := action.GenerateDockerfile{ BaseImage: baseImage, - FromDir: indexName, + IndexDir: indexName, ExtraLabels: extraLabels, Writer: f, } From 0436d759796553ed96a70488b791821ab46629b9 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Thu, 22 Jul 2021 12:14:25 -0400 Subject: [PATCH 06/10] add short description for generate subcommand Signed-off-by: Joe Lanford Signed-off-by: Khaled Janania --- cmd/opm/alpha/generate/cmd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/opm/alpha/generate/cmd.go b/cmd/opm/alpha/generate/cmd.go index 18f61c615..dff7400c6 100644 --- a/cmd/opm/alpha/generate/cmd.go +++ b/cmd/opm/alpha/generate/cmd.go @@ -15,7 +15,10 @@ import ( ) func NewCmd() *cobra.Command { - cmd := &cobra.Command{Use: "generate"} + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate various artifacts for declarative config indexes", + } cmd.AddCommand( newDockerfileCmd(), ) From 56b5be81df2ab1e1633f60879221de7d14991eaf Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Tue, 20 Jul 2021 16:31:49 -0400 Subject: [PATCH 07/10] add declcfg.WalkFS function Signed-off-by: Joe Lanford Signed-off-by: Khaled Janania --- internal/declcfg/load.go | 74 +++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/internal/declcfg/load.go b/internal/declcfg/load.go index f61368811..a5cfca970 100644 --- a/internal/declcfg/load.go +++ b/internal/declcfg/load.go @@ -17,40 +17,61 @@ import ( "github.com/operator-framework/operator-registry/internal/property" ) -// LoadFS loads a declarative config from the provided root FS. LoadFS walks the -// filesystem from root and uses a gitignore-style filename matcher to skip files +type WalkFunc func(path string, cfg *DeclarativeConfig, err error) error + +// WalkFS walks root using a gitignore-style filename matcher to skip files // that match patterns found in .indexignore files found throughout the filesystem. -// If LoadFS encounters an error loading or parsing any file, the error will be -// immedidately returned. -func LoadFS(root fs.FS) (*DeclarativeConfig, error) { +// It calls walkFn for each declarative config file it finds. If WalkFS encounters +// an error loading or parsing any file, the error will be immediately returned. +func WalkFS(root fs.FS, walkFn WalkFunc) error { if root == nil { - return nil, fmt.Errorf("no declarative config filesystem provided") + return fmt.Errorf("no declarative config filesystem provided") } - cfg := &DeclarativeConfig{} - matcher, err := ignore.NewMatcher(root, ".indexignore") if err != nil { - return nil, err + return err } - if err := walkFiles(root, func(path string, r io.Reader) error { - if matcher.Match(path, false) { + return fs.WalkDir(root, ".", func(path string, info fs.DirEntry, err error) error { + if err != nil { + return walkFn(path, nil, err) + } + if info.IsDir() || matcher.Match(path, false) { return nil } - fileCfg, err := readYAMLOrJSON(r) + file, err := root.Open(path) if err != nil { - return fmt.Errorf("could not load config file %q: %v", path, err) + return walkFn(path, nil, err) } - if err := readBundleObjects(fileCfg.Bundles, root, path); err != nil { + defer file.Close() + cfg, err := readYAMLOrJSON(file) + if err != nil { + return walkFn(path, cfg, err) + } + if err := readBundleObjects(cfg.Bundles, root, path); err != nil { return fmt.Errorf("read bundle objects: %v", err) } - cfg.Packages = append(cfg.Packages, fileCfg.Packages...) - cfg.Bundles = append(cfg.Bundles, fileCfg.Bundles...) - cfg.Others = append(cfg.Others, fileCfg.Others...) + return walkFn(path, cfg, err) + }) +} +// LoadFS loads a declarative config from the provided root FS. LoadFS walks the +// filesystem from root and uses a gitignore-style filename matcher to skip files +// that match patterns found in .indexignore files found throughout the filesystem. +// If LoadFS encounters an error loading or parsing any file, the error will be +// immediately returned. +func LoadFS(root fs.FS) (*DeclarativeConfig, error) { + cfg := &DeclarativeConfig{} + if err := WalkFS(root, func(path string, fcfg *DeclarativeConfig, err error) error { + if err != nil { + return err + } + cfg.Packages = append(cfg.Packages, fcfg.Packages...) + cfg.Bundles = append(cfg.Bundles, fcfg.Bundles...) + cfg.Others = append(cfg.Others, fcfg.Others...) return nil }); err != nil { - return nil, fmt.Errorf("failed to read declarative configs dir: %v", err) + return nil, err } return cfg, nil } @@ -125,20 +146,3 @@ func readYAMLOrJSON(r io.Reader) (*DeclarativeConfig, error) { } return cfg, nil } - -func walkFiles(root fs.FS, f func(string, io.Reader) error) error { - return fs.WalkDir(root, ".", func(path string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - file, err := root.Open(path) - if err != nil { - return err - } - defer file.Close() - return f(path, file) - }) -} From c65b4cfa0d1dafcc1a45a98c81146d9ca030a149 Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Fri, 4 Jun 2021 11:34:20 -0700 Subject: [PATCH 08/10] opm alpha diff: generate a diff between two sets of catalog/bundle refs Implements enhancements/catalog-data-transfer-reduction.md Signed-off-by: Eric Stroczynski Signed-off-by: Khaled Janania --- cmd/opm/alpha/cmd.go | 4 +- cmd/opm/alpha/diff/cmd.go | 158 +++ go.mod | 2 + go.sum | 8 +- internal/action/diff.go | 66 ++ internal/action/diff_test.go | 157 +++ internal/action/render_test.go | 18 +- .../bar-bundle-v0.1.0/bundle.Dockerfile | 12 + .../bar-bundle-v0.1.0/manifests/bar.csv.yaml | 16 + .../manifests/bars.test.bar.crd.yaml | 12 + .../metadata/annotations.yaml | 4 + .../bar-bundle-v0.2.0/bundle.Dockerfile | 12 + .../bar-bundle-v0.2.0/manifests/bar.csv.yaml | 20 + .../manifests/bars.test.bar.crd.yaml | 12 + .../metadata/annotations.yaml | 4 + .../bar-bundle-v1.0.0/bundle.Dockerfile | 12 + .../bar-bundle-v1.0.0/manifests/bar.csv.yaml | 21 + .../manifests/bars.test.bar.crd.yaml | 17 + .../metadata/annotations.yaml | 4 + .../baz-bundle-v1.0.0/bundle.Dockerfile | 12 + .../baz-bundle-v1.0.0/manifests/baz.csv.yaml | 16 + .../manifests/bazs.test.baz.crd.yaml | 12 + .../metadata/annotations.yaml | 4 + .../baz-bundle-v1.0.1/bundle.Dockerfile | 12 + .../baz-bundle-v1.0.1/manifests/baz.csv.yaml | 20 + .../manifests/bazs.test.baz.crd.yaml | 12 + .../metadata/annotations.yaml | 4 + .../baz-bundle-v1.1.0/bundle.Dockerfile | 12 + .../baz-bundle-v1.1.0/manifests/baz.csv.yaml | 17 + .../manifests/bazs.test.baz.crd.yaml | 12 + .../metadata/annotations.yaml | 4 + .../metadata/dependencies.yaml | 2 +- .../metadata/dependencies.yaml | 2 +- .../foo-bundle-v0.3.0/bundle.Dockerfile | 12 + .../foo-bundle-v0.3.0/manifests/foo.csv.yaml | 21 + .../manifests/foos.test.foo.crd.yaml | 17 + .../metadata/annotations.yaml | 4 + .../metadata/dependencies.yaml | 11 + .../foo-bundle-v0.3.1/bundle.Dockerfile | 12 + .../foo-bundle-v0.3.1/manifests/foo.csv.yaml | 23 + .../manifests/foos.test.foo.crd.yaml | 17 + .../metadata/annotations.yaml | 4 + .../metadata/dependencies.yaml | 11 + .../foo-index-v0.2.0-declcfg/foo/index.yaml | 4 +- .../index-declcfgs/exp-headsonly/index.yaml | 184 +++ .../index-declcfgs/exp-latest/index.yaml | 152 +++ .../testdata/index-declcfgs/latest/index.yaml | 362 ++++++ .../testdata/index-declcfgs/old/index.yaml | 254 ++++ internal/declcfg/declcfg.go | 13 +- internal/declcfg/declcfg_to_model.go | 31 + internal/declcfg/diff.go | 393 ++++++ internal/declcfg/diff_test.go | 1051 +++++++++++++++++ internal/model/model.go | 5 + internal/property/property.go | 18 +- .../mitchellh/hashstructure/v2/LICENSE | 21 + .../mitchellh/hashstructure/v2/README.md | 76 ++ .../mitchellh/hashstructure/v2/errors.go | 22 + .../mitchellh/hashstructure/v2/go.mod | 3 + .../hashstructure/v2/hashstructure.go | 482 ++++++++ .../mitchellh/hashstructure/v2/include.go | 22 + vendor/modules.txt | 5 + 61 files changed, 3904 insertions(+), 26 deletions(-) create mode 100644 cmd/opm/alpha/diff/cmd.go create mode 100644 internal/action/diff.go create mode 100644 internal/action/diff_test.go create mode 100644 internal/action/testdata/bar-bundle-v0.1.0/bundle.Dockerfile create mode 100644 internal/action/testdata/bar-bundle-v0.1.0/manifests/bar.csv.yaml create mode 100644 internal/action/testdata/bar-bundle-v0.1.0/manifests/bars.test.bar.crd.yaml create mode 100644 internal/action/testdata/bar-bundle-v0.1.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/bar-bundle-v0.2.0/bundle.Dockerfile create mode 100644 internal/action/testdata/bar-bundle-v0.2.0/manifests/bar.csv.yaml create mode 100644 internal/action/testdata/bar-bundle-v0.2.0/manifests/bars.test.bar.crd.yaml create mode 100644 internal/action/testdata/bar-bundle-v0.2.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/bar-bundle-v1.0.0/bundle.Dockerfile create mode 100644 internal/action/testdata/bar-bundle-v1.0.0/manifests/bar.csv.yaml create mode 100644 internal/action/testdata/bar-bundle-v1.0.0/manifests/bars.test.bar.crd.yaml create mode 100644 internal/action/testdata/bar-bundle-v1.0.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.0/bundle.Dockerfile create mode 100644 internal/action/testdata/baz-bundle-v1.0.0/manifests/baz.csv.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.0/manifests/bazs.test.baz.crd.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.1/bundle.Dockerfile create mode 100644 internal/action/testdata/baz-bundle-v1.0.1/manifests/baz.csv.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.1/manifests/bazs.test.baz.crd.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.0.1/metadata/annotations.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.1.0/bundle.Dockerfile create mode 100644 internal/action/testdata/baz-bundle-v1.1.0/manifests/baz.csv.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.1.0/manifests/bazs.test.baz.crd.yaml create mode 100644 internal/action/testdata/baz-bundle-v1.1.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.0/bundle.Dockerfile create mode 100644 internal/action/testdata/foo-bundle-v0.3.0/manifests/foo.csv.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.0/manifests/foos.test.foo.crd.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.0/metadata/annotations.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.0/metadata/dependencies.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.1/bundle.Dockerfile create mode 100644 internal/action/testdata/foo-bundle-v0.3.1/manifests/foo.csv.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.1/manifests/foos.test.foo.crd.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.1/metadata/annotations.yaml create mode 100644 internal/action/testdata/foo-bundle-v0.3.1/metadata/dependencies.yaml create mode 100644 internal/action/testdata/index-declcfgs/exp-headsonly/index.yaml create mode 100644 internal/action/testdata/index-declcfgs/exp-latest/index.yaml create mode 100644 internal/action/testdata/index-declcfgs/latest/index.yaml create mode 100644 internal/action/testdata/index-declcfgs/old/index.yaml create mode 100644 internal/declcfg/diff.go create mode 100644 internal/declcfg/diff_test.go create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/LICENSE create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/README.md create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/errors.go create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/go.mod create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/hashstructure.go create mode 100644 vendor/github.com/mitchellh/hashstructure/v2/include.go diff --git a/cmd/opm/alpha/cmd.go b/cmd/opm/alpha/cmd.go index 55c402b4b..78335f76b 100644 --- a/cmd/opm/alpha/cmd.go +++ b/cmd/opm/alpha/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle" + "github.com/operator-framework/operator-registry/cmd/opm/alpha/diff" "github.com/operator-framework/operator-registry/cmd/opm/alpha/generate" "github.com/operator-framework/operator-registry/cmd/opm/alpha/list" ) @@ -17,8 +18,9 @@ func NewCmd() *cobra.Command { runCmd.AddCommand( bundle.NewCmd(), - generate.NewCmd(), list.NewCmd(), + generate.NewCmd(), + diff.NewCmd(), ) return runCmd } diff --git a/cmd/opm/alpha/diff/cmd.go b/cmd/opm/alpha/diff/cmd.go new file mode 100644 index 000000000..048fd3407 --- /dev/null +++ b/cmd/opm/alpha/diff/cmd.go @@ -0,0 +1,158 @@ +package diff + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/operator-framework/operator-registry/internal/action" + "github.com/operator-framework/operator-registry/internal/declcfg" + containerd "github.com/operator-framework/operator-registry/pkg/image/containerdregistry" + "github.com/operator-framework/operator-registry/pkg/lib/certs" +) + +const ( + retryInterval = time.Second * 5 + timeout = time.Minute * 1 +) + +type diff struct { + oldRefs []string + newRefs []string + + output string + caFile string + + debug bool + logger *logrus.Entry +} + +func NewCmd() *cobra.Command { + a := diff{ + logger: logrus.NewEntry(logrus.New()), + } + cmd := &cobra.Command{ + Use: "diff [old-refs]... new-refs...", + Short: "Diff old and new catalog references into a declarative config", + Long: templates.LongDesc(` +Diff a set of old and new catalog references ("refs") to produce a +declarative config containing only packages channels, and versions not present +in the old set. This is known as "latest" mode. +These references are passed through 'opm render' to produce a single declarative config. + +This command has special behavior when old-refs are omitted, called "heads-only" mode: +instead of the output being that of 'opm render refs...' +(which would be the case given the preceding behavior description), +only the channel heads of all channels in all packages are included in the output, +and dependencies. Dependencies are assumed to be provided by either an old ref, +in which case they are not included in the diff, or a new ref, in which +case they are included. Dependencies provided by some catalog unknown to +'opm alpha diff' will not cause the command to error, but an error will occur +if that catalog is not serving these dependencies at runtime. +`), + Example: templates.Examples(` +# Diff a catalog at some old state and latest state into a declarative config index. +mkdir -p catalog-index +opm alpha diff registry.org/my-catalog:abc123 registry.org/my-catalog:def456 -o yaml > ./my-catalog-index/index.yaml + +# Build and push this index into an index image. +opm alpha generate dockerfile ./my-catalog-index +docker build -t registry.org/my-catalog:latest-abc123-def456 -f index.Dockerfile . +docker push registry.org/my-catalog:latest-abc123-def456 + +# Create a new catalog from the heads of an existing catalog, then build and push the image like above. +opm alpha diff registry.org/my-catalog:def456 -o yaml > my-catalog-index/index.yaml +docker build -t registry.org/my-catalog:headsonly-def456 -f index.Dockerfile . +docker push registry.org/my-catalog:headsonly-def456 +`), + Args: cobra.RangeArgs(1, 2), + PreRunE: func(cmd *cobra.Command, args []string) error { + if a.debug { + a.logger.Logger.SetLevel(logrus.DebugLevel) + } + a.logger.Logger.SetOutput(os.Stderr) + return nil + }, + RunE: a.addFunc, + } + + cmd.Flags().StringVarP(&a.output, "output", "o", "yaml", "Output format (json|yaml)") + cmd.Flags().StringVarP(&a.caFile, "ca-file", "", "", "the root Certificates to use with this command") + + cmd.Flags().BoolVar(&a.debug, "debug", false, "enable debug logging") + return cmd +} + +func (a *diff) addFunc(cmd *cobra.Command, args []string) error { + a.parseArgs(args) + + skipTLS, err := cmd.PersistentFlags().GetBool("skip-tls") + if err != nil { + panic(err) + } + + var write func(declcfg.DeclarativeConfig, io.Writer) error + switch a.output { + case "yaml": + write = declcfg.WriteYAML + case "json": + write = declcfg.WriteJSON + default: + return fmt.Errorf("invalid --output value: %q", a.output) + } + + rootCAs, err := certs.RootCAs(a.caFile) + if err != nil { + a.logger.Fatalf("error getting root CAs: %v", err) + } + reg, err := containerd.NewRegistry(containerd.SkipTLS(skipTLS), containerd.WithLog(a.logger), containerd.WithRootCAs(rootCAs)) + if err != nil { + a.logger.Fatalf("error creating containerd registry: %v", err) + } + defer func() { + if err := reg.Destroy(); err != nil { + a.logger.Errorf("error destroying local cache: %v", err) + } + }() + + ctx, cancel := context.WithTimeout(context.TODO(), timeout) + defer cancel() + + diff := action.Diff{ + Registry: reg, + OldRefs: a.oldRefs, + NewRefs: a.newRefs, + Logger: a.logger, + } + cfg, err := diff.Run(ctx) + if err != nil { + a.logger.Fatalf("error generating diff: %v", err) + } + + if err := write(*cfg, os.Stdout); err != nil { + a.logger.Fatalf("error writing diff: %v", err) + } + + return nil +} + +func (a *diff) parseArgs(args []string) { + var old, new string + switch len(args) { + case 1: + new = args[0] + case 2: + old, new = args[0], args[1] + default: + panic("should never be here, CLI must enforce arg size") + } + a.oldRefs = strings.Split(old, ",") + a.newRefs = strings.Split(new, ",") +} diff --git a/go.mod b/go.mod index 401faf549..32d6af202 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/joelanford/ignore v0.0.0-20210607151042-0d25dc18b62d github.com/mattn/go-sqlite3 v1.10.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 + github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 github.com/opencontainers/go-digest v1.0.0 @@ -41,6 +42,7 @@ require ( github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.6.0 + github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cobra v1.1.1 github.com/stretchr/testify v1.6.1 github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 // indirect diff --git a/go.sum b/go.sum index 3b583a70a..2c2cdcf16 100644 --- a/go.sum +++ b/go.sum @@ -494,6 +494,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -548,6 +549,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -630,6 +633,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -695,8 +699,9 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -795,6 +800,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/action/diff.go b/internal/action/diff.go new file mode 100644 index 000000000..27c06432f --- /dev/null +++ b/internal/action/diff.go @@ -0,0 +1,66 @@ +package action + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" + + "github.com/operator-framework/operator-registry/internal/declcfg" + "github.com/operator-framework/operator-registry/internal/model" + "github.com/operator-framework/operator-registry/pkg/image" +) + +type Diff struct { + Registry image.Registry + + OldRefs []string + NewRefs []string + + Logger *logrus.Entry +} + +func (a Diff) Run(ctx context.Context) (*declcfg.DeclarativeConfig, error) { + if err := a.validate(); err != nil { + return nil, err + } + + // Heads-only mode does not require an old ref, so there may be nothing to render. + var oldModel model.Model + if len(a.OldRefs) != 0 { + oldRender := Render{Refs: a.OldRefs, Registry: a.Registry} + oldCfg, err := oldRender.Run(ctx) + if err != nil { + return nil, fmt.Errorf("error rendering old refs: %v", err) + } + oldModel, err = declcfg.ConvertToModel(*oldCfg) + if err != nil { + return nil, fmt.Errorf("error converting old declarative config to model: %v", err) + } + } + + newRender := Render{Refs: a.NewRefs, Registry: a.Registry} + newCfg, err := newRender.Run(ctx) + if err != nil { + return nil, fmt.Errorf("error rendering new refs: %v", err) + } + newModel, err := declcfg.ConvertToModel(*newCfg) + if err != nil { + return nil, fmt.Errorf("error converting new declarative config to model: %v", err) + } + + diffModel, err := declcfg.Diff(oldModel, newModel) + if err != nil { + return nil, fmt.Errorf("error generating diff: %v", err) + } + + cfg := declcfg.ConvertFromModel(diffModel) + return &cfg, nil +} + +func (p Diff) validate() error { + if len(p.NewRefs) == 0 { + return fmt.Errorf("no new refs to diff") + } + return nil +} diff --git a/internal/action/diff_test.go b/internal/action/diff_test.go new file mode 100644 index 000000000..71aa55db6 --- /dev/null +++ b/internal/action/diff_test.go @@ -0,0 +1,157 @@ +package action_test + +import ( + "context" + "embed" + "io/fs" + "path" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/internal/action" + "github.com/operator-framework/operator-registry/internal/declcfg" + "github.com/operator-framework/operator-registry/pkg/containertools" + "github.com/operator-framework/operator-registry/pkg/image" + "github.com/operator-framework/operator-registry/pkg/lib/bundle" +) + +func TestDiff(t *testing.T) { + type spec struct { + name string + diff action.Diff + expectedCfg *declcfg.DeclarativeConfig + assertion require.ErrorAssertionFunc + } + + registry, err := newDiffRegistry() + require.NoError(t, err) + + specs := []spec{ + { + name: "Success/Latest", + diff: action.Diff{ + Registry: registry, + OldRefs: []string{filepath.Join("testdata", "index-declcfgs", "old")}, + NewRefs: []string{filepath.Join("testdata", "index-declcfgs", "latest")}, + }, + expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-latest")), + assertion: require.NoError, + }, + { + name: "Success/HeadsOnly", + diff: action.Diff{ + Registry: registry, + NewRefs: []string{filepath.Join("testdata", "index-declcfgs", "latest")}, + }, + expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-headsonly")), + assertion: require.NoError, + }, + } + + for _, s := range specs { + t.Run(s.name, func(t *testing.T) { + actualCfg, actualErr := s.diff.Run(context.Background()) + s.assertion(t, actualErr) + require.Equal(t, s.expectedCfg, actualCfg) + }) + } +} + +var ( + //go:embed testdata/foo-bundle-v0.1.0/manifests/* + //go:embed testdata/foo-bundle-v0.1.0/metadata/* + fooBundlev010 embed.FS + //go:embed testdata/foo-bundle-v0.2.0/manifests/* + //go:embed testdata/foo-bundle-v0.2.0/metadata/* + fooBundlev020 embed.FS + //go:embed testdata/foo-bundle-v0.3.0/manifests/* + //go:embed testdata/foo-bundle-v0.3.0/metadata/* + fooBundlev030 embed.FS + //go:embed testdata/foo-bundle-v0.3.1/manifests/* + //go:embed testdata/foo-bundle-v0.3.1/metadata/* + fooBundlev031 embed.FS + //go:embed testdata/bar-bundle-v0.1.0/manifests/* + //go:embed testdata/bar-bundle-v0.1.0/metadata/* + barBundlev010 embed.FS + //go:embed testdata/bar-bundle-v0.2.0/manifests/* + //go:embed testdata/bar-bundle-v0.2.0/metadata/* + barBundlev020 embed.FS + //go:embed testdata/bar-bundle-v1.0.0/manifests/* + //go:embed testdata/bar-bundle-v1.0.0/metadata/* + barBundlev100 embed.FS + //go:embed testdata/baz-bundle-v1.0.0/manifests/* + //go:embed testdata/baz-bundle-v1.0.0/metadata/* + bazBundlev100 embed.FS + //go:embed testdata/baz-bundle-v1.0.1/manifests/* + //go:embed testdata/baz-bundle-v1.0.1/metadata/* + bazBundlev101 embed.FS + //go:embed testdata/baz-bundle-v1.1.0/manifests/* + //go:embed testdata/baz-bundle-v1.1.0/metadata/* + bazBundlev110 embed.FS +) + +var bundleToFS = map[string]embed.FS{ + "test.registry/foo-operator/foo-bundle:v0.1.0": fooBundlev010, + "test.registry/foo-operator/foo-bundle:v0.2.0": fooBundlev020, + "test.registry/foo-operator/foo-bundle:v0.3.0": fooBundlev030, + "test.registry/foo-operator/foo-bundle:v0.3.1": fooBundlev031, + "test.registry/bar-operator/bar-bundle:v0.1.0": barBundlev010, + "test.registry/bar-operator/bar-bundle:v0.2.0": barBundlev020, + "test.registry/bar-operator/bar-bundle:v1.0.0": barBundlev100, + "test.registry/baz-operator/baz-bundle:v1.0.0": bazBundlev100, + "test.registry/baz-operator/baz-bundle:v1.0.1": bazBundlev101, + "test.registry/baz-operator/baz-bundle:v1.1.0": bazBundlev110, +} + +//go:embed testdata/index-declcfgs +var indicesDir embed.FS + +func newDiffRegistry() (image.Registry, error) { + subDeclcfgImage, err := fs.Sub(indicesDir, "testdata/index-declcfgs") + if err != nil { + return nil, err + } + reg := &image.MockRegistry{ + RemoteImages: map[image.Reference]*image.MockImage{ + image.SimpleReference("test.registry/catalog/index-declcfg:latest"): { + Labels: map[string]string{containertools.ConfigsLocationLabel: "/latest/index.yaml"}, + FS: subDeclcfgImage, + }, + image.SimpleReference("test.registry/catalog/index-declcfg:old"): { + Labels: map[string]string{containertools.ConfigsLocationLabel: "/old/index.yaml"}, + FS: subDeclcfgImage, + }, + }, + } + + for name, bfs := range bundleToFS { + base := filepath.Base(name) + pkg := base[:strings.Index(base, ":")] + base = strings.ReplaceAll(base, ":", "-") + subImage, err := fs.Sub(bfs, path.Join("testdata", base)) + if err != nil { + return nil, err + } + reg.RemoteImages[image.SimpleReference(name)] = &image.MockImage{ + Labels: map[string]string{bundle.PackageLabel: pkg}, + FS: subImage, + } + } + + return reg, nil +} + +func loadDirFS(t *testing.T, parent fs.FS, dir string) *declcfg.DeclarativeConfig { + sub, err := fs.Sub(parent, dir) + if err != nil { + t.Fatal(err) + } + cfg, err := declcfg.LoadFS(sub) + if err != nil { + t.Fatal(err) + } + return cfg +} diff --git a/internal/action/render_test.go b/internal/action/render_test.go index f75ae54e9..0f04c7592 100644 --- a/internal/action/render_test.go +++ b/internal/action/render_test.go @@ -87,7 +87,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.1.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.1.0"), property.MustBuildBundleObjectData(foov1csv), property.MustBuildBundleObjectData(foov1crd), @@ -115,7 +115,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.2.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.2.0"), property.MustBuildSkips("foo.v0.1.1"), property.MustBuildSkips("foo.v0.1.2"), @@ -164,7 +164,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.1.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.1.0"), property.MustBuildBundleObjectData(foov1csv), property.MustBuildBundleObjectData(foov1crd), @@ -192,7 +192,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.2.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.2.0"), property.MustBuildSkips("foo.v0.1.1"), property.MustBuildSkips("foo.v0.1.2"), @@ -240,7 +240,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.1.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.1.0"), property.MustBuildBundleObjectData(foov1csv), property.MustBuildBundleObjectData(foov1crd), @@ -268,7 +268,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.2.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.2.0"), property.MustBuildSkips("foo.v0.1.1"), property.MustBuildSkips("foo.v0.1.2"), @@ -316,7 +316,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.1.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.1.0"), property.MustBuildBundleObjectData(foov1csv), property.MustBuildBundleObjectData(foov1crd), @@ -344,7 +344,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.2.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.2.0"), property.MustBuildSkips("foo.v0.1.1"), property.MustBuildSkips("foo.v0.1.2"), @@ -386,7 +386,7 @@ func TestRender(t *testing.T) { property.MustBuildGVK("test.foo", "v1", "Foo"), property.MustBuildGVKRequired("test.bar", "v1alpha1", "Bar"), property.MustBuildPackage("foo", "0.2.0"), - property.MustBuildPackageRequired("bar", "v0.1.0"), + property.MustBuildPackageRequired("bar", "<0.1.0"), property.MustBuildSkipRange("<0.2.0"), property.MustBuildSkips("foo.v0.1.1"), property.MustBuildSkips("foo.v0.1.2"), diff --git a/internal/action/testdata/bar-bundle-v0.1.0/bundle.Dockerfile b/internal/action/testdata/bar-bundle-v0.1.0/bundle.Dockerfile new file mode 100644 index 000000000..0c32642a1 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.1.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=bar +LABEL operators.operatorframework.io.bundle.channels.v1=alpha + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/bar-bundle-v0.1.0/manifests/bar.csv.yaml b/internal/action/testdata/bar-bundle-v0.1.0/manifests/bar.csv.yaml new file mode 100644 index 000000000..3550e4950 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.1.0/manifests/bar.csv.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: bar.v0.1.0 +spec: + customresourcedefinitions: + owned: + - group: test.bar + version: v1alpha1 + kind: Bar + name: bars.test.bar + version: 0.1.0 + relatedImages: + - name: operator + image: test.registry/bar-operator/bar:v0.1.0 diff --git a/internal/action/testdata/bar-bundle-v0.1.0/manifests/bars.test.bar.crd.yaml b/internal/action/testdata/bar-bundle-v0.1.0/manifests/bars.test.bar.crd.yaml new file mode 100644 index 000000000..45d703e76 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.1.0/manifests/bars.test.bar.crd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bars.test.bar +spec: + group: test.bar + names: + kind: Bar + plural: bars + versions: + - name: v1alpha1 diff --git a/internal/action/testdata/bar-bundle-v0.1.0/metadata/annotations.yaml b/internal/action/testdata/bar-bundle-v0.1.0/metadata/annotations.yaml new file mode 100644 index 000000000..e85bbf677 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.1.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: bar + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.bundle.channel.default.v1: alpha diff --git a/internal/action/testdata/bar-bundle-v0.2.0/bundle.Dockerfile b/internal/action/testdata/bar-bundle-v0.2.0/bundle.Dockerfile new file mode 100644 index 000000000..0c32642a1 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.2.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=bar +LABEL operators.operatorframework.io.bundle.channels.v1=alpha + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/bar-bundle-v0.2.0/manifests/bar.csv.yaml b/internal/action/testdata/bar-bundle-v0.2.0/manifests/bar.csv.yaml new file mode 100644 index 000000000..7a4bc1b31 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.2.0/manifests/bar.csv.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: bar.v0.2.0 + annotations: + olm.skipRange: <0.2.0 +spec: + customresourcedefinitions: + owned: + - group: test.bar + version: v1alpha1 + kind: Bar + name: bars.test.bar + version: 0.2.0 + skips: + - bar.v0.1.0 + relatedImages: + - name: operator + image: test.registry/bar-operator/bar:v0.2.0 diff --git a/internal/action/testdata/bar-bundle-v0.2.0/manifests/bars.test.bar.crd.yaml b/internal/action/testdata/bar-bundle-v0.2.0/manifests/bars.test.bar.crd.yaml new file mode 100644 index 000000000..45d703e76 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.2.0/manifests/bars.test.bar.crd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bars.test.bar +spec: + group: test.bar + names: + kind: Bar + plural: bars + versions: + - name: v1alpha1 diff --git a/internal/action/testdata/bar-bundle-v0.2.0/metadata/annotations.yaml b/internal/action/testdata/bar-bundle-v0.2.0/metadata/annotations.yaml new file mode 100644 index 000000000..e85bbf677 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v0.2.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: bar + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.bundle.channel.default.v1: alpha diff --git a/internal/action/testdata/bar-bundle-v1.0.0/bundle.Dockerfile b/internal/action/testdata/bar-bundle-v1.0.0/bundle.Dockerfile new file mode 100644 index 000000000..bf252f160 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v1.0.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=bar +LABEL operators.operatorframework.io.bundle.channels.v1=stable + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/bar-bundle-v1.0.0/manifests/bar.csv.yaml b/internal/action/testdata/bar-bundle-v1.0.0/manifests/bar.csv.yaml new file mode 100644 index 000000000..9924b1adb --- /dev/null +++ b/internal/action/testdata/bar-bundle-v1.0.0/manifests/bar.csv.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: bar.v1.0.0 +spec: + customresourcedefinitions: + owned: + - group: test.bar + version: v1alpha1 + kind: Bar + name: bars.test.bar + - group: test.bar + version: v1 + kind: Bar + name: bars.test.bar + version: 1.0.0 + replaces: bar.v0.2.0 + relatedImages: + - name: operator + image: test.registry/bar-operator/bar:v1.0.0 diff --git a/internal/action/testdata/bar-bundle-v1.0.0/manifests/bars.test.bar.crd.yaml b/internal/action/testdata/bar-bundle-v1.0.0/manifests/bars.test.bar.crd.yaml new file mode 100644 index 000000000..413f7a544 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v1.0.0/manifests/bars.test.bar.crd.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bars.test.bar +spec: + group: test.bar + names: + kind: Bar + plural: bars + versions: + - name: v1alpha1 + served: true + storage: false + - name: v1 + served: true + storage: true diff --git a/internal/action/testdata/bar-bundle-v1.0.0/metadata/annotations.yaml b/internal/action/testdata/bar-bundle-v1.0.0/metadata/annotations.yaml new file mode 100644 index 000000000..2b425eae5 --- /dev/null +++ b/internal/action/testdata/bar-bundle-v1.0.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: bar + operators.operatorframework.io.bundle.channels.v1: stable + operators.operatorframework.io.bundle.channel.default.v1: stable diff --git a/internal/action/testdata/baz-bundle-v1.0.0/bundle.Dockerfile b/internal/action/testdata/baz-bundle-v1.0.0/bundle.Dockerfile new file mode 100644 index 000000000..5351aff0b --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=baz +LABEL operators.operatorframework.io.bundle.channels.v1=stable + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/baz-bundle-v1.0.0/manifests/baz.csv.yaml b/internal/action/testdata/baz-bundle-v1.0.0/manifests/baz.csv.yaml new file mode 100644 index 000000000..681ae352d --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.0/manifests/baz.csv.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: baz.v1.0.0 +spec: + customresourcedefinitions: + owned: + - group: test.baz + version: v1 + kind: Baz + name: bazs.test.baz + version: 1.0.0 + relatedImages: + - name: operator + image: test.registry/baz-operator/baz:v1.0.0 diff --git a/internal/action/testdata/baz-bundle-v1.0.0/manifests/bazs.test.baz.crd.yaml b/internal/action/testdata/baz-bundle-v1.0.0/manifests/bazs.test.baz.crd.yaml new file mode 100644 index 000000000..757fde072 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.0/manifests/bazs.test.baz.crd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bazs.test.baz +spec: + group: test.baz + names: + kind: Baz + plural: bazs + versions: + - name: v1 diff --git a/internal/action/testdata/baz-bundle-v1.0.0/metadata/annotations.yaml b/internal/action/testdata/baz-bundle-v1.0.0/metadata/annotations.yaml new file mode 100644 index 000000000..23fcd1406 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: baz + operators.operatorframework.io.bundle.channels.v1: stable + operators.operatorframework.io.bundle.channel.default.v1: stable diff --git a/internal/action/testdata/baz-bundle-v1.0.1/bundle.Dockerfile b/internal/action/testdata/baz-bundle-v1.0.1/bundle.Dockerfile new file mode 100644 index 000000000..5351aff0b --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.1/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=baz +LABEL operators.operatorframework.io.bundle.channels.v1=stable + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/baz-bundle-v1.0.1/manifests/baz.csv.yaml b/internal/action/testdata/baz-bundle-v1.0.1/manifests/baz.csv.yaml new file mode 100644 index 000000000..0d83700cd --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.1/manifests/baz.csv.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: baz.v1.0.1 + annotations: + olm.skipRange: <1.0.1 +spec: + customresourcedefinitions: + owned: + - group: test.baz + version: v1 + kind: Baz + name: bazs.test.baz + version: 1.0.1 + skips: + - baz.v1.0.0 + relatedImages: + - name: operator + image: test.registry/baz-operator/baz:v1.0.1 diff --git a/internal/action/testdata/baz-bundle-v1.0.1/manifests/bazs.test.baz.crd.yaml b/internal/action/testdata/baz-bundle-v1.0.1/manifests/bazs.test.baz.crd.yaml new file mode 100644 index 000000000..757fde072 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.1/manifests/bazs.test.baz.crd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bazs.test.baz +spec: + group: test.baz + names: + kind: Baz + plural: bazs + versions: + - name: v1 diff --git a/internal/action/testdata/baz-bundle-v1.0.1/metadata/annotations.yaml b/internal/action/testdata/baz-bundle-v1.0.1/metadata/annotations.yaml new file mode 100644 index 000000000..23fcd1406 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.0.1/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: baz + operators.operatorframework.io.bundle.channels.v1: stable + operators.operatorframework.io.bundle.channel.default.v1: stable diff --git a/internal/action/testdata/baz-bundle-v1.1.0/bundle.Dockerfile b/internal/action/testdata/baz-bundle-v1.1.0/bundle.Dockerfile new file mode 100644 index 000000000..5351aff0b --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.1.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=baz +LABEL operators.operatorframework.io.bundle.channels.v1=stable + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/baz-bundle-v1.1.0/manifests/baz.csv.yaml b/internal/action/testdata/baz-bundle-v1.1.0/manifests/baz.csv.yaml new file mode 100644 index 000000000..e13b42514 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.1.0/manifests/baz.csv.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: baz.v1.1.0 +spec: + customresourcedefinitions: + owned: + - group: test.baz + version: v1 + kind: Baz + name: bazs.test.baz + version: 1.1.0 + replaces: baz.v1.0.0 + relatedImages: + - name: operator + image: test.registry/baz-operator/baz:v1.1.0 diff --git a/internal/action/testdata/baz-bundle-v1.1.0/manifests/bazs.test.baz.crd.yaml b/internal/action/testdata/baz-bundle-v1.1.0/manifests/bazs.test.baz.crd.yaml new file mode 100644 index 000000000..757fde072 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.1.0/manifests/bazs.test.baz.crd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bazs.test.baz +spec: + group: test.baz + names: + kind: Baz + plural: bazs + versions: + - name: v1 diff --git a/internal/action/testdata/baz-bundle-v1.1.0/metadata/annotations.yaml b/internal/action/testdata/baz-bundle-v1.1.0/metadata/annotations.yaml new file mode 100644 index 000000000..23fcd1406 --- /dev/null +++ b/internal/action/testdata/baz-bundle-v1.1.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: baz + operators.operatorframework.io.bundle.channels.v1: stable + operators.operatorframework.io.bundle.channel.default.v1: stable diff --git a/internal/action/testdata/foo-bundle-v0.1.0/metadata/dependencies.yaml b/internal/action/testdata/foo-bundle-v0.1.0/metadata/dependencies.yaml index 7baa36dab..e5d50aefd 100644 --- a/internal/action/testdata/foo-bundle-v0.1.0/metadata/dependencies.yaml +++ b/internal/action/testdata/foo-bundle-v0.1.0/metadata/dependencies.yaml @@ -3,7 +3,7 @@ dependencies: - type: olm.package value: packageName: bar - version: v0.1.0 + version: <0.1.0 - type: olm.gvk value: group: "test.bar" diff --git a/internal/action/testdata/foo-bundle-v0.2.0/metadata/dependencies.yaml b/internal/action/testdata/foo-bundle-v0.2.0/metadata/dependencies.yaml index 7baa36dab..e5d50aefd 100644 --- a/internal/action/testdata/foo-bundle-v0.2.0/metadata/dependencies.yaml +++ b/internal/action/testdata/foo-bundle-v0.2.0/metadata/dependencies.yaml @@ -3,7 +3,7 @@ dependencies: - type: olm.package value: packageName: bar - version: v0.1.0 + version: <0.1.0 - type: olm.gvk value: group: "test.bar" diff --git a/internal/action/testdata/foo-bundle-v0.3.0/bundle.Dockerfile b/internal/action/testdata/foo-bundle-v0.3.0/bundle.Dockerfile new file mode 100644 index 000000000..df0043928 --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.0/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=foo +LABEL operators.operatorframework.io.bundle.channels.v1=beta + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/foo-bundle-v0.3.0/manifests/foo.csv.yaml b/internal/action/testdata/foo-bundle-v0.3.0/manifests/foo.csv.yaml new file mode 100644 index 000000000..589731b07 --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.0/manifests/foo.csv.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: foo.v0.3.0 +spec: + customresourcedefinitions: + owned: + - group: test.foo + version: v1 + kind: Foo + name: foos.test.foo + - group: test.foo + version: v2 + kind: Foo + name: foos.test.foo + version: 0.3.0 + replaces: foo.v0.2.0 + relatedImages: + - name: operator + image: test.registry/foo-operator/foo:v0.3.0 diff --git a/internal/action/testdata/foo-bundle-v0.3.0/manifests/foos.test.foo.crd.yaml b/internal/action/testdata/foo-bundle-v0.3.0/manifests/foos.test.foo.crd.yaml new file mode 100644 index 000000000..6bf85a3ca --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.0/manifests/foos.test.foo.crd.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: foos.test.foo +spec: + group: test.foo + names: + kind: Foo + plural: foos + versions: + - name: v1 + served: true + storage: false + - name: v2 + served: true + storage: true diff --git a/internal/action/testdata/foo-bundle-v0.3.0/metadata/annotations.yaml b/internal/action/testdata/foo-bundle-v0.3.0/metadata/annotations.yaml new file mode 100644 index 000000000..0cec7ef3e --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.0/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: foo + operators.operatorframework.io.bundle.channels.v1: beta + operators.operatorframework.io.bundle.channel.default.v1: beta \ No newline at end of file diff --git a/internal/action/testdata/foo-bundle-v0.3.0/metadata/dependencies.yaml b/internal/action/testdata/foo-bundle-v0.3.0/metadata/dependencies.yaml new file mode 100644 index 000000000..2315b30f6 --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.0/metadata/dependencies.yaml @@ -0,0 +1,11 @@ +--- +dependencies: + - type: olm.package + value: + packageName: bar + version: <0.2.0 + - type: olm.gvk + value: + group: "test.bar" + version: "v1alpha1" + kind: "Bar" diff --git a/internal/action/testdata/foo-bundle-v0.3.1/bundle.Dockerfile b/internal/action/testdata/foo-bundle-v0.3.1/bundle.Dockerfile new file mode 100644 index 000000000..df0043928 --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.1/bundle.Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=foo +LABEL operators.operatorframework.io.bundle.channels.v1=beta + +# Copy files to locations specified by labels. +COPY manifests /manifests/ +COPY metadata /metadata/ diff --git a/internal/action/testdata/foo-bundle-v0.3.1/manifests/foo.csv.yaml b/internal/action/testdata/foo-bundle-v0.3.1/manifests/foo.csv.yaml new file mode 100644 index 000000000..ab0f11aaf --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.1/manifests/foo.csv.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: foo.v0.3.1 +spec: + customresourcedefinitions: + owned: + - group: test.foo + version: v1 + kind: Foo + name: foos.test.foo + - group: test.foo + version: v2 + kind: Foo + name: foos.test.foo + version: 0.3.1 + replaces: foo.v0.2.0 + skips: + - foo.v0.3.0 + relatedImages: + - name: operator + image: test.registry/foo-operator/foo:v0.3.1 diff --git a/internal/action/testdata/foo-bundle-v0.3.1/manifests/foos.test.foo.crd.yaml b/internal/action/testdata/foo-bundle-v0.3.1/manifests/foos.test.foo.crd.yaml new file mode 100644 index 000000000..6bf85a3ca --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.1/manifests/foos.test.foo.crd.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: foos.test.foo +spec: + group: test.foo + names: + kind: Foo + plural: foos + versions: + - name: v1 + served: true + storage: false + - name: v2 + served: true + storage: true diff --git a/internal/action/testdata/foo-bundle-v0.3.1/metadata/annotations.yaml b/internal/action/testdata/foo-bundle-v0.3.1/metadata/annotations.yaml new file mode 100644 index 000000000..0cec7ef3e --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.1/metadata/annotations.yaml @@ -0,0 +1,4 @@ +annotations: + operators.operatorframework.io.bundle.package.v1: foo + operators.operatorframework.io.bundle.channels.v1: beta + operators.operatorframework.io.bundle.channel.default.v1: beta \ No newline at end of file diff --git a/internal/action/testdata/foo-bundle-v0.3.1/metadata/dependencies.yaml b/internal/action/testdata/foo-bundle-v0.3.1/metadata/dependencies.yaml new file mode 100644 index 000000000..2315b30f6 --- /dev/null +++ b/internal/action/testdata/foo-bundle-v0.3.1/metadata/dependencies.yaml @@ -0,0 +1,11 @@ +--- +dependencies: + - type: olm.package + value: + packageName: bar + version: <0.2.0 + - type: olm.gvk + value: + group: "test.bar" + version: "v1alpha1" + kind: "Bar" diff --git a/internal/action/testdata/foo-index-v0.2.0-declcfg/foo/index.yaml b/internal/action/testdata/foo-index-v0.2.0-declcfg/foo/index.yaml index 2e108e380..e42e746b2 100644 --- a/internal/action/testdata/foo-index-v0.2.0-declcfg/foo/index.yaml +++ b/internal/action/testdata/foo-index-v0.2.0-declcfg/foo/index.yaml @@ -28,7 +28,7 @@ properties: - type: olm.package.required value: packageName: bar - versionRange: v0.1.0 + versionRange: <0.1.0 - type: olm.skipRange value: <0.1.0 - type: olm.bundle.object @@ -72,7 +72,7 @@ properties: - type: olm.package.required value: packageName: bar - versionRange: v0.1.0 + versionRange: <0.1.0 - type: olm.skipRange value: <0.2.0 - type: olm.skips diff --git a/internal/action/testdata/index-declcfgs/exp-headsonly/index.yaml b/internal/action/testdata/index-declcfgs/exp-headsonly/index.yaml new file mode 100644 index 000000000..ac7ee30f8 --- /dev/null +++ b/internal/action/testdata/index-declcfgs/exp-headsonly/index.yaml @@ -0,0 +1,184 @@ +--- +defaultChannel: stable +name: bar +schema: olm.package +--- +# Added because foo.v0.3.1 depends on bar <0.2.0 +image: test.registry/bar-operator/bar-bundle:v0.1.0 +name: bar.v0.1.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MC4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djAuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJ2ZXJzaW9uIjoiMC4xLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.1.0 + name: operator +schema: olm.bundle +--- +# Added because foo.v0.3.1 depends on Bar, test.bar/v1alpha1. +# Note that dependency selection cannot decide between bar 0.1.0 +# and 0.2.0, as this is resolver territory. +image: test.registry/bar-operator/bar-bundle:v0.2.0 +name: bar.v0.2.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMi4wIn0sIm5hbWUiOiJiYXIudjAuMi4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJhciIsImtpbmQiOiJCYXIiLCJuYW1lIjoiYmFycy50ZXN0LmJhciIsInZlcnNpb24iOiJ2MWFscGhhMSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXItb3BlcmF0b3IvYmFyOnYwLjIuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmFyLnYwLjEuMCJdLCJ2ZXJzaW9uIjoiMC4yLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.2.0 +- type: olm.skipRange + value: <0.2.0 +- type: olm.skips + value: bar.v0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.2.0 + name: operator +schema: olm.bundle +--- +image: test.registry/bar-operator/bar-bundle:v1.0.0 +name: bar.v1.0.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYxIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MS4wLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn0seyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djEuMC4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhci52MC4yLjAiLCJ2ZXJzaW9uIjoiMS4wLjAifX0= +- type: olm.channel + value: + name: alpha + replaces: bar.v0.2.0 +- type: olm.channel + value: + name: stable +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1 +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 1.0.0 +relatedImages: +- image: test.registry/bar-operator/bar:v1.0.0 + name: operator +schema: olm.bundle +--- +defaultChannel: stable +name: baz +schema: olm.package +--- +image: test.registry/baz-operator/baz-bundle:v1.1.0 +name: baz.v1.1.0 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhei52MS4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmF6Iiwia2luZCI6IkJheiIsIm5hbWUiOiJiYXpzLnRlc3QuYmF6IiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhei1vcGVyYXRvci9iYXo6djEuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhei52MS4wLjAiLCJ2ZXJzaW9uIjoiMS4xLjAifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.1.0 +- type: olm.skips + value: baz.v1.0.1 +relatedImages: +- image: test.registry/baz-operator/baz:v1.1.0 + name: operator +schema: olm.bundle +--- +defaultChannel: beta +name: foo +schema: olm.package +--- +image: test.registry/foo-operator/foo-bundle:v0.3.1 +name: foo.v0.3.1 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYyIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvby52MC4zLjEifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYxIn0seyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYyIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Zvby1vcGVyYXRvci9mb286djAuMy4xIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImZvby52MC4yLjAiLCJza2lwcyI6WyJmb28udjAuMy4wIl0sInZlcnNpb24iOiIwLjMuMSJ9fQ== +- type: olm.channel + value: + name: beta + replaces: foo.v0.2.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v2 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.3.1 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.2.0 +- type: olm.skips + value: foo.v0.3.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.3.1 + name: operator +schema: olm.bundle diff --git a/internal/action/testdata/index-declcfgs/exp-latest/index.yaml b/internal/action/testdata/index-declcfgs/exp-latest/index.yaml new file mode 100644 index 000000000..4b9766352 --- /dev/null +++ b/internal/action/testdata/index-declcfgs/exp-latest/index.yaml @@ -0,0 +1,152 @@ +--- +defaultChannel: stable +name: bar +schema: olm.package +--- +image: test.registry/bar-operator/bar-bundle:v1.0.0 +name: bar.v1.0.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYxIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MS4wLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn0seyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djEuMC4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhci52MC4yLjAiLCJ2ZXJzaW9uIjoiMS4wLjAifX0= +- type: olm.channel + value: + name: alpha + replaces: bar.v0.2.0 +- type: olm.channel + value: + name: stable +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1 +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 1.0.0 +relatedImages: +- image: test.registry/bar-operator/bar:v1.0.0 + name: operator +schema: olm.bundle +--- +defaultChannel: stable +name: baz +schema: olm.package +--- +image: test.registry/baz-operator/baz-bundle:v1.0.1 +name: baz.v1.0.1 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzEuMC4xIn0sIm5hbWUiOiJiYXoudjEuMC4xIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJheiIsImtpbmQiOiJCYXoiLCJuYW1lIjoiYmF6cy50ZXN0LmJheiIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXotb3BlcmF0b3IvYmF6OnYxLjAuMSIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmF6LnYxLjAuMCJdLCJ2ZXJzaW9uIjoiMS4wLjEifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.0.1 +- type: olm.skipRange + value: <1.0.0 +relatedImages: +- image: test.registry/baz-operator/baz:v1.0.1 + name: operator +schema: olm.bundle +--- +image: test.registry/baz-operator/baz-bundle:v1.1.0 +name: baz.v1.1.0 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhei52MS4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmF6Iiwia2luZCI6IkJheiIsIm5hbWUiOiJiYXpzLnRlc3QuYmF6IiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhei1vcGVyYXRvci9iYXo6djEuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhei52MS4wLjAiLCJ2ZXJzaW9uIjoiMS4xLjAifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.1.0 +- type: olm.skips + value: baz.v1.0.1 +relatedImages: +- image: test.registry/baz-operator/baz:v1.1.0 + name: operator +schema: olm.bundle +--- +defaultChannel: beta +name: foo +schema: olm.package +--- +image: test.registry/foo-operator/foo-bundle:v0.3.1 +name: foo.v0.3.1 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYyIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvby52MC4zLjEifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYxIn0seyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYyIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Zvby1vcGVyYXRvci9mb286djAuMy4xIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImZvby52MC4yLjAiLCJza2lwcyI6WyJmb28udjAuMy4wIl0sInZlcnNpb24iOiIwLjMuMSJ9fQ== +- type: olm.channel + value: + name: beta + replaces: foo.v0.2.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v2 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.3.1 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.2.0 +- type: olm.skips + value: foo.v0.3.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.3.1 + name: operator +schema: olm.bundle diff --git a/internal/action/testdata/index-declcfgs/latest/index.yaml b/internal/action/testdata/index-declcfgs/latest/index.yaml new file mode 100644 index 000000000..b8e4782d6 --- /dev/null +++ b/internal/action/testdata/index-declcfgs/latest/index.yaml @@ -0,0 +1,362 @@ +--- +defaultChannel: stable +name: bar +schema: olm.package +--- +image: test.registry/bar-operator/bar-bundle:v0.1.0 +name: bar.v0.1.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MC4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djAuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJ2ZXJzaW9uIjoiMC4xLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.1.0 + name: operator +schema: olm.bundle +--- +image: test.registry/bar-operator/bar-bundle:v0.2.0 +name: bar.v0.2.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMi4wIn0sIm5hbWUiOiJiYXIudjAuMi4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJhciIsImtpbmQiOiJCYXIiLCJuYW1lIjoiYmFycy50ZXN0LmJhciIsInZlcnNpb24iOiJ2MWFscGhhMSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXItb3BlcmF0b3IvYmFyOnYwLjIuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmFyLnYwLjEuMCJdLCJ2ZXJzaW9uIjoiMC4yLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.2.0 +- type: olm.skipRange + value: <0.2.0 +- type: olm.skips + value: bar.v0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.2.0 + name: operator +schema: olm.bundle +--- +image: test.registry/bar-operator/bar-bundle:v1.0.0 +name: bar.v1.0.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYxIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MS4wLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn0seyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djEuMC4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhci52MC4yLjAiLCJ2ZXJzaW9uIjoiMS4wLjAifX0= +- type: olm.channel + value: + name: alpha + replaces: bar.v0.2.0 +- type: olm.channel + value: + name: stable +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1 +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 1.0.0 +relatedImages: +- image: test.registry/bar-operator/bar:v1.0.0 + name: operator +schema: olm.bundle +--- +defaultChannel: stable +name: baz +schema: olm.package +--- +image: test.registry/baz-operator/baz-bundle:v1.0.0 +name: baz.v1.0.0 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhei52MS4wLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmF6Iiwia2luZCI6IkJheiIsIm5hbWUiOiJiYXpzLnRlc3QuYmF6IiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhei1vcGVyYXRvci9iYXo6djEuMC4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJ2ZXJzaW9uIjoiMS4wLjAifX0= +- type: olm.channel + value: + name: stable +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.0.0 +- type: olm.skipRange + value: <1.0.0 +relatedImages: +- image: test.registry/baz-operator/baz:v1.0.0 + name: operator +schema: olm.bundle +--- +image: test.registry/baz-operator/baz-bundle:v1.0.1 +name: baz.v1.0.1 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzEuMC4xIn0sIm5hbWUiOiJiYXoudjEuMC4xIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJheiIsImtpbmQiOiJCYXoiLCJuYW1lIjoiYmF6cy50ZXN0LmJheiIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXotb3BlcmF0b3IvYmF6OnYxLjAuMSIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmF6LnYxLjAuMCJdLCJ2ZXJzaW9uIjoiMS4wLjEifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.0.1 +- type: olm.skipRange + value: <1.0.0 +relatedImages: +- image: test.registry/baz-operator/baz:v1.0.1 + name: operator +schema: olm.bundle +--- +image: test.registry/baz-operator/baz-bundle:v1.1.0 +name: baz.v1.1.0 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhei52MS4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmF6Iiwia2luZCI6IkJheiIsIm5hbWUiOiJiYXpzLnRlc3QuYmF6IiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhei1vcGVyYXRvci9iYXo6djEuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImJhei52MS4wLjAiLCJ2ZXJzaW9uIjoiMS4xLjAifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.1.0 +- type: olm.skips + value: baz.v1.0.1 +relatedImages: +- image: test.registry/baz-operator/baz:v1.1.0 + name: operator +schema: olm.bundle +--- +defaultChannel: beta +name: foo +schema: olm.package +--- +image: test.registry/foo-operator/foo-bundle:v0.1.0 +name: foo.v0.1.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMS4wIn0sIm5hbWUiOiJmb28udjAuMS4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmZvbyIsImtpbmQiOiJGb28iLCJuYW1lIjoiZm9vcy50ZXN0LmZvbyIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9mb28tb3BlcmF0b3IvZm9vOnYwLjEuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwidmVyc2lvbiI6IjAuMS4wIn19 +- type: olm.channel + value: + name: beta +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.1.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.1.0 +- type: olm.skipRange + value: <0.1.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.1.0 + name: operator +schema: olm.bundle +--- +image: test.registry/foo-operator/foo-bundle:v0.2.0 +name: foo.v0.2.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMi4wIn0sIm5hbWUiOiJmb28udjAuMi4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmZvbyIsImtpbmQiOiJGb28iLCJuYW1lIjoiZm9vcy50ZXN0LmZvbyIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9mb28tb3BlcmF0b3IvZm9vOnYwLjIuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwicmVwbGFjZXMiOiJmb28udjAuMS4wIiwic2tpcHMiOlsiZm9vLnYwLjEuMSIsImZvby52MC4xLjIiXSwidmVyc2lvbiI6IjAuMi4wIn19 +- type: olm.channel + value: + name: beta + replaces: foo.v0.1.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.2.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.1.0 +- type: olm.skipRange + value: <0.2.0 +- type: olm.skips + value: foo.v0.1.1 +- type: olm.skips + value: foo.v0.1.2 +relatedImages: +- image: test.registry/foo-operator/foo:v0.2.0 + name: operator +schema: olm.bundle +--- +image: test.registry/foo-operator/foo-bundle:v0.3.0 +name: foo.v0.3.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYyIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvby52MC4zLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYxIn0seyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYyIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Zvby1vcGVyYXRvci9mb286djAuMy4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImZvby52MC4yLjAiLCJ2ZXJzaW9uIjoiMC4zLjAifX0= +- type: olm.channel + value: + name: beta + replaces: foo.v0.2.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v2 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.3.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.2.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.3.0 + name: operator +schema: olm.bundle +--- +image: test.registry/foo-operator/foo-bundle:v0.3.1 +name: foo.v0.3.1 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYyIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvby52MC4zLjEifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYxIn0seyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYyIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Zvby1vcGVyYXRvci9mb286djAuMy4xIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImZvby52MC4yLjAiLCJza2lwcyI6WyJmb28udjAuMy4wIl0sInZlcnNpb24iOiIwLjMuMSJ9fQ== +- type: olm.channel + value: + name: beta + replaces: foo.v0.2.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v2 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.3.1 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.2.0 +- type: olm.skips + value: foo.v0.3.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.3.1 + name: operator +schema: olm.bundle diff --git a/internal/action/testdata/index-declcfgs/old/index.yaml b/internal/action/testdata/index-declcfgs/old/index.yaml new file mode 100644 index 000000000..b97ccacbc --- /dev/null +++ b/internal/action/testdata/index-declcfgs/old/index.yaml @@ -0,0 +1,254 @@ +--- +defaultChannel: alpha +name: bar +schema: olm.package +--- +image: test.registry/bar-operator/bar-bundle:v0.1.0 +name: bar.v0.1.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhci52MC4xLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmFyIiwia2luZCI6IkJhciIsIm5hbWUiOiJiYXJzLnRlc3QuYmFyIiwidmVyc2lvbiI6InYxYWxwaGExIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhci1vcGVyYXRvci9iYXI6djAuMS4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJ2ZXJzaW9uIjoiMC4xLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.1.0 + name: operator +schema: olm.bundle +--- +image: test.registry/bar-operator/bar-bundle:v0.2.0 +name: bar.v0.2.0 +package: bar +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhcnMudGVzdC5iYXIifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmFyIiwibmFtZXMiOnsia2luZCI6IkJhciIsInBsdXJhbCI6ImJhcnMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MWFscGhhMSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMi4wIn0sIm5hbWUiOiJiYXIudjAuMi4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJhciIsImtpbmQiOiJCYXIiLCJuYW1lIjoiYmFycy50ZXN0LmJhciIsInZlcnNpb24iOiJ2MWFscGhhMSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXItb3BlcmF0b3IvYmFyOnYwLjIuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmFyLnYwLjEuMCJdLCJ2ZXJzaW9uIjoiMC4yLjAifX0= +- type: olm.channel + value: + name: alpha +- type: olm.gvk + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: bar + version: 0.2.0 +- type: olm.skipRange + value: <0.2.0 +- type: olm.skips + value: bar.v0.1.0 +relatedImages: +- image: test.registry/bar-operator/bar:v0.2.0 + name: operator +schema: olm.bundle +--- +defaultChannel: stable +name: baz +schema: olm.package +--- +image: test.registry/baz-operator/baz-bundle:v1.0.0 +name: baz.v1.0.0 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhei52MS4wLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuYmF6Iiwia2luZCI6IkJheiIsIm5hbWUiOiJiYXpzLnRlc3QuYmF6IiwidmVyc2lvbiI6InYxIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Jhei1vcGVyYXRvci9iYXo6djEuMC4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJ2ZXJzaW9uIjoiMS4wLjAifX0= +- type: olm.channel + value: + name: stable +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.0.0 +- type: olm.skipRange + value: <1.0.0 +relatedImages: +- image: test.registry/baz-operator/baz:v1.0.0 + name: operator +schema: olm.bundle +--- +image: test.registry/baz-operator/baz-bundle:v1.0.1 +name: baz.v1.0.1 +package: baz +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImJhenMudGVzdC5iYXoifSwic3BlYyI6eyJncm91cCI6InRlc3QuYmF6IiwibmFtZXMiOnsia2luZCI6IkJheiIsInBsdXJhbCI6ImJhenMifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzEuMC4xIn0sIm5hbWUiOiJiYXoudjEuMC4xIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmJheiIsImtpbmQiOiJCYXoiLCJuYW1lIjoiYmF6cy50ZXN0LmJheiIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9iYXotb3BlcmF0b3IvYmF6OnYxLjAuMSIsIm5hbWUiOiJvcGVyYXRvciJ9XSwic2tpcHMiOlsiYmF6LnYxLjAuMCJdLCJ2ZXJzaW9uIjoiMS4wLjEifX0= +- type: olm.channel + value: + name: stable + replaces: baz.v1.0.0 +- type: olm.gvk + value: + group: test.baz + kind: Baz + version: v1 +- type: olm.package + value: + packageName: baz + version: 1.0.1 +- type: olm.skipRange + value: <1.0.0 +- type: olm.skips + value: baz.v1.0.0 +relatedImages: +- image: test.registry/baz-operator/baz:v1.0.1 + name: operator +schema: olm.bundle +--- +defaultChannel: beta +name: foo +schema: olm.package +--- +image: test.registry/foo-operator/foo-bundle:v0.1.0 +name: foo.v0.1.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMS4wIn0sIm5hbWUiOiJmb28udjAuMS4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmZvbyIsImtpbmQiOiJGb28iLCJuYW1lIjoiZm9vcy50ZXN0LmZvbyIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9mb28tb3BlcmF0b3IvZm9vOnYwLjEuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwidmVyc2lvbiI6IjAuMS4wIn19 +- type: olm.channel + value: + name: beta +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.1.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.1.0 +- type: olm.skipRange + value: <0.1.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.1.0 + name: operator +schema: olm.bundle +--- +image: test.registry/foo-operator/foo-bundle:v0.2.0 +name: foo.v0.2.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSJ9XX19 +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsiYW5ub3RhdGlvbnMiOnsib2xtLnNraXBSYW5nZSI6Ilx1MDAzYzAuMi4wIn0sIm5hbWUiOiJmb28udjAuMi4wIn0sInNwZWMiOnsiY3VzdG9tcmVzb3VyY2VkZWZpbml0aW9ucyI6eyJvd25lZCI6W3siZ3JvdXAiOiJ0ZXN0LmZvbyIsImtpbmQiOiJGb28iLCJuYW1lIjoiZm9vcy50ZXN0LmZvbyIsInZlcnNpb24iOiJ2MSJ9XX0sInJlbGF0ZWRJbWFnZXMiOlt7ImltYWdlIjoidGVzdC5yZWdpc3RyeS9mb28tb3BlcmF0b3IvZm9vOnYwLjIuMCIsIm5hbWUiOiJvcGVyYXRvciJ9XSwicmVwbGFjZXMiOiJmb28udjAuMS4wIiwic2tpcHMiOlsiZm9vLnYwLjEuMSIsImZvby52MC4xLjIiXSwidmVyc2lvbiI6IjAuMi4wIn19 +- type: olm.channel + value: + name: beta + replaces: foo.v0.1.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.2.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.1.0 +- type: olm.skipRange + value: <0.2.0 +- type: olm.skips + value: foo.v0.1.1 +- type: olm.skips + value: foo.v0.1.2 +relatedImages: +- image: test.registry/foo-operator/foo:v0.2.0 + name: operator +schema: olm.bundle +--- +image: test.registry/foo-operator/foo-bundle:v0.3.0 +name: foo.v0.3.0 +package: foo +properties: +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoiYXBpZXh0ZW5zaW9ucy5rOHMuaW8vdjEiLCJraW5kIjoiQ3VzdG9tUmVzb3VyY2VEZWZpbml0aW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvb3MudGVzdC5mb28ifSwic3BlYyI6eyJncm91cCI6InRlc3QuZm9vIiwibmFtZXMiOnsia2luZCI6IkZvbyIsInBsdXJhbCI6ImZvb3MifSwidmVyc2lvbnMiOlt7Im5hbWUiOiJ2MSIsInNlcnZlZCI6dHJ1ZSwic3RvcmFnZSI6ZmFsc2V9LHsibmFtZSI6InYyIiwic2VydmVkIjp0cnVlLCJzdG9yYWdlIjp0cnVlfV19fQ== +- type: olm.bundle.object + value: + data: eyJhcGlWZXJzaW9uIjoib3BlcmF0b3JzLmNvcmVvcy5jb20vdjFhbHBoYTEiLCJraW5kIjoiQ2x1c3RlclNlcnZpY2VWZXJzaW9uIiwibWV0YWRhdGEiOnsibmFtZSI6ImZvby52MC4zLjAifSwic3BlYyI6eyJjdXN0b21yZXNvdXJjZWRlZmluaXRpb25zIjp7Im93bmVkIjpbeyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYxIn0seyJncm91cCI6InRlc3QuZm9vIiwia2luZCI6IkZvbyIsIm5hbWUiOiJmb29zLnRlc3QuZm9vIiwidmVyc2lvbiI6InYyIn1dfSwicmVsYXRlZEltYWdlcyI6W3siaW1hZ2UiOiJ0ZXN0LnJlZ2lzdHJ5L2Zvby1vcGVyYXRvci9mb286djAuMy4wIiwibmFtZSI6Im9wZXJhdG9yIn1dLCJyZXBsYWNlcyI6ImZvby52MC4yLjAiLCJ2ZXJzaW9uIjoiMC4zLjAifX0= +- type: olm.channel + value: + name: beta + replaces: foo.v0.2.0 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v1 +- type: olm.gvk + value: + group: test.foo + kind: Foo + version: v2 +- type: olm.gvk.required + value: + group: test.bar + kind: Bar + version: v1alpha1 +- type: olm.package + value: + packageName: foo + version: 0.3.0 +- type: olm.package.required + value: + packageName: bar + versionRange: <0.2.0 +relatedImages: +- image: test.registry/foo-operator/foo:v0.3.0 + name: operator +schema: olm.bundle diff --git a/internal/declcfg/declcfg.go b/internal/declcfg/declcfg.go index c0b8635a1..0da0c61e7 100644 --- a/internal/declcfg/declcfg.go +++ b/internal/declcfg/declcfg.go @@ -30,13 +30,22 @@ type Icon struct { MediaType string `json:"mediatype"` } +// Bundle specifies all metadata and data of a bundle object. +// Top-level fields are the source of truth, i.e. not CSV values. +// +// Notes: +// - Any field slice type field or type containing a slice somewhere +// where two types/fields are equal if their contents are equal regardless +// of order must have a `hash:"set"` field tag for bundle comparison. +// - Any fields that have a `json:"-"` tag must be included in the equality +// evaluation in bundlesEqual(). type Bundle struct { Schema string `json:"schema"` Name string `json:"name"` Package string `json:"package"` Image string `json:"image"` - Properties []property.Property `json:"properties,omitempty"` - RelatedImages []RelatedImage `json:"relatedImages,omitempty"` + Properties []property.Property `json:"properties,omitempty" hash:"set"` + RelatedImages []RelatedImage `json:"relatedImages,omitempty" hash:"set"` // These fields are present so that we can continue serving // the GRPC API the way packageserver expects us to in a diff --git a/internal/declcfg/declcfg_to_model.go b/internal/declcfg/declcfg_to_model.go index 4a5e12b69..9150627c2 100644 --- a/internal/declcfg/declcfg_to_model.go +++ b/internal/declcfg/declcfg_to_model.go @@ -1,8 +1,10 @@ package declcfg import ( + "encoding/json" "fmt" + "github.com/blang/semver" "github.com/operator-framework/operator-registry/internal/model" "github.com/operator-framework/operator-registry/internal/property" ) @@ -66,6 +68,23 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) { } mpkg.Channels[bundleChannel.Name] = pkgChannel } + + // Parse version from the package property, falling back to the CSV's spec.version field. + var ver semver.Version + for _, pkgProp := range props.Packages { + if pkgProp.PackageName == mpkg.Name && pkgProp.Version != "" { + if ver, err = semver.Parse(pkgProp.Version); err != nil { + return nil, fmt.Errorf("error parsing bundle version: %v", err) + } + break + } + } + if ver.Equals(semver.Version{}) { + if ver, err = getCSVVersion([]byte(b.CsvJSON)); err != nil { + return nil, fmt.Errorf("error reading bundle version from CSV: %v", err) + } + } + pkgChannel.Bundles[b.Name] = &model.Bundle{ Package: mpkg, Channel: pkgChannel, @@ -77,6 +96,8 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) { RelatedImages: relatedImagesToModelRelatedImages(b.RelatedImages), CsvJSON: b.CsvJSON, Objects: b.Objects, + PropertiesP: props, + Version: ver, } } } @@ -119,3 +140,13 @@ func relatedImagesToModelRelatedImages(in []RelatedImage) []model.RelatedImage { } return out } + +func getCSVVersion(csvJSON []byte) (semver.Version, error) { + var tmp struct { + Spec struct { + Version semver.Version `json:"version"` + } `json:"spec"` + } + err := json.Unmarshal(csvJSON, &tmp) + return tmp.Spec.Version, err +} diff --git a/internal/declcfg/diff.go b/internal/declcfg/diff.go new file mode 100644 index 000000000..f1fb9259b --- /dev/null +++ b/internal/declcfg/diff.go @@ -0,0 +1,393 @@ +package declcfg + +import ( + "reflect" + "sort" + + "github.com/blang/semver" + "github.com/mitchellh/hashstructure/v2" + + "github.com/operator-framework/operator-registry/internal/model" + "github.com/operator-framework/operator-registry/internal/property" +) + +// Diff returns a Model containing everything in newModel not in oldModel, +// and all bundles that exist in oldModel but are different in newModel. +// If oldModel is empty, only channel heads in newModel's packages are +// added to the output Model. All dependencies not in oldModel are also added. +func Diff(oldModel, newModel model.Model) (model.Model, error) { + + // TODO(estroz): loading both oldModel and newModel into memory may + // exceed process/hardware limits. Instead, store models on-disk then + // load by package. + + outputModel := model.Model{} + if len(oldModel) == 0 { + // Heads-only mode. + + // Make shallow copies of packages and channels that are only + // filled with channel heads. + for _, newPkg := range newModel { + outputPkg := copyPackageNoChannels(newPkg) + outputModel[outputPkg.Name] = outputPkg + for _, newCh := range newPkg.Channels { + outputCh := copyChannelNoBundles(newCh, outputPkg) + outputPkg.Channels[outputCh.Name] = outputCh + head, err := newCh.Head() + if err != nil { + return nil, err + } + outputBundle := copyBundle(head, outputCh, outputPkg) + outputModel.AddBundle(*outputBundle) + } + } + } else { + // Latest mode. + + // Copy newModel to create an output model by deletion, + // which is more succinct than by addition and potentially + // more memory efficient. + for _, newPkg := range newModel { + outputModel[newPkg.Name] = copyPackage(newPkg) + } + + // NB(estroz): if a net-new package or channel is published, + // this currently adds the entire package. I'm fairly sure + // this behavior is ok because the next diff after a new + // package is published still has only new data. + for _, outputPkg := range outputModel { + oldPkg, oldHasPkg := oldModel[outputPkg.Name] + if !oldHasPkg { + // outputPkg was already copied to outputModel above. + continue + } + if err := diffPackages(oldPkg, outputPkg); err != nil { + return nil, err + } + if len(outputPkg.Channels) == 0 { + // Remove empty packages. + delete(outputModel, outputPkg.Name) + } + } + } + + // Add dependencies to outputModel not already present in oldModel. + if err := addAllDependencies(newModel, oldModel, outputModel); err != nil { + return nil, err + } + + // Default channel may not have been copied, so set it to the new default channel here. + for _, outputPkg := range outputModel { + outputHasDefault := false + newPkg := newModel[outputPkg.Name] + outputPkg.DefaultChannel, outputHasDefault = outputPkg.Channels[newPkg.DefaultChannel.Name] + if !outputHasDefault { + // Create a name-only channel since oldModel contains the channel already. + outputPkg.DefaultChannel = copyChannelNoBundles(newPkg.DefaultChannel, outputPkg) + } + } + + return outputModel, nil +} + +// diffPackages removes any bundles and channels from newPkg that +// are in oldPkg, but not those that differ in any way. +func diffPackages(oldPkg, newPkg *model.Package) error { + for _, newCh := range newPkg.Channels { + oldCh, oldHasCh := oldPkg.Channels[newCh.Name] + if !oldHasCh { + // newCh is assumed to have been copied to outputModel by the caller. + continue + } + + for _, newBundle := range newCh.Bundles { + oldBundle, oldHasBundle := oldCh.Bundles[newBundle.Name] + if !oldHasBundle { + // newBundle is copied to outputModel by the caller if it is a channel head. + continue + } + equal, err := bundlesEqual(oldBundle, newBundle) + if err != nil { + return err + } + if equal { + delete(newCh.Bundles, newBundle.Name) + } + } + if len(newCh.Bundles) == 0 { + // Remove empty channels. + delete(newPkg.Channels, newCh.Name) + } + } + + return nil +} + +// bundlesEqual computes then compares the hashes of b1 and b2 for equality. +func bundlesEqual(b1, b2 *model.Bundle) (bool, error) { + // Use a declarative config bundle type to avoid infinite recursion. + dcBundle1 := convertFromModelBundle(b1) + dcBundle2 := convertFromModelBundle(b2) + + hash1, err := hashstructure.Hash(dcBundle1, hashstructure.FormatV2, nil) + if err != nil { + return false, err + } + hash2, err := hashstructure.Hash(dcBundle2, hashstructure.FormatV2, nil) + if err != nil { + return false, err + } + // CsvJSON and Objects are ignored by Hash, so they must be compared separately. + return hash1 == hash2 && b1.CsvJSON == b2.CsvJSON && reflect.DeepEqual(b1.Objects, b2.Objects), nil +} + +func addAllDependencies(newModel, oldModel, outputModel model.Model) error { + // Get every oldModel's bundle's dependencies, and their dependencies, etc. by BFS. + allProvidingBundles := []*model.Bundle{} + for curr := getBundles(outputModel); len(curr) != 0; { + reqGVKs, reqPkgs, err := findDependencies(curr) + if err != nil { + return err + } + // Break early so the entire source model is not iterated through unnecessarily. + if len(reqGVKs) == 0 && len(reqPkgs) == 0 { + break + } + curr = nil + // Get bundles that provide dependencies from newModel, which should have + // the latest bundles of each dependency package. + for _, pkg := range newModel { + providingBundles := getBundlesThatProvide(pkg, reqGVKs, reqPkgs) + curr = append(curr, providingBundles...) + allProvidingBundles = append(allProvidingBundles, providingBundles...) + } + } + + // Add the diff between an oldModel dependency package and its new counterpart + // or the entire package if oldModel does not have it. + // + // TODO(estroz): add bundles then fill in dependency upgrade graph + // by selecting latest versions, as the EP specifies. + dependencyPkgs := map[string]*model.Package{} + for _, b := range allProvidingBundles { + if _, copied := dependencyPkgs[b.Package.Name]; !copied { + dependencyPkgs[b.Package.Name] = copyPackage(b.Package) + } + } + for _, newDepPkg := range dependencyPkgs { + // newDepPkg is a copy of a newModel pkg, so running diffPackages + // on it and oldPkg, which may have some but not all bundles, + // will produce a set of all bundles that outputModel doesn't have. + // Otherwise, just add the whole package. + if oldPkg, oldHasPkg := oldModel[newDepPkg.Name]; oldHasPkg { + if err := diffPackages(oldPkg, newDepPkg); err != nil { + return err + } + if len(newDepPkg.Channels) == 0 { + // Skip empty packages. + continue + } + } + outputModel[newDepPkg.Name] = newDepPkg + } + + return nil +} + +// getBundles collects all bundles specified by m. Since each bundle +// references its package, their uniqueness property holds in a flat list. +func getBundles(m model.Model) (bundles []*model.Bundle) { + for _, pkg := range m { + for _, ch := range pkg.Channels { + for _, b := range ch.Bundles { + b := b + bundles = append(bundles, b) + } + } + } + return bundles +} + +// findDependencies finds all GVK and package dependencies and indexes them +// by the apropriate key for lookups. +func findDependencies(bundles []*model.Bundle) (map[property.GVK]struct{}, map[string][]semver.Range, error) { + // Find all dependencies of bundles in the output model. + reqGVKs := map[property.GVK]struct{}{} + reqPkgs := map[string][]semver.Range{} + for _, b := range bundles { + + for _, gvkReq := range b.PropertiesP.GVKsRequired { + gvk := property.GVK{ + Group: gvkReq.Group, + Version: gvkReq.Version, + Kind: gvkReq.Kind, + } + reqGVKs[gvk] = struct{}{} + } + + for _, pkgReq := range b.PropertiesP.PackagesRequired { + var inRange semver.Range + if pkgReq.VersionRange != "" { + var err error + if inRange, err = semver.ParseRange(pkgReq.VersionRange); err != nil { + // Should never happen since model has been validated. + return nil, nil, err + } + } else { + // Any bundle in this package will satisfy a range-less package requirement. + inRange = func(semver.Version) bool { return true } + } + reqPkgs[pkgReq.PackageName] = append(reqPkgs[pkgReq.PackageName], inRange) + } + } + + return reqGVKs, reqPkgs, nil +} + +// getBundlesThatProvide returns the latest-version bundles in pkg that provide +// a GVK or version in reqGVKs or reqPkgs, respectively. +func getBundlesThatProvide(pkg *model.Package, reqGVKs map[property.GVK]struct{}, reqPkgs map[string][]semver.Range) (providingBundles []*model.Bundle) { + // Pre-allocate the amount of space needed for all ranges + // specified by requiring bundles. + var bundlesByRange [][]*model.Bundle + ranges, isPkgRequired := reqPkgs[pkg.Name] + if isPkgRequired { + bundlesByRange = make([][]*model.Bundle, len(ranges)) + } + // Collect package bundles that provide a GVK or are in a range. + bundlesProvidingGVK := make(map[property.GVK][]*model.Bundle) + for _, ch := range pkg.Channels { + for _, b := range ch.Bundles { + b := b + for _, gvk := range b.PropertiesP.GVKs { + if _, hasGVK := reqGVKs[gvk]; hasGVK { + bundlesProvidingGVK[gvk] = append(bundlesProvidingGVK[gvk], b) + } + } + for i, inRange := range ranges { + if inRange(b.Version) { + bundlesByRange[i] = append(bundlesByRange[i], b) + } + } + } + } + + // Sort bundles providing a GVK by version and use the latest version. + latestBundles := make(map[string]*model.Bundle) + for gvk, bundles := range bundlesProvidingGVK { + sort.Slice(bundles, func(i, j int) bool { + return bundles[i].Version.LT(bundles[j].Version) + }) + lb := bundles[len(bundles)-1] + latestBundles[lb.Version.String()] = lb + delete(reqGVKs, gvk) + } + + // Sort bundles in a range by version and use the latest version. + unsatisfiedRanges := []semver.Range{} + for i, bundlesInRange := range bundlesByRange { + if len(bundlesInRange) == 0 { + unsatisfiedRanges = append(unsatisfiedRanges, ranges[i]) + continue + } + sort.Slice(bundlesInRange, func(i, j int) bool { + return bundlesInRange[i].Version.LT(bundlesInRange[j].Version) + }) + lb := bundlesInRange[len(bundlesInRange)-1] + latestBundles[lb.Version.String()] = lb + } + if isPkgRequired && len(unsatisfiedRanges) == 0 { + delete(reqPkgs, pkg.Name) + } + // TODO(estroz): handle missed ranges with logs. + + // Return deduplicated bundles that provide GVKs/versions. + for _, b := range latestBundles { + providingBundles = append(providingBundles, b) + } + return providingBundles +} + +func convertFromModelBundle(b *model.Bundle) Bundle { + return Bundle{ + Schema: schemaBundle, + Name: b.Name, + Package: b.Package.Name, + Image: b.Image, + RelatedImages: modelRelatedImagesToRelatedImages(b.RelatedImages), + CsvJSON: b.CsvJSON, + Objects: b.Objects, + Properties: b.Properties, + } +} + +func copyPackageNoChannels(in *model.Package) *model.Package { + cp := &model.Package{ + Name: in.Name, + Description: in.Description, + Channels: make(map[string]*model.Channel, len(in.Channels)), + } + if in.Icon != nil { + cp.Icon = &model.Icon{ + Data: make([]byte, len(in.Icon.Data)), + MediaType: in.Icon.MediaType, + } + copy(cp.Icon.Data, in.Icon.Data) + } + return cp +} + +func copyPackage(in *model.Package) *model.Package { + cp := copyPackageNoChannels(in) + for _, ch := range in.Channels { + cp.Channels[ch.Name] = copyChannel(ch, cp) + } + return cp +} + +func copyChannelNoBundles(in *model.Channel, pkg *model.Package) *model.Channel { + cp := &model.Channel{ + Name: in.Name, + Package: pkg, + Bundles: make(map[string]*model.Bundle, len(in.Bundles)), + } + return cp +} + +func copyChannel(in *model.Channel, pkg *model.Package) *model.Channel { + cp := copyChannelNoBundles(in, pkg) + for _, b := range in.Bundles { + cp.Bundles[b.Name] = copyBundle(b, cp, pkg) + } + return cp +} + +func copyBundle(in *model.Bundle, ch *model.Channel, pkg *model.Package) *model.Bundle { + cp := &model.Bundle{ + Name: in.Name, + Channel: ch, + Package: pkg, + Image: in.Image, + Replaces: in.Replaces, + Version: semver.MustParse(in.Version.String()), + CsvJSON: in.CsvJSON, + } + cp.PropertiesP, _ = property.Parse(in.Properties) + if len(in.Skips) != 0 { + cp.Skips = make([]string, len(in.Skips)) + copy(cp.Skips, in.Skips) + } + if len(in.Properties) != 0 { + cp.Properties = make([]property.Property, len(in.Properties)) + copy(cp.Properties, in.Properties) + } + if len(in.RelatedImages) != 0 { + cp.RelatedImages = make([]model.RelatedImage, len(in.RelatedImages)) + copy(cp.RelatedImages, in.RelatedImages) + } + if len(in.Objects) != 0 { + cp.Objects = make([]string, len(in.Objects)) + copy(cp.Objects, in.Objects) + } + return cp +} diff --git a/internal/declcfg/diff_test.go b/internal/declcfg/diff_test.go new file mode 100644 index 000000000..c155b8c80 --- /dev/null +++ b/internal/declcfg/diff_test.go @@ -0,0 +1,1051 @@ +package declcfg + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/internal/model" + "github.com/operator-framework/operator-registry/internal/property" +) + +type deprecated struct{} + +const deprecatedType = "olm.deprecated" + +func init() { + property.AddToScheme(deprecatedType, &deprecated{}) +} + +func TestDiffLatest(t *testing.T) { + type spec struct { + name string + oldCfg DeclarativeConfig + newCfg DeclarativeConfig + expCfg DeclarativeConfig + assertion require.ErrorAssertionFunc + } + + specs := []spec{ + { + name: "NoDiff/Empty", + oldCfg: DeclarativeConfig{}, + newCfg: DeclarativeConfig{}, + expCfg: DeclarativeConfig{}, + }, + { + name: "NoDiff/OneEqualBundle", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{}, + }, + { + name: "NoDiff/UnsortedBundleProps", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{}, + }, + { + name: "HasDiff/OneModifiedBundle", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("bar", ">=1.0.0"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("bar", ">=1.0.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/ManyBundlesAndChannels", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("fast", ""), + property.MustBuildPackage("foo", "0.2.0-alpha.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.1", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("fast", "foo.v0.2.0-alpha.0"), + property.MustBuildPackage("foo", "0.2.0-alpha.1"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuild(&deprecated{}), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.2.0"), + property.MustBuildSkips("foo.v0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("fast", ""), + property.MustBuildPackage("foo", "0.2.0-alpha.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.1", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("fast", "foo.v0.2.0-alpha.0"), + property.MustBuildPackage("foo", "0.2.0-alpha.1"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-clusterwide", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("foo", "0.1.0-clusterwide"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuild(&deprecated{}), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-clusterwide", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("foo", "0.1.0-clusterwide"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.2.0"), + property.MustBuildSkips("foo.v0.1.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/OldBundleUpdatedDependencyRange", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/BundleNewDependencyRange", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/NewBundleNewDependencyRange", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-clusterwide", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("foo", "0.1.0-clusterwide"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-clusterwide", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("foo", "0.1.0-clusterwide"), + property.MustBuildPackageRequired("etcd", ">=0.9.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/OneNewDependencyRange", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + }, + }, + }, + { + name: "HasDiff/TwoDependencyRanges", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0 <0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "foo.v0.1.0"), + property.MustBuildPackage("foo", "0.2.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.0 <0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "foo.v0.1.0"), + property.MustBuildPackage("foo", "0.2.0"), + property.MustBuildPackageRequired("etcd", ">=0.9.2"), + }, + }, + }, + }, + }, + { + name: "HasDiff/BundleNewDependencyGVK", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + }, + } + + for _, s := range specs { + t.Run(s.name, func(t *testing.T) { + if s.assertion == nil { + s.assertion = require.NoError + } + + oldModel, err := ConvertToModel(s.oldCfg) + require.NoError(t, err) + + newModel, err := ConvertToModel(s.newCfg) + require.NoError(t, err) + + outputModel, err := Diff(oldModel, newModel) + s.assertion(t, err) + + outputCfg := ConvertFromModel(outputModel) + require.Equal(t, s.expCfg, outputCfg) + }) + } +} + +func TestDiffHeadsOnly(t *testing.T) { + type spec struct { + name string + newCfg DeclarativeConfig + expCfg DeclarativeConfig + assertion require.ErrorAssertionFunc + } + + specs := []spec{ + { + name: "NoDiff/Empty", + newCfg: DeclarativeConfig{}, + expCfg: DeclarativeConfig{}, + }, + { + name: "HasDiff/OneBundle", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + }, + }, + }, + { + name: "HasDiff/Graph", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("alpha", "foo.v0.2.0-alpha.1"), + property.MustBuildChannel("stable", "foo.v0.1.0"), + property.MustBuildPackage("foo", "0.2.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("alpha", ""), + property.MustBuildPackage("foo", "0.2.0-alpha.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.1", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("alpha", "foo.v0.2.0-alpha.0"), + property.MustBuildPackage("foo", "0.2.0-alpha.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.0"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1-clusterwide", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("etcd", "0.9.1-clusterwide"), + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.0"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1-clusterwide", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("clusterwide", ""), + property.MustBuildPackage("etcd", "0.9.1-clusterwide"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("alpha", "foo.v0.2.0-alpha.1"), + property.MustBuildChannel("stable", "foo.v0.1.0"), + property.MustBuildPackage("foo", "0.2.0"), + }, + }, + }, + }, + }, + } + + for _, s := range specs { + t.Run(s.name, func(t *testing.T) { + if s.assertion == nil { + s.assertion = require.NoError + } + + newModel, err := ConvertToModel(s.newCfg) + require.NoError(t, err) + + outputModel, err := Diff(model.Model{}, newModel) + s.assertion(t, err) + + outputCfg := ConvertFromModel(outputModel) + require.Equal(t, s.expCfg, outputCfg) + }) + } +} diff --git a/internal/model/model.go b/internal/model/model.go index 78639d93d..f2bb959de 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/blang/semver" "github.com/h2non/filetype" "github.com/h2non/filetype/matchers" "github.com/h2non/filetype/types" @@ -217,6 +218,10 @@ type Bundle struct { // backwards-compatible way. Objects []string CsvJSON string + + // These fields are used to compare bundles in a diff. + PropertiesP *property.Properties + Version semver.Version } func (b *Bundle) Validate() error { diff --git a/internal/property/property.go b/internal/property/property.go index 5f535eea3..5806a6475 100644 --- a/internal/property/property.go +++ b/internal/property/property.go @@ -121,16 +121,16 @@ func (f File) GetData(root fs.FS, cwd string) ([]byte, error) { } type Properties struct { - Packages []Package - PackagesRequired []PackageRequired - Channels []Channel - GVKs []GVK - GVKsRequired []GVKRequired - Skips []Skips - SkipRanges []SkipRange - BundleObjects []BundleObject + Packages []Package `hash:"set"` + PackagesRequired []PackageRequired `hash:"set"` + Channels []Channel `hash:"set"` + GVKs []GVK `hash:"set"` + GVKsRequired []GVKRequired `hash:"set"` + Skips []Skips `hash:"set"` + SkipRanges []SkipRange `hash:"set"` + BundleObjects []BundleObject `hash:"set"` - Others []Property + Others []Property `hash:"set"` } const ( diff --git a/vendor/github.com/mitchellh/hashstructure/v2/LICENSE b/vendor/github.com/mitchellh/hashstructure/v2/LICENSE new file mode 100644 index 000000000..a3866a291 --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/hashstructure/v2/README.md b/vendor/github.com/mitchellh/hashstructure/v2/README.md new file mode 100644 index 000000000..21f36be19 --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/README.md @@ -0,0 +1,76 @@ +# hashstructure [![GoDoc](https://godoc.org/github.com/mitchellh/hashstructure?status.svg)](https://godoc.org/github.com/mitchellh/hashstructure) + +hashstructure is a Go library for creating a unique hash value +for arbitrary values in Go. + +This can be used to key values in a hash (for use in a map, set, etc.) +that are complex. The most common use case is comparing two values without +sending data across the network, caching values locally (de-dup), and so on. + +## Features + + * Hash any arbitrary Go value, including complex types. + + * Tag a struct field to ignore it and not affect the hash value. + + * Tag a slice type struct field to treat it as a set where ordering + doesn't affect the hash code but the field itself is still taken into + account to create the hash value. + + * Optionally, specify a custom hash function to optimize for speed, collision + avoidance for your data set, etc. + + * Optionally, hash the output of `.String()` on structs that implement fmt.Stringer, + allowing effective hashing of time.Time + + * Optionally, override the hashing process by implementing `Hashable`. + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/hashstructure/v2 +``` + +**Note on v2:** It is highly recommended you use the "v2" release since this +fixes some significant hash collisions issues from v1. In practice, we used +v1 for many years in real projects at HashiCorp and never had issues, but it +is highly dependent on the shape of the data you're hashing and how you use +those hashes. + +When using v2+, you can still generate weaker v1 hashes by using the +`FormatV1` format when calling `Hash`. + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure). + +A quick code example is shown below: + +```go +type ComplexStruct struct { + Name string + Age uint + Metadata map[string]interface{} +} + +v := ComplexStruct{ + Name: "mitchellh", + Age: 64, + Metadata: map[string]interface{}{ + "car": true, + "location": "California", + "siblings": []string{"Bob", "John"}, + }, +} + +hash, err := hashstructure.Hash(v, hashstructure.FormatV2, nil) +if err != nil { + panic(err) +} + +fmt.Printf("%d", hash) +// Output: +// 2307517237273902113 +``` diff --git a/vendor/github.com/mitchellh/hashstructure/v2/errors.go b/vendor/github.com/mitchellh/hashstructure/v2/errors.go new file mode 100644 index 000000000..44b895147 --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/errors.go @@ -0,0 +1,22 @@ +package hashstructure + +import ( + "fmt" +) + +// ErrNotStringer is returned when there's an error with hash:"string" +type ErrNotStringer struct { + Field string +} + +// Error implements error for ErrNotStringer +func (ens *ErrNotStringer) Error() string { + return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) +} + +// ErrFormat is returned when an invalid format is given to the Hash function. +type ErrFormat struct{} + +func (*ErrFormat) Error() string { + return "format must be one of the defined Format values in the hashstructure library" +} diff --git a/vendor/github.com/mitchellh/hashstructure/v2/go.mod b/vendor/github.com/mitchellh/hashstructure/v2/go.mod new file mode 100644 index 000000000..7f7736ce0 --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/mitchellh/hashstructure/v2 + +go 1.14 diff --git a/vendor/github.com/mitchellh/hashstructure/v2/hashstructure.go b/vendor/github.com/mitchellh/hashstructure/v2/hashstructure.go new file mode 100644 index 000000000..3dc0eb74e --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/hashstructure.go @@ -0,0 +1,482 @@ +package hashstructure + +import ( + "encoding/binary" + "fmt" + "hash" + "hash/fnv" + "reflect" + "time" +) + +// HashOptions are options that are available for hashing. +type HashOptions struct { + // Hasher is the hash function to use. If this isn't set, it will + // default to FNV. + Hasher hash.Hash64 + + // TagName is the struct tag to look at when hashing the structure. + // By default this is "hash". + TagName string + + // ZeroNil is flag determining if nil pointer should be treated equal + // to a zero value of pointed type. By default this is false. + ZeroNil bool + + // IgnoreZeroValue is determining if zero value fields should be + // ignored for hash calculation. + IgnoreZeroValue bool + + // SlicesAsSets assumes that a `set` tag is always present for slices. + // Default is false (in which case the tag is used instead) + SlicesAsSets bool + + // UseStringer will attempt to use fmt.Stringer always. If the struct + // doesn't implement fmt.Stringer, it'll fall back to trying usual tricks. + // If this is true, and the "string" tag is also set, the tag takes + // precedence (meaning that if the type doesn't implement fmt.Stringer, we + // panic) + UseStringer bool +} + +// Format specifies the hashing process used. Different formats typically +// generate different hashes for the same value and have different properties. +type Format uint + +const ( + // To disallow the zero value + formatInvalid Format = iota + + // FormatV1 is the format used in v1.x of this library. This has the + // downsides noted in issue #18 but allows simultaneous v1/v2 usage. + FormatV1 + + // FormatV2 is the current recommended format and fixes the issues + // noted in FormatV1. + FormatV2 + + formatMax // so we can easily find the end +) + +// Hash returns the hash value of an arbitrary value. +// +// If opts is nil, then default options will be used. See HashOptions +// for the default values. The same *HashOptions value cannot be used +// concurrently. None of the values within a *HashOptions struct are +// safe to read/write while hashing is being done. +// +// The "format" is required and must be one of the format values defined +// by this library. You should probably just use "FormatV2". This allows +// generated hashes uses alternate logic to maintain compatibility with +// older versions. +// +// Notes on the value: +// +// * Unexported fields on structs are ignored and do not affect the +// hash value. +// +// * Adding an exported field to a struct with the zero value will change +// the hash value. +// +// For structs, the hashing can be controlled using tags. For example: +// +// struct { +// Name string +// UUID string `hash:"ignore"` +// } +// +// The available tag values are: +// +// * "ignore" or "-" - The field will be ignored and not affect the hash code. +// +// * "set" - The field will be treated as a set, where ordering doesn't +// affect the hash code. This only works for slices. +// +// * "string" - The field will be hashed as a string, only works when the +// field implements fmt.Stringer +// +func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) { + // Validate our format + if format <= formatInvalid || format >= formatMax { + return 0, &ErrFormat{} + } + + // Create default options + if opts == nil { + opts = &HashOptions{} + } + if opts.Hasher == nil { + opts.Hasher = fnv.New64() + } + if opts.TagName == "" { + opts.TagName = "hash" + } + + // Reset the hash + opts.Hasher.Reset() + + // Create our walker and walk the structure + w := &walker{ + format: format, + h: opts.Hasher, + tag: opts.TagName, + zeronil: opts.ZeroNil, + ignorezerovalue: opts.IgnoreZeroValue, + sets: opts.SlicesAsSets, + stringer: opts.UseStringer, + } + return w.visit(reflect.ValueOf(v), nil) +} + +type walker struct { + format Format + h hash.Hash64 + tag string + zeronil bool + ignorezerovalue bool + sets bool + stringer bool +} + +type visitOpts struct { + // Flags are a bitmask of flags to affect behavior of this visit + Flags visitFlag + + // Information about the struct containing this field + Struct interface{} + StructField string +} + +var timeType = reflect.TypeOf(time.Time{}) + +func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { + t := reflect.TypeOf(0) + + // Loop since these can be wrapped in multiple layers of pointers + // and interfaces. + for { + // If we have an interface, dereference it. We have to do this up + // here because it might be a nil in there and the check below must + // catch that. + if v.Kind() == reflect.Interface { + v = v.Elem() + continue + } + + if v.Kind() == reflect.Ptr { + if w.zeronil { + t = v.Type().Elem() + } + v = reflect.Indirect(v) + continue + } + + break + } + + // If it is nil, treat it like a zero. + if !v.IsValid() { + v = reflect.Zero(t) + } + + // Binary writing can use raw ints, we have to convert to + // a sized-int, we'll choose the largest... + switch v.Kind() { + case reflect.Int: + v = reflect.ValueOf(int64(v.Int())) + case reflect.Uint: + v = reflect.ValueOf(uint64(v.Uint())) + case reflect.Bool: + var tmp int8 + if v.Bool() { + tmp = 1 + } + v = reflect.ValueOf(tmp) + } + + k := v.Kind() + + // We can shortcut numeric values by directly binary writing them + if k >= reflect.Int && k <= reflect.Complex64 { + // A direct hash calculation + w.h.Reset() + err := binary.Write(w.h, binary.LittleEndian, v.Interface()) + return w.h.Sum64(), err + } + + switch v.Type() { + case timeType: + w.h.Reset() + b, err := v.Interface().(time.Time).MarshalBinary() + if err != nil { + return 0, err + } + + err = binary.Write(w.h, binary.LittleEndian, b) + return w.h.Sum64(), err + } + + switch k { + case reflect.Array: + var h uint64 + l := v.Len() + for i := 0; i < l; i++ { + current, err := w.visit(v.Index(i), nil) + if err != nil { + return 0, err + } + + h = hashUpdateOrdered(w.h, h, current) + } + + return h, nil + + case reflect.Map: + var includeMap IncludableMap + if opts != nil && opts.Struct != nil { + if v, ok := opts.Struct.(IncludableMap); ok { + includeMap = v + } + } + + // Build the hash for the map. We do this by XOR-ing all the key + // and value hashes. This makes it deterministic despite ordering. + var h uint64 + for _, k := range v.MapKeys() { + v := v.MapIndex(k) + if includeMap != nil { + incl, err := includeMap.HashIncludeMap( + opts.StructField, k.Interface(), v.Interface()) + if err != nil { + return 0, err + } + if !incl { + continue + } + } + + kh, err := w.visit(k, nil) + if err != nil { + return 0, err + } + vh, err := w.visit(v, nil) + if err != nil { + return 0, err + } + + fieldHash := hashUpdateOrdered(w.h, kh, vh) + h = hashUpdateUnordered(h, fieldHash) + } + + if w.format != FormatV1 { + // Important: read the docs for hashFinishUnordered + h = hashFinishUnordered(w.h, h) + } + + return h, nil + + case reflect.Struct: + parent := v.Interface() + var include Includable + if impl, ok := parent.(Includable); ok { + include = impl + } + + if impl, ok := parent.(Hashable); ok { + return impl.Hash() + } + + // If we can address this value, check if the pointer value + // implements our interfaces and use that if so. + if v.CanAddr() { + vptr := v.Addr() + parentptr := vptr.Interface() + if impl, ok := parentptr.(Includable); ok { + include = impl + } + + if impl, ok := parentptr.(Hashable); ok { + return impl.Hash() + } + } + + t := v.Type() + h, err := w.visit(reflect.ValueOf(t.Name()), nil) + if err != nil { + return 0, err + } + + l := v.NumField() + for i := 0; i < l; i++ { + if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { + var f visitFlag + fieldType := t.Field(i) + if fieldType.PkgPath != "" { + // Unexported + continue + } + + tag := fieldType.Tag.Get(w.tag) + if tag == "ignore" || tag == "-" { + // Ignore this field + continue + } + + if w.ignorezerovalue { + if innerV.IsZero() { + continue + } + } + + // if string is set, use the string value + if tag == "string" || w.stringer { + if impl, ok := innerV.Interface().(fmt.Stringer); ok { + innerV = reflect.ValueOf(impl.String()) + } else if tag == "string" { + // We only show this error if the tag explicitly + // requests a stringer. + return 0, &ErrNotStringer{ + Field: v.Type().Field(i).Name, + } + } + } + + // Check if we implement includable and check it + if include != nil { + incl, err := include.HashInclude(fieldType.Name, innerV) + if err != nil { + return 0, err + } + if !incl { + continue + } + } + + switch tag { + case "set": + f |= visitFlagSet + } + + kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) + if err != nil { + return 0, err + } + + vh, err := w.visit(innerV, &visitOpts{ + Flags: f, + Struct: parent, + StructField: fieldType.Name, + }) + if err != nil { + return 0, err + } + + fieldHash := hashUpdateOrdered(w.h, kh, vh) + h = hashUpdateUnordered(h, fieldHash) + } + + if w.format != FormatV1 { + // Important: read the docs for hashFinishUnordered + h = hashFinishUnordered(w.h, h) + } + } + + return h, nil + + case reflect.Slice: + // We have two behaviors here. If it isn't a set, then we just + // visit all the elements. If it is a set, then we do a deterministic + // hash code. + var h uint64 + var set bool + if opts != nil { + set = (opts.Flags & visitFlagSet) != 0 + } + l := v.Len() + for i := 0; i < l; i++ { + current, err := w.visit(v.Index(i), nil) + if err != nil { + return 0, err + } + + if set || w.sets { + h = hashUpdateUnordered(h, current) + } else { + h = hashUpdateOrdered(w.h, h, current) + } + } + + if set && w.format != FormatV1 { + // Important: read the docs for hashFinishUnordered + h = hashFinishUnordered(w.h, h) + } + + return h, nil + + case reflect.String: + // Directly hash + w.h.Reset() + _, err := w.h.Write([]byte(v.String())) + return w.h.Sum64(), err + + default: + return 0, fmt.Errorf("unknown kind to hash: %s", k) + } + +} + +func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { + // For ordered updates, use a real hash function + h.Reset() + + // We just panic if the binary writes fail because we are writing + // an int64 which should never be fail-able. + e1 := binary.Write(h, binary.LittleEndian, a) + e2 := binary.Write(h, binary.LittleEndian, b) + if e1 != nil { + panic(e1) + } + if e2 != nil { + panic(e2) + } + + return h.Sum64() +} + +func hashUpdateUnordered(a, b uint64) uint64 { + return a ^ b +} + +// After mixing a group of unique hashes with hashUpdateUnordered, it's always +// necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered +// is a simple XOR, and calling hashUpdateUnordered on hashes produced by +// hashUpdateUnordered can effectively cancel out a previous change to the hash +// result if the same hash value appears later on. For example, consider: +// +// hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) = +// H("A") ^ H("B")) ^ (H("A") ^ H("C")) = +// (H("A") ^ H("A")) ^ (H("B") ^ H(C)) = +// H(B) ^ H(C) = +// hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C")) +// +// hashFinishUnordered "hardens" the result, so that encountering partially +// overlapping input data later on in a different context won't cancel out. +func hashFinishUnordered(h hash.Hash64, a uint64) uint64 { + h.Reset() + + // We just panic if the writes fail + e1 := binary.Write(h, binary.LittleEndian, a) + if e1 != nil { + panic(e1) + } + + return h.Sum64() +} + +// visitFlag is used as a bitmask for affecting visit behavior +type visitFlag uint + +const ( + visitFlagInvalid visitFlag = iota + visitFlagSet = iota << 1 +) diff --git a/vendor/github.com/mitchellh/hashstructure/v2/include.go b/vendor/github.com/mitchellh/hashstructure/v2/include.go new file mode 100644 index 000000000..702d35415 --- /dev/null +++ b/vendor/github.com/mitchellh/hashstructure/v2/include.go @@ -0,0 +1,22 @@ +package hashstructure + +// Includable is an interface that can optionally be implemented by +// a struct. It will be called for each field in the struct to check whether +// it should be included in the hash. +type Includable interface { + HashInclude(field string, v interface{}) (bool, error) +} + +// IncludableMap is an interface that can optionally be implemented by +// a struct. It will be called when a map-type field is found to ask the +// struct if the map item should be included in the hash. +type IncludableMap interface { + HashIncludeMap(field string, k, v interface{}) (bool, error) +} + +// Hashable is an interface that can optionally be implemented by a struct +// to override the hash value. This value will override the hash value for +// the entire struct. Entries in the struct will not be hashed. +type Hashable interface { + Hash() (uint64, error) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4d45ed0f5..2c349dfae 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -320,6 +320,9 @@ github.com/maxbrunsfeld/counterfeiter/v6/command github.com/maxbrunsfeld/counterfeiter/v6/generator # github.com/mitchellh/go-wordwrap v1.0.0 github.com/mitchellh/go-wordwrap +# github.com/mitchellh/hashstructure/v2 v2.0.2 +## explicit +github.com/mitchellh/hashstructure/v2 # github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure # github.com/moby/term v0.0.0-20200312100748-672ec06f55cd @@ -435,6 +438,8 @@ github.com/russross/blackfriday # github.com/sirupsen/logrus v1.6.0 ## explicit github.com/sirupsen/logrus +# github.com/spf13/afero v1.6.0 +## explicit # github.com/spf13/cobra v1.1.1 ## explicit github.com/spf13/cobra From aaac94c84e6dddecdd5ea6cf73a9a42d966412c3 Mon Sep 17 00:00:00 2001 From: Khaled Janania Date: Tue, 27 Jul 2021 09:50:38 -0400 Subject: [PATCH 09/10] some code cleanup removing unused code and commented code Signed-off-by: Khaled Janania --- pkg/lib/registry/registry.go | 2 -- pkg/sqlite/load.go | 45 ------------------------------------ 2 files changed, 47 deletions(-) diff --git a/pkg/lib/registry/registry.go b/pkg/lib/registry/registry.go index 8eb0c1ab5..f0b601ad0 100644 --- a/pkg/lib/registry/registry.go +++ b/pkg/lib/registry/registry.go @@ -8,8 +8,6 @@ import ( "sort" "strings" - // "github.com/blang/semver" - // "github.com/blang/semver/v4" "github.com/sirupsen/logrus" "golang.org/x/mod/semver" utilerrors "k8s.io/apimachinery/pkg/util/errors" diff --git a/pkg/sqlite/load.go b/pkg/sqlite/load.go index 387c029f0..0d46101a9 100644 --- a/pkg/sqlite/load.go +++ b/pkg/sqlite/load.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/json" - "errors" "fmt" "strings" @@ -931,50 +930,6 @@ type CSVNameError struct { PackageVersion string } -var CSVNameNotFound CSVNameError - -func (e *CSVNameError) Error() string { - return fmt.Sprintf("%s: %s %s", e.Err, e.PackageName, e.PackageVersion) -} - -func (s *sqlLoader) getCSVName(tx *sql.Tx, packageName string, version string) (string, error) { - query := `SELECT DISTINCT operatorbundle.name FROM operatorbundle - INNER JOIN channel_entry ON operatorbundle.name=channel_entry.operatorbundle_name - WHERE channel_entry.package_name=? AND operatorbundle.version=?` - getID, err := tx.Prepare(query) - // getID, err := tx.Prepare(` - // SELECT DISTINCT channel_entry.operatorbundle_name - // FROM channel_entry - // WHERE channel_entry.package_name=?`) - - if err != nil { - return "", err - } - defer getID.Close() - rows, err := getID.Query(packageName, version) - - var errs []error - if err != nil { - errs = append(errs, fmt.Errorf("failed query: %s \nerror: %s", query, err)) - return "", utilerrors.NewAggregate(errs) - } - - var csvName sql.NullString - if rows.Next() { - if err := rows.Scan(&csvName); err != nil { - return "", err - } - } else { - return "", &CSVNameError{errors.New("no CSV name found"), packageName, version} - } - - if err := rows.Close(); err != nil { - return "", err - } - - return csvName.String, nil -} - func (s *sqlLoader) RemovePackage(packageName string) error { if err := func() error { tx, err := s.db.Begin() From ed81392e3636962c1b48967f81078ea0104de15c Mon Sep 17 00:00:00 2001 From: Khaled Janania Date: Tue, 27 Jul 2021 14:42:33 -0400 Subject: [PATCH 10/10] Adding tests and removing unused error Signed-off-by: Khaled Janania --- pkg/sqlite/load.go | 6 --- pkg/sqlite/load_test.go | 114 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/pkg/sqlite/load.go b/pkg/sqlite/load.go index 0d46101a9..319046d33 100644 --- a/pkg/sqlite/load.go +++ b/pkg/sqlite/load.go @@ -924,12 +924,6 @@ func (s *sqlLoader) getCSVNames(tx *sql.Tx, packageName string) ([]string, error return csvNames, nil } -type CSVNameError struct { - Err error - PackageName string - PackageVersion string -} - func (s *sqlLoader) RemovePackage(packageName string) error { if err := func() error { tx, err := s.db.Begin() diff --git a/pkg/sqlite/load_test.go b/pkg/sqlite/load_test.go index 9cde831ae..b714fb88f 100644 --- a/pkg/sqlite/load_test.go +++ b/pkg/sqlite/load_test.go @@ -2,6 +2,7 @@ package sqlite import ( "context" + "database/sql" "encoding/json" "fmt" "strings" @@ -577,3 +578,116 @@ func TestAddBundlePropertiesFromAnnotations(t *testing.T) { }) } } + +func TestReplaceChannelHead(t *testing.T) { + type fields struct { + bundles []*registry.Bundle + pkgs []registry.PackageManifest + } + type args struct { + bundle string + } + type expected struct { + err error + alphaChannel string + stableChannel string + } + tests := []struct { + description string + fields fields + args args + expected expected + }{ + { + description: "ContainsDefaultChannel", + fields: fields{ + bundles: []*registry.Bundle{ + newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSV(t, "csv-a", "csv-b")), + newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")), + newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")), + }, + pkgs: []registry.PackageManifest{ + { + PackageName: "pkg-0", + Channels: []registry.PackageChannel{ + { + Name: "alpha", + CurrentCSVName: "csv-a", + }, + { + Name: "stable", + CurrentCSVName: "csv-b", + }, + }, + DefaultChannelName: "stable", + }, + }, + }, + args: args{ + bundle: "csv-a", + }, + expected: expected{ + err: nil, + alphaChannel: "csv-b", + stableChannel: "csv-b", + }, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + db, cleanup := CreateTestDb(t) + defer cleanup() + store, err := NewSQLLiteLoader(db) + require.NoError(t, err) + err = store.Migrate(context.TODO()) + require.NoError(t, err) + + for _, bundle := range tt.fields.bundles { + // Throw away any errors loading bundles (not testing this) + store.AddOperatorBundle(bundle) + } + + for _, pkg := range tt.fields.pkgs { + // Throw away any errors loading packages (not testing this) + store.AddPackageChannels(pkg) + } + tx, err := db.Begin() + require.NoError(t, err) + loader := store.(*sqlLoader) + err = loader.replaceChannelHead(tx, "csv-b", "csv-a") + require.NoError(t, err) + var ( + alphaChannel sql.NullString + stableChannel sql.NullString + ) + newChannelHeadRows, err := tx.Query("SELECT head_operatorbundle_name FROM channel WHERE name = ?", "alpha") + require.NoError(t, err) + if newChannelHeadRows.Next() { + if err := newChannelHeadRows.Scan(&alphaChannel); err != nil { + if nerr := newChannelHeadRows.Close(); nerr != nil { + require.NoError(t, nerr) + } + require.NoError(t, err) + } + } + newChannelHeadRows, err = tx.Query("SELECT head_operatorbundle_name FROM channel WHERE name = ?", "stable") + require.NoError(t, err) + if newChannelHeadRows.Next() { + if err := newChannelHeadRows.Scan(&stableChannel); err != nil { + if nerr := newChannelHeadRows.Close(); nerr != nil { + require.NoError(t, nerr) + } + require.NoError(t, err) + } + } + require.NoError(t, err) + require.Equal(t, tt.expected.err, err) + t.Logf("tt.expected.alphaChannel %#v", tt.expected.alphaChannel) + t.Logf("tt.expected.stableChannel %#v", tt.expected.stableChannel) + t.Logf("actual alphaChannel %#v", alphaChannel.String) + t.Logf("actual stableChannel %#v", stableChannel.String) + require.Equal(t, tt.expected.alphaChannel, alphaChannel.String) + require.Equal(t, tt.expected.stableChannel, stableChannel.String) + }) + } +}