Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ability to deprecate bundles #397

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions bundles/etcd.0.9.0/metadata/annotations.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
annotations:
operators.operatorframework.io.bundle.package.v1: "etcd"
operators.operatorframework.io.bundle.channels.v1: "alpha,stable"
operators.operatorframework.io.bundle.channel.default.v1: "stable"
operators.operatorframework.io.bundle.channels.v1: "alpha,stable,beta"
operators.operatorframework.io.bundle.channel.default.v1: "stable"

1 change: 1 addition & 0 deletions cmd/opm/index/cmd.go
Original file line number Diff line number Diff line change
@@ -25,4 +25,5 @@ func AddCommand(parent *cobra.Command) {
addIndexAddCmd(cmd)
cmd.AddCommand(newIndexExportCmd())
cmd.AddCommand(newIndexPruneCmd())
cmd.AddCommand(newIndexDeprecateTruncateCmd())
}
133 changes: 133 additions & 0 deletions cmd/opm/index/deprecate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package index

import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"

"github.com/operator-framework/operator-registry/pkg/containertools"
"github.com/operator-framework/operator-registry/pkg/lib/indexer"
)

var deprecateLong = templates.LongDesc(`
Deprecate and truncate operator bundles from an index.
Deprecated bundles will no longer be installable. Bundles that are replaced by deprecated bundles will be removed enirely from the index.
For example:
Given the update graph in quay.io/my/index:v1
1.4.0 -- replaces -> 1.3.0 -- replaces -> 1.2.0 -- replaces -> 1.1.0
Applying the command:
opm index deprecate --bundles "quay.io/my/bundle:1.3.0" --from-index "quay.io/my/index:v1" --tag "quay.io/my/index:v2"
Produces the following update graph in quay.io/my/index:v2
1.4.0 -- replaces -> 1.3.0 [deprecated]
Deprecating a bundle that removes the default channel is not allowed. Changing the default channel prior to deprecation is possible by publishing a new bundle to the index.
`)

func newIndexDeprecateTruncateCmd() *cobra.Command {
indexCmd := &cobra.Command{
Hidden: true,
Use: "deprecatetruncate",
Short: "Deprecate and truncate operator bundles from an index.",
Long: deprecateLong,
PreRunE: func(cmd *cobra.Command, args []string) error {
if debug, _ := cmd.Flags().GetBool("debug"); debug {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
},
RunE: runIndexDeprecateTruncateCmdFunc,
}

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", "", "previous index to add to")
indexCmd.Flags().StringSliceP("bundles", "b", nil, "comma separated list of bundles to add")
if err := indexCmd.MarkFlagRequired("bundles"); err != nil {
logrus.Panic("Failed to set required `bundles` flag for `index add`")
}
indexCmd.Flags().StringP("binary-image", "i", "", "container image for on-image `opm` command")
indexCmd.Flags().StringP("container-tool", "c", "", "tool to interact with container images (save, build, etc.). One of: [docker, podman]")
indexCmd.Flags().StringP("build-tool", "u", "", "tool to build container images. One of: [docker, podman]. Defaults to podman. Overrides part of container-tool.")
indexCmd.Flags().StringP("pull-tool", "p", "", "tool to pull container images. One of: [none, docker, podman]. Defaults to none. Overrides part of container-tool.")
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 runIndexDeprecateTruncateCmdFunc(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
}

bundles, err := cmd.Flags().GetStringSlice("bundles")
if err != nil {
return err
}

binaryImage, err := cmd.Flags().GetString("binary-image")
if err != nil {
return err
}

tag, err := cmd.Flags().GetString("tag")
if err != nil {
return err
}

permissive, err := cmd.Flags().GetBool("permissive")
if err != nil {
return err
}

pullTool, buildTool, err := getContainerTools(cmd)
if err != nil {
return err
}

logger := logrus.WithFields(logrus.Fields{"bundles": bundles})

logger.Info("deprecating bundles from the index")

indexDeprecator := indexer.NewIndexDeprecator(
containertools.NewContainerTool(buildTool, containertools.PodmanTool),
containertools.NewContainerTool(pullTool, containertools.NoneTool),
logger)

request := indexer.DeprecateFromIndexRequest{
Generate: generate,
FromIndex: fromIndex,
BinarySourceImage: binaryImage,
OutDockerfile: outDockerfile,
Tag: tag,
Bundles: bundles,
Permissive: permissive,
}

err = indexDeprecator.DeprecateFromIndex(request)
if err != nil {
return err
}

return nil
}
59 changes: 59 additions & 0 deletions pkg/lib/indexer/indexer.go
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ type ImageIndexer struct {
RegistryAdder registry.RegistryAdder
RegistryDeleter registry.RegistryDeleter
RegistryPruner registry.RegistryPruner
RegistryDeprecator registry.RegistryDeprecator
BuildTool containertools.ContainerTool
PullTool containertools.ContainerTool
Logger *logrus.Entry
@@ -527,3 +528,61 @@ func generatePackageYaml(dbQuerier pregistry.Query, packageName, downloadPath st

return utilerrors.NewAggregate(errs)
}

// DeprecateFromIndexRequest defines the parameters to send to the PruneFromIndex API
type DeprecateFromIndexRequest struct {
Generate bool
Permissive bool
BinarySourceImage string
FromIndex string
OutDockerfile string
Bundles []string
Tag string
}

// DeprecateFromIndex takes a DeprecateFromIndexRequest and deprecates the requested
// bundles.
func (i ImageIndexer) DeprecateFromIndex(request DeprecateFromIndexRequest) error {
buildDir, outDockerfile, cleanup, err := buildContext(request.Generate, request.OutDockerfile)
defer cleanup()
if err != nil {
return err
}

databasePath, err := i.extractDatabase(buildDir, request.FromIndex)
if err != nil {
return err
}

// Run opm registry prune on the database
deprecateFromRegistryReq := registry.DeprecateFromRegistryRequest{
Bundles: request.Bundles,
InputDatabase: databasePath,
Permissive: request.Permissive,
}

// Prune the bundles from the registry
err = i.RegistryDeprecator.DeprecateFromRegistry(deprecateFromRegistryReq)
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 with requested tooling
err = build(outDockerfile, request.Tag, i.CommandRunner, i.Logger)
if err != nil {
return err
}

return nil
}
17 changes: 17 additions & 0 deletions pkg/lib/indexer/interfaces.go
Original file line number Diff line number Diff line change
@@ -80,3 +80,20 @@ func NewIndexPruner(containerTool containertools.ContainerTool, logger *logrus.E
Logger: logger,
}
}

// IndexDeprecator prunes operators out of an index
type IndexDeprecator interface {
DeprecateFromIndex(DeprecateFromIndexRequest) error
}

func NewIndexDeprecator(buildTool, pullTool containertools.ContainerTool, logger *logrus.Entry) IndexDeprecator {
return ImageIndexer{
DockerfileGenerator: containertools.NewDockerfileGenerator(logger),
CommandRunner: containertools.NewCommandRunner(buildTool, logger),
LabelReader: containertools.NewLabelReader(pullTool, logger),
RegistryDeprecator: registry.NewRegistryDeprecator(logger),
BuildTool: buildTool,
PullTool: pullTool,
Logger: logger,
}
}
10 changes: 10 additions & 0 deletions pkg/lib/registry/interfaces.go
Original file line number Diff line number Diff line change
@@ -36,3 +36,13 @@ func NewRegistryPruner(logger *logrus.Entry) RegistryPruner {
Logger: logger,
}
}

type RegistryDeprecator interface {
DeprecateFromRegistry(DeprecateFromRegistryRequest) error
}

func NewRegistryDeprecator(logger *logrus.Entry) RegistryDeprecator {
return RegistryUpdater{
Logger: logger,
}
}
34 changes: 34 additions & 0 deletions pkg/lib/registry/registry.go
Original file line number Diff line number Diff line change
@@ -228,3 +228,37 @@ func (r RegistryUpdater) PruneFromRegistry(request PruneFromRegistryRequest) err

return nil
}

type DeprecateFromRegistryRequest struct {
Permissive bool
InputDatabase string
Bundles []string
}

func (r RegistryUpdater) DeprecateFromRegistry(request DeprecateFromRegistryRequest) error {
db, err := sql.Open("sqlite3", 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 fmt.Errorf("unable to migrate database: %s", err)
}

deprecator := sqlite.NewSQLDeprecatorForBundles(dbLoader, request.Bundles)
if err := deprecator.Deprecate(); err != nil {
r.Logger.Debugf("unable to deprecate bundles from database: %s", err)
if !request.Permissive {
r.Logger.WithError(err).Error("permissive mode disabled")
return err
}
r.Logger.WithError(err).Warn("permissive mode enabled")
}

return nil
}
1 change: 1 addition & 0 deletions pkg/registry/interface.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ type Load interface {
AddPackageChannels(manifest PackageManifest) error
AddBundlePackageChannels(manifest PackageManifest, bundle *Bundle) error
RemovePackage(packageName string) error
DeprecateBundle(path string) error
ClearNonHeadBundles() error
}

184 changes: 181 additions & 3 deletions pkg/registry/populator_test.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package registry_test
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/rand"
"os"
@@ -18,6 +19,7 @@ import (
"github.com/operator-framework/operator-registry/pkg/registry"
"github.com/operator-framework/operator-registry/pkg/sqlite"

"k8s.io/apimachinery/pkg/util/errors"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)

@@ -109,6 +111,10 @@ func TestQuerierForImage(t *testing.T) {
Name: "alpha",
CurrentCSVName: "etcdoperator.v0.9.2",
},
{
Name: "beta",
CurrentCSVName: "etcdoperator.v0.9.0",
},
{
Name: "stable",
CurrentCSVName: "etcdoperator.v0.9.2",
@@ -185,12 +191,14 @@ func TestQuerierForImage(t *testing.T) {
{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
{"etcd", "stable", "etcdoperator.v0.9.0", ""},
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.1"},
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"}}, etcdChannelEntriesThatProvide)
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
{"etcd", "beta", "etcdoperator.v0.9.0", ""}}, etcdChannelEntriesThatProvide)

etcdLatestChannelEntriesThatProvide, err := store.GetLatestChannelEntriesThatProvide(context.TODO(), "etcd.database.coreos.com", "v1beta2", "EtcdCluster")
require.NoError(t, err)
require.ElementsMatch(t, []*registry.ChannelEntry{{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"}}, etcdLatestChannelEntriesThatProvide)
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
{"etcd", "beta", "etcdoperator.v0.9.0", ""}}, etcdLatestChannelEntriesThatProvide)

etcdBundleByProvides, err := store.GetBundleThatProvides(context.TODO(), "etcd.database.coreos.com", "v1beta2", "EtcdCluster")
require.NoError(t, err)
@@ -229,7 +237,7 @@ func TestQuerierForImage(t *testing.T) {

listChannels, err := store.ListChannels(context.TODO(), "etcd")
require.NoError(t, err)
expectedListChannels := []string{"alpha", "stable"}
expectedListChannels := []string{"alpha", "stable", "beta"}
require.ElementsMatch(t, expectedListChannels, listChannels)

currentCSVName, err := store.GetCurrentCSVNameForChannel(context.TODO(), "etcd", "alpha")
@@ -670,3 +678,173 @@ func CheckBundlesHaveContentsIfNoPath(t *testing.T, db *sql.DB) {
require.NotZero(t, bundlelen.Int64, "length of bundle for %s should not be zero, it has no bundle path", name.String)
}
}

func TestDeprecateBundle(t *testing.T) {
type args struct {
bundles []string
}
type pkgChannel map[string][]string
type expected struct {
err error
remainingBundles []string
deprecatedBundles []string
remainingPkgChannels pkgChannel
}
tests := []struct {
description string
args args
expected expected
}{
{
description: "BundleDeprecated/IgnoreIfNotInIndex",
args: args{
bundles: []string{
"quay.io/test/etcd.0.6.0",
},
},
expected: expected{
err: errors.NewAggregate([]error{fmt.Errorf("error deprecating bundle quay.io/test/etcd.0.6.0: %s", registry.ErrBundleImageNotInDatabase)}),
remainingBundles: []string{
"quay.io/test/etcd.0.9.0",
"quay.io/test/etcd.0.9.2",
"quay.io/test/prometheus.0.22.2",
"quay.io/test/prometheus.0.14.0",
"quay.io/test/prometheus.0.15.0",
},
deprecatedBundles: []string{},
remainingPkgChannels: pkgChannel{
"etcd": []string{
"beta",
"alpha",
"stable",
},
"prometheus": []string{
"preview",
"stable",
},
},
},
},
{
description: "BundleDeprecated/SingleChannel",
args: args{
bundles: []string{
"quay.io/test/prometheus.0.15.0",
},
},
expected: expected{
err: nil,
remainingBundles: []string{
"quay.io/test/etcd.0.9.0",
"quay.io/test/etcd.0.9.2",
"quay.io/test/prometheus.0.22.2",
"quay.io/test/prometheus.0.15.0",
},
deprecatedBundles: []string{
"quay.io/test/prometheus.0.15.0",
},
remainingPkgChannels: pkgChannel{
"etcd": []string{
"beta",
"alpha",
"stable",
},
"prometheus": []string{
"preview",
"stable",
},
},
},
},
{
description: "BundleDeprecated/ChannelRemoved",
args: args{
bundles: []string{
"quay.io/test/etcd.0.9.2",
},
},
expected: expected{
err: nil,
remainingBundles: []string{
"quay.io/test/etcd.0.9.2",
"quay.io/test/prometheus.0.22.2",
"quay.io/test/prometheus.0.14.0",
"quay.io/test/prometheus.0.15.0",
},
deprecatedBundles: []string{
"quay.io/test/etcd.0.9.2",
},
remainingPkgChannels: pkgChannel{
"etcd": []string{
"alpha",
"stable",
},
"prometheus": []string{
"preview",
"stable",
},
},
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is looking pretty good, but doesn't quite convince me that there are no bugs - at the very least I think this should verify the full end state of the graph, not just the bundles/packages/channels.

I also want to make sure that this covers:

  • bundle is in two channels
  • bundle is in two packages
  • bundle is skipped by another bundle
  • bundle has children that are skipped that get removed

(it might already cover these implicitly, just need to check)

}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
db, cleanup := CreateTestDb(t)
defer cleanup()

querier, err := createAndPopulateDB(db)
require.NoError(t, err)

store, err := sqlite.NewSQLLiteLoader(db)
require.NoError(t, err)

deprecator := sqlite.NewSQLDeprecatorForBundles(store, tt.args.bundles)
err = deprecator.Deprecate()
require.Equal(t, tt.expected.err, err)

// Ensure remaining bundlePaths in db match
bundles, err := querier.ListBundles(context.Background())
require.NoError(t, err)
var bundlePaths []string
for _, bundle := range bundles {
bundlePaths = append(bundlePaths, bundle.BundlePath)
}
require.ElementsMatch(t, tt.expected.remainingBundles, bundlePaths)

// Ensure deprecated bundles match
var deprecatedBundles []string
deprecatedProperty, err := json.Marshal(registry.DeprecatedProperty{})
require.NoError(t, err)
for _, bundle := range bundles {
for _, prop := range bundle.Properties {
if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
deprecatedBundles = append(deprecatedBundles, bundle.BundlePath)
}
}
}

require.ElementsMatch(t, tt.expected.deprecatedBundles, deprecatedBundles)

// Ensure remaining channels match
packages, err := querier.ListPackages(context.Background())
require.NoError(t, err)

for _, pkg := range packages {
channelEntries, err := querier.GetChannelEntriesFromPackage(context.Background(), pkg)
require.NoError(t, err)

uniqueChannels := make(map[string]struct{})
var channels []string
for _, ch := range channelEntries {
uniqueChannels[ch.ChannelName] = struct{}{}
}
for k := range uniqueChannels {
channels = append(channels, k)
}
require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
}
})
}
}
16 changes: 14 additions & 2 deletions pkg/registry/types.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,13 @@ import (
var (
// ErrPackageNotInDatabase is an error that describes a package not found error when querying the registry
ErrPackageNotInDatabase = errors.New("Package not in database")

// ErrBundleImageNotInDatabase is an error that describes a bundle image not found when querying the registry
ErrBundleImageNotInDatabase = errors.New("Bundle Image not in database")

// ErrRemovingDefaultChannelDuringDeprecation is an error that describes a bundle deprecation causing the deletion
// of the default channel
ErrRemovingDefaultChannelDuringDeprecation = errors.New("Bundle deprecation causing default channel removal")
)

// BundleImageAlreadyAddedErr is an error that describes a bundle is already added
@@ -33,8 +40,9 @@ func (e PackageVersionAlreadyAddedErr) Error() string {
}

const (
GVKType = "olm.gvk"
PackageType = "olm.package"
GVKType = "olm.gvk"
PackageType = "olm.package"
DeprecatedType = "olm.deprecated"
)

// APIKey stores GroupVersionKind for use as map keys
@@ -204,6 +212,10 @@ type PackageProperty struct {
Version string `json:"version" yaml:"version"`
}

type DeprecatedProperty struct {
// Whether the bundle is deprecated
}

// Validate will validate GVK dependency type and return error(s)
func (gd *GVKDependency) Validate() []error {
errs := []error{}
49 changes: 49 additions & 0 deletions pkg/sqlite/deprecate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package sqlite

import (
"errors"
"fmt"

"github.com/sirupsen/logrus"
utilerrors "k8s.io/apimachinery/pkg/util/errors"

"github.com/operator-framework/operator-registry/pkg/registry"
)

type SQLDeprecator interface {
Deprecate() error
}

// BundleDeprecator removes bundles from the database
type BundleDeprecator struct {
store registry.Load
bundles []string
}

var _ SQLDeprecator = &BundleDeprecator{}

func NewSQLDeprecatorForBundles(store registry.Load, bundles []string) *BundleDeprecator {
return &BundleDeprecator{
store: store,
bundles: bundles,
}
}

func (d *BundleDeprecator) Deprecate() error {
log := logrus.WithField("bundles", d.bundles)

log.Info("deprecating bundles")

var errs []error

for _, bundlePath := range d.bundles {
if err := d.store.DeprecateBundle(bundlePath); err != nil {
if !errors.Is(err, registry.ErrBundleImageNotInDatabase) && !errors.Is(err, registry.ErrRemovingDefaultChannelDuringDeprecation) {
return utilerrors.NewAggregate(append(errs, fmt.Errorf("error deprecating bundle %s: %s", bundlePath, err)))
}
errs = append(errs, fmt.Errorf("error deprecating bundle %s: %s", bundlePath, err))
}
}

return utilerrors.NewAggregate(errs)
}
167 changes: 167 additions & 0 deletions pkg/sqlite/load.go
Original file line number Diff line number Diff line change
@@ -805,3 +805,170 @@ func (s *sqlLoader) addBundleProperties(tx *sql.Tx, bundle *registry.Bundle) err

return nil
}

func (s *sqlLoader) rmChannelEntry(tx *sql.Tx, csvName string) error {
getEntryID := `SELECT entry_id FROM channel_entry WHERE operatorbundle_name=?`
rows, err := tx.QueryContext(context.TODO(), getEntryID, csvName)
if err != nil {
return err
}
var entryIDs []int64
for rows.Next() {
var entryID sql.NullInt64
rows.Scan(&entryID)
entryIDs = append(entryIDs, entryID.Int64)
}
err = rows.Close()
if err != nil {
return err
}

updateChannelEntry, err := tx.Prepare(`UPDATE channel_entry SET replaces=NULL WHERE replaces=?`)
if err != nil {
return err
}
for _, id := range entryIDs {
if _, err := updateChannelEntry.Exec(id); err != nil {
return err
}
}
err = updateChannelEntry.Close()
if err != nil {
return err
}

stmt, err := tx.Prepare("DELETE FROM channel_entry WHERE operatorbundle_name=?")
if err != nil {
return err
}
defer stmt.Close()

if _, err := stmt.Exec(csvName); err != nil {
return err
}

return nil
}

func getTailFromBundle(tx *sql.Tx, name string) (bundles []string, err error) {
getReplacesSkips := `SELECT replaces, skips FROM operatorbundle WHERE name=?`
isDefaultChannelHead := `SELECT head_operatorbundle_name FROM channel
INNER JOIN package ON channel.name = package.default_channel
WHERE channel.head_operatorbundle_name = ?`

tail := make(map[string]struct{})
next := name

for next != "" {
rows, err := tx.QueryContext(context.TODO(), getReplacesSkips, next)
if err != nil {
return nil, err
}
var replaces sql.NullString
var skips sql.NullString
if rows.Next() {
if err := rows.Scan(&replaces, &skips); err != nil {
return nil, err
}
}
rows.Close()
if skips.Valid && skips.String != "" {
for _, skip := range strings.Split(skips.String, ",") {
tail[skip] = struct{}{}
}
}
if replaces.Valid && replaces.String != "" {
// check if replaces is the head of the defaultChannel
// if it is, the defaultChannel will be removed
// this is not allowed because we cannot know which channel to promote as the new default
rows, err := tx.QueryContext(context.TODO(), isDefaultChannelHead, replaces.String)
if err != nil {
return nil, err
}
if rows.Next() {
var defaultChannelHead sql.NullString
err := rows.Scan(&defaultChannelHead)
if err != nil {
return nil, err
}
if defaultChannelHead.Valid {
return nil, registry.ErrRemovingDefaultChannelDuringDeprecation
}
}
next = replaces.String
tail[replaces.String] = struct{}{}
} else {
next = ""
}
}
var allTails []string

for k := range tail {
allTails = append(allTails, k)
}

return allTails, nil

}

func getBundleNameAndVersionForImage(tx *sql.Tx, path string) (string, string, error) {
query := `SELECT name, version FROM operatorbundle WHERE bundlepath=? LIMIT 1`
rows, err := tx.QueryContext(context.TODO(), query, path)
if err != nil {
return "", "", err
}
defer rows.Close()

var name sql.NullString
var version sql.NullString
if rows.Next() {
if err := rows.Scan(&name, &version); err != nil {
return "", "", err
}
}
if name.Valid && version.Valid {
return name.String, version.String, nil
}
return "", "", registry.ErrBundleImageNotInDatabase
}

func (s *sqlLoader) DeprecateBundle(path string) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer func() {
tx.Rollback()
}()

name, version, err := getBundleNameAndVersionForImage(tx, path)
if err != nil {
return err
}
tailBundles, err := getTailFromBundle(tx, name)
if err != nil {
return err
}

for _, bundle := range tailBundles {
err := s.rmBundle(tx, bundle)
if err != nil {
return err
}
err = s.rmChannelEntry(tx, bundle)
if err != nil {
return err
}
}

deprecatedValue, err := json.Marshal(registry.DeprecatedProperty{})
if err != nil {
return err
}
err = s.addProperty(tx, registry.DeprecatedType, string(deprecatedValue), name, version, path)
if err != nil {
return err
}

return tx.Commit()
}
174 changes: 174 additions & 0 deletions pkg/sqlite/load_test.go
Original file line number Diff line number Diff line change
@@ -223,6 +223,21 @@ func newUnstructuredCSV(t *testing.T, name, replaces string) *unstructured.Unstr
return &unstructured.Unstructured{Object: out}
}

func newUnstructuredCSVwithSkips(t *testing.T, name, replaces string, skips ...string) *unstructured.Unstructured {
csv := &registry.ClusterServiceVersion{}
csv.TypeMeta.Kind = "ClusterServiceVersion"
csv.SetName(name)
allSkips, err := json.Marshal(skips)
require.NoError(t, err)
replacesSkips := fmt.Sprintf(`{"replaces": "%s", "skips": %s}`, replaces, string(allSkips))
t.Logf("%v", replacesSkips)
csv.Spec = json.RawMessage(replacesSkips)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(csv)
require.NoError(t, err)
return &unstructured.Unstructured{Object: out}
}

func newBundle(t *testing.T, name, pkgName string, channels []string, objs ...*unstructured.Unstructured) *registry.Bundle {
bundle := registry.NewBundle(name, pkgName, channels, objs...)

@@ -232,3 +247,162 @@ func newBundle(t *testing.T, name, pkgName string, channels []string, objs ...*u

return bundle
}

func TestGetTailFromBundle(t *testing.T) {
type fields struct {
bundles []*registry.Bundle
pkgs []registry.PackageManifest
}
type args struct {
bundle string
}
type expected struct {
err error
tail []string
}
tests := []struct {
description string
fields fields
args args
expected expected
}{
{
description: "GetTailFromBundle/RemoveDefaultChannelForbidden",
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: registry.ErrRemovingDefaultChannelDuringDeprecation,
tail: nil,
},
},
{
description: "GetTailFromBundle/RemovingNonDefaultChannel",
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: "alpha",
},
},
},
args: args{
bundle: "csv-a",
},
expected: expected{
err: nil,
tail: []string{
"csv-b",
"csv-c",
},
},
},
{
description: "GetTailFromBundle/HandlesSkips",
fields: fields{
bundles: []*registry.Bundle{
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSVwithSkips(t, "csv-a", "csv-b", "csv-d", "csv-e", "csv-f")),
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: "alpha",
},
},
},
args: args{
bundle: "csv-a",
},
expected: expected{
err: nil,
tail: []string{
"csv-b",
"csv-c",
"csv-d",
"csv-e",
"csv-f",
},
},
},
}

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)
tail, err := getTailFromBundle(tx, tt.args.bundle)

require.Equal(t, tt.expected.err, err)
t.Logf("tt.expected.tail %#v", tt.expected.tail)
t.Logf("tail %#v", tail)
require.ElementsMatch(t, tt.expected.tail, tail)
})
}

}
2 changes: 1 addition & 1 deletion pkg/sqlite/query.go
Original file line number Diff line number Diff line change
@@ -779,7 +779,7 @@ func (s *SQLQuerier) GetBundleVersion(ctx context.Context, image string) (string
if version.Valid {
return version.String, nil
}
return "", fmt.Errorf("bundle %s not found", image)
return "", nil
}

func (s *SQLQuerier) GetBundlePathsForPackage(ctx context.Context, pkgName string) ([]string, error) {