Skip to content

Commit dcc7e50

Browse files
committedJul 29, 2020
Adding deprecated property type to bundles
Adding support marking bundles as deprecated and truncating the update graph of a given package. Deprecated versions will not be installable. Deprecating a bundle can result in the removal of channels but this is not permitted for the default channel. Bundles that are not in the index will be ignored during deprecation.
1 parent 3403f43 commit dcc7e50

File tree

14 files changed

+844
-8
lines changed

14 files changed

+844
-8
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
annotations:
22
operators.operatorframework.io.bundle.package.v1: "etcd"
3-
operators.operatorframework.io.bundle.channels.v1: "alpha,stable"
4-
operators.operatorframework.io.bundle.channel.default.v1: "stable"
3+
operators.operatorframework.io.bundle.channels.v1: "alpha,stable,beta"
4+
operators.operatorframework.io.bundle.channel.default.v1: "stable"
5+

‎cmd/opm/index/cmd.go

+1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ func AddCommand(parent *cobra.Command) {
2525
addIndexAddCmd(cmd)
2626
cmd.AddCommand(newIndexExportCmd())
2727
cmd.AddCommand(newIndexPruneCmd())
28+
cmd.AddCommand(newIndexDeprecateTruncateCmd())
2829
}

‎cmd/opm/index/deprecate.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package index
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
"github.com/spf13/cobra"
6+
"k8s.io/kubectl/pkg/util/templates"
7+
8+
"github.com/operator-framework/operator-registry/pkg/containertools"
9+
"github.com/operator-framework/operator-registry/pkg/lib/indexer"
10+
)
11+
12+
var deprecateLong = templates.LongDesc(`
13+
Deprecate and truncate operator bundles from an index.
14+
15+
Deprecated bundles will no longer be installable. Bundles that are replaced by deprecated bundles will be removed enirely from the index.
16+
17+
For example:
18+
19+
Given the update graph in quay.io/my/index:v1
20+
1.4.0 -- replaces -> 1.3.0 -- replaces -> 1.2.0 -- replaces -> 1.1.0
21+
22+
Applying the command:
23+
opm index deprecate --bundles "quay.io/my/bundle:1.3.0" --from-index "quay.io/my/index:v1" --tag "quay.io/my/index:v2"
24+
25+
Produces the following update graph in quay.io/my/index:v2
26+
1.4.0 -- replaces -> 1.3.0 [deprecated]
27+
28+
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.
29+
`)
30+
31+
func newIndexDeprecateTruncateCmd() *cobra.Command {
32+
indexCmd := &cobra.Command{
33+
Hidden: true,
34+
Use: "deprecatetruncate",
35+
Short: "Deprecate and truncate operator bundles from an index.",
36+
Long: deprecateLong,
37+
PreRunE: func(cmd *cobra.Command, args []string) error {
38+
if debug, _ := cmd.Flags().GetBool("debug"); debug {
39+
logrus.SetLevel(logrus.DebugLevel)
40+
}
41+
return nil
42+
},
43+
RunE: runIndexDeprecateTruncateCmdFunc,
44+
}
45+
46+
indexCmd.Flags().Bool("debug", false, "enable debug logging")
47+
indexCmd.Flags().Bool("generate", false, "if enabled, just creates the dockerfile and saves it to local disk")
48+
indexCmd.Flags().StringP("out-dockerfile", "d", "", "if generating the dockerfile, this flag is used to (optionally) specify a dockerfile name")
49+
indexCmd.Flags().StringP("from-index", "f", "", "previous index to add to")
50+
indexCmd.Flags().StringSliceP("bundles", "b", nil, "comma separated list of bundles to add")
51+
if err := indexCmd.MarkFlagRequired("bundles"); err != nil {
52+
logrus.Panic("Failed to set required `bundles` flag for `index add`")
53+
}
54+
indexCmd.Flags().StringP("binary-image", "i", "", "container image for on-image `opm` command")
55+
indexCmd.Flags().StringP("container-tool", "c", "", "tool to interact with container images (save, build, etc.). One of: [docker, podman]")
56+
indexCmd.Flags().StringP("build-tool", "u", "", "tool to build container images. One of: [docker, podman]. Defaults to podman. Overrides part of container-tool.")
57+
indexCmd.Flags().StringP("pull-tool", "p", "", "tool to pull container images. One of: [none, docker, podman]. Defaults to none. Overrides part of container-tool.")
58+
indexCmd.Flags().StringP("tag", "t", "", "custom tag for container image being built")
59+
indexCmd.Flags().Bool("permissive", false, "allow registry load errors")
60+
if err := indexCmd.Flags().MarkHidden("debug"); err != nil {
61+
logrus.Panic(err.Error())
62+
}
63+
64+
return indexCmd
65+
}
66+
67+
func runIndexDeprecateTruncateCmdFunc(cmd *cobra.Command, args []string) error {
68+
generate, err := cmd.Flags().GetBool("generate")
69+
if err != nil {
70+
return err
71+
}
72+
73+
outDockerfile, err := cmd.Flags().GetString("out-dockerfile")
74+
if err != nil {
75+
return err
76+
}
77+
78+
fromIndex, err := cmd.Flags().GetString("from-index")
79+
if err != nil {
80+
return err
81+
}
82+
83+
bundles, err := cmd.Flags().GetStringSlice("bundles")
84+
if err != nil {
85+
return err
86+
}
87+
88+
binaryImage, err := cmd.Flags().GetString("binary-image")
89+
if err != nil {
90+
return err
91+
}
92+
93+
tag, err := cmd.Flags().GetString("tag")
94+
if err != nil {
95+
return err
96+
}
97+
98+
permissive, err := cmd.Flags().GetBool("permissive")
99+
if err != nil {
100+
return err
101+
}
102+
103+
pullTool, buildTool, err := getContainerTools(cmd)
104+
if err != nil {
105+
return err
106+
}
107+
108+
logger := logrus.WithFields(logrus.Fields{"bundles": bundles})
109+
110+
logger.Info("deprecating bundles from the index")
111+
112+
indexDeprecator := indexer.NewIndexDeprecator(
113+
containertools.NewContainerTool(buildTool, containertools.PodmanTool),
114+
containertools.NewContainerTool(pullTool, containertools.NoneTool),
115+
logger)
116+
117+
request := indexer.DeprecateFromIndexRequest{
118+
Generate: generate,
119+
FromIndex: fromIndex,
120+
BinarySourceImage: binaryImage,
121+
OutDockerfile: outDockerfile,
122+
Tag: tag,
123+
Bundles: bundles,
124+
Permissive: permissive,
125+
}
126+
127+
err = indexDeprecator.DeprecateFromIndex(request)
128+
if err != nil {
129+
return err
130+
}
131+
132+
return nil
133+
}

‎pkg/lib/indexer/indexer.go

+59
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ImageIndexer struct {
4141
RegistryAdder registry.RegistryAdder
4242
RegistryDeleter registry.RegistryDeleter
4343
RegistryPruner registry.RegistryPruner
44+
RegistryDeprecator registry.RegistryDeprecator
4445
BuildTool containertools.ContainerTool
4546
PullTool containertools.ContainerTool
4647
Logger *logrus.Entry
@@ -527,3 +528,61 @@ func generatePackageYaml(dbQuerier pregistry.Query, packageName, downloadPath st
527528

528529
return utilerrors.NewAggregate(errs)
529530
}
531+
532+
// DeprecateFromIndexRequest defines the parameters to send to the PruneFromIndex API
533+
type DeprecateFromIndexRequest struct {
534+
Generate bool
535+
Permissive bool
536+
BinarySourceImage string
537+
FromIndex string
538+
OutDockerfile string
539+
Bundles []string
540+
Tag string
541+
}
542+
543+
// DeprecateFromIndex takes a DeprecateFromIndexRequest and deprecates the requested
544+
// bundles.
545+
func (i ImageIndexer) DeprecateFromIndex(request DeprecateFromIndexRequest) error {
546+
buildDir, outDockerfile, cleanup, err := buildContext(request.Generate, request.OutDockerfile)
547+
defer cleanup()
548+
if err != nil {
549+
return err
550+
}
551+
552+
databasePath, err := i.extractDatabase(buildDir, request.FromIndex)
553+
if err != nil {
554+
return err
555+
}
556+
557+
// Run opm registry prune on the database
558+
deprecateFromRegistryReq := registry.DeprecateFromRegistryRequest{
559+
Bundles: request.Bundles,
560+
InputDatabase: databasePath,
561+
Permissive: request.Permissive,
562+
}
563+
564+
// Prune the bundles from the registry
565+
err = i.RegistryDeprecator.DeprecateFromRegistry(deprecateFromRegistryReq)
566+
if err != nil {
567+
return err
568+
}
569+
570+
// generate the dockerfile
571+
dockerfile := i.DockerfileGenerator.GenerateIndexDockerfile(request.BinarySourceImage, databasePath)
572+
err = write(dockerfile, outDockerfile, i.Logger)
573+
if err != nil {
574+
return err
575+
}
576+
577+
if request.Generate {
578+
return nil
579+
}
580+
581+
// build the dockerfile with requested tooling
582+
err = build(outDockerfile, request.Tag, i.CommandRunner, i.Logger)
583+
if err != nil {
584+
return err
585+
}
586+
587+
return nil
588+
}

‎pkg/lib/indexer/interfaces.go

+17
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,20 @@ func NewIndexPruner(containerTool containertools.ContainerTool, logger *logrus.E
8080
Logger: logger,
8181
}
8282
}
83+
84+
// IndexDeprecator prunes operators out of an index
85+
type IndexDeprecator interface {
86+
DeprecateFromIndex(DeprecateFromIndexRequest) error
87+
}
88+
89+
func NewIndexDeprecator(buildTool, pullTool containertools.ContainerTool, logger *logrus.Entry) IndexDeprecator {
90+
return ImageIndexer{
91+
DockerfileGenerator: containertools.NewDockerfileGenerator(logger),
92+
CommandRunner: containertools.NewCommandRunner(buildTool, logger),
93+
LabelReader: containertools.NewLabelReader(pullTool, logger),
94+
RegistryDeprecator: registry.NewRegistryDeprecator(logger),
95+
BuildTool: buildTool,
96+
PullTool: pullTool,
97+
Logger: logger,
98+
}
99+
}

‎pkg/lib/registry/interfaces.go

+10
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,13 @@ func NewRegistryPruner(logger *logrus.Entry) RegistryPruner {
3636
Logger: logger,
3737
}
3838
}
39+
40+
type RegistryDeprecator interface {
41+
DeprecateFromRegistry(DeprecateFromRegistryRequest) error
42+
}
43+
44+
func NewRegistryDeprecator(logger *logrus.Entry) RegistryDeprecator {
45+
return RegistryUpdater{
46+
Logger: logger,
47+
}
48+
}

‎pkg/lib/registry/registry.go

+34
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,37 @@ func (r RegistryUpdater) PruneFromRegistry(request PruneFromRegistryRequest) err
228228

229229
return nil
230230
}
231+
232+
type DeprecateFromRegistryRequest struct {
233+
Permissive bool
234+
InputDatabase string
235+
Bundles []string
236+
}
237+
238+
func (r RegistryUpdater) DeprecateFromRegistry(request DeprecateFromRegistryRequest) error {
239+
db, err := sql.Open("sqlite3", request.InputDatabase)
240+
if err != nil {
241+
return err
242+
}
243+
defer db.Close()
244+
245+
dbLoader, err := sqlite.NewSQLLiteLoader(db)
246+
if err != nil {
247+
return err
248+
}
249+
if err := dbLoader.Migrate(context.TODO()); err != nil {
250+
return fmt.Errorf("unable to migrate database: %s", err)
251+
}
252+
253+
deprecator := sqlite.NewSQLDeprecatorForBundles(dbLoader, request.Bundles)
254+
if err := deprecator.Deprecate(); err != nil {
255+
r.Logger.Debugf("unable to deprecate bundles from database: %s", err)
256+
if !request.Permissive {
257+
r.Logger.WithError(err).Error("permissive mode disabled")
258+
return err
259+
}
260+
r.Logger.WithError(err).Warn("permissive mode enabled")
261+
}
262+
263+
return nil
264+
}

‎pkg/registry/interface.go

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Load interface {
1212
AddPackageChannels(manifest PackageManifest) error
1313
AddBundlePackageChannels(manifest PackageManifest, bundle *Bundle) error
1414
RemovePackage(packageName string) error
15+
DeprecateBundle(path string) error
1516
ClearNonHeadBundles() error
1617
}
1718

‎pkg/registry/populator_test.go

+181-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registry_test
33
import (
44
"context"
55
"database/sql"
6+
"encoding/json"
67
"fmt"
78
"math/rand"
89
"os"
@@ -18,6 +19,7 @@ import (
1819
"github.com/operator-framework/operator-registry/pkg/registry"
1920
"github.com/operator-framework/operator-registry/pkg/sqlite"
2021

22+
"k8s.io/apimachinery/pkg/util/errors"
2123
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2224
)
2325

@@ -109,6 +111,10 @@ func TestQuerierForImage(t *testing.T) {
109111
Name: "alpha",
110112
CurrentCSVName: "etcdoperator.v0.9.2",
111113
},
114+
{
115+
Name: "beta",
116+
CurrentCSVName: "etcdoperator.v0.9.0",
117+
},
112118
{
113119
Name: "stable",
114120
CurrentCSVName: "etcdoperator.v0.9.2",
@@ -185,12 +191,14 @@ func TestQuerierForImage(t *testing.T) {
185191
{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
186192
{"etcd", "stable", "etcdoperator.v0.9.0", ""},
187193
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.1"},
188-
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"}}, etcdChannelEntriesThatProvide)
194+
{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
195+
{"etcd", "beta", "etcdoperator.v0.9.0", ""}}, etcdChannelEntriesThatProvide)
189196

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

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

230238
listChannels, err := store.ListChannels(context.TODO(), "etcd")
231239
require.NoError(t, err)
232-
expectedListChannels := []string{"alpha", "stable"}
240+
expectedListChannels := []string{"alpha", "stable", "beta"}
233241
require.ElementsMatch(t, expectedListChannels, listChannels)
234242

235243
currentCSVName, err := store.GetCurrentCSVNameForChannel(context.TODO(), "etcd", "alpha")
@@ -670,3 +678,173 @@ func CheckBundlesHaveContentsIfNoPath(t *testing.T, db *sql.DB) {
670678
require.NotZero(t, bundlelen.Int64, "length of bundle for %s should not be zero, it has no bundle path", name.String)
671679
}
672680
}
681+
682+
func TestDeprecateBundle(t *testing.T) {
683+
type args struct {
684+
bundles []string
685+
}
686+
type pkgChannel map[string][]string
687+
type expected struct {
688+
err error
689+
remainingBundles []string
690+
deprecatedBundles []string
691+
remainingPkgChannels pkgChannel
692+
}
693+
tests := []struct {
694+
description string
695+
args args
696+
expected expected
697+
}{
698+
{
699+
description: "BundleDeprecated/IgnoreIfNotInIndex",
700+
args: args{
701+
bundles: []string{
702+
"quay.io/test/etcd.0.6.0",
703+
},
704+
},
705+
expected: expected{
706+
err: errors.NewAggregate([]error{fmt.Errorf("error deprecating bundle quay.io/test/etcd.0.6.0: %s", registry.ErrBundleImageNotInDatabase)}),
707+
remainingBundles: []string{
708+
"quay.io/test/etcd.0.9.0",
709+
"quay.io/test/etcd.0.9.2",
710+
"quay.io/test/prometheus.0.22.2",
711+
"quay.io/test/prometheus.0.14.0",
712+
"quay.io/test/prometheus.0.15.0",
713+
},
714+
deprecatedBundles: []string{},
715+
remainingPkgChannels: pkgChannel{
716+
"etcd": []string{
717+
"beta",
718+
"alpha",
719+
"stable",
720+
},
721+
"prometheus": []string{
722+
"preview",
723+
"stable",
724+
},
725+
},
726+
},
727+
},
728+
{
729+
description: "BundleDeprecated/SingleChannel",
730+
args: args{
731+
bundles: []string{
732+
"quay.io/test/prometheus.0.15.0",
733+
},
734+
},
735+
expected: expected{
736+
err: nil,
737+
remainingBundles: []string{
738+
"quay.io/test/etcd.0.9.0",
739+
"quay.io/test/etcd.0.9.2",
740+
"quay.io/test/prometheus.0.22.2",
741+
"quay.io/test/prometheus.0.15.0",
742+
},
743+
deprecatedBundles: []string{
744+
"quay.io/test/prometheus.0.15.0",
745+
},
746+
remainingPkgChannels: pkgChannel{
747+
"etcd": []string{
748+
"beta",
749+
"alpha",
750+
"stable",
751+
},
752+
"prometheus": []string{
753+
"preview",
754+
"stable",
755+
},
756+
},
757+
},
758+
},
759+
{
760+
description: "BundleDeprecated/ChannelRemoved",
761+
args: args{
762+
bundles: []string{
763+
"quay.io/test/etcd.0.9.2",
764+
},
765+
},
766+
expected: expected{
767+
err: nil,
768+
remainingBundles: []string{
769+
"quay.io/test/etcd.0.9.2",
770+
"quay.io/test/prometheus.0.22.2",
771+
"quay.io/test/prometheus.0.14.0",
772+
"quay.io/test/prometheus.0.15.0",
773+
},
774+
deprecatedBundles: []string{
775+
"quay.io/test/etcd.0.9.2",
776+
},
777+
remainingPkgChannels: pkgChannel{
778+
"etcd": []string{
779+
"alpha",
780+
"stable",
781+
},
782+
"prometheus": []string{
783+
"preview",
784+
"stable",
785+
},
786+
},
787+
},
788+
},
789+
}
790+
791+
for _, tt := range tests {
792+
t.Run(tt.description, func(t *testing.T) {
793+
logrus.SetLevel(logrus.DebugLevel)
794+
db, cleanup := CreateTestDb(t)
795+
defer cleanup()
796+
797+
querier, err := createAndPopulateDB(db)
798+
require.NoError(t, err)
799+
800+
store, err := sqlite.NewSQLLiteLoader(db)
801+
require.NoError(t, err)
802+
803+
deprecator := sqlite.NewSQLDeprecatorForBundles(store, tt.args.bundles)
804+
err = deprecator.Deprecate()
805+
require.Equal(t, tt.expected.err, err)
806+
807+
// Ensure remaining bundlePaths in db match
808+
bundles, err := querier.ListBundles(context.Background())
809+
require.NoError(t, err)
810+
var bundlePaths []string
811+
for _, bundle := range bundles {
812+
bundlePaths = append(bundlePaths, bundle.BundlePath)
813+
}
814+
require.ElementsMatch(t, tt.expected.remainingBundles, bundlePaths)
815+
816+
// Ensure deprecated bundles match
817+
var deprecatedBundles []string
818+
deprecatedProperty, err := json.Marshal(registry.DeprecatedProperty{})
819+
require.NoError(t, err)
820+
for _, bundle := range bundles {
821+
for _, prop := range bundle.Properties {
822+
if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
823+
deprecatedBundles = append(deprecatedBundles, bundle.BundlePath)
824+
}
825+
}
826+
}
827+
828+
require.ElementsMatch(t, tt.expected.deprecatedBundles, deprecatedBundles)
829+
830+
// Ensure remaining channels match
831+
packages, err := querier.ListPackages(context.Background())
832+
require.NoError(t, err)
833+
834+
for _, pkg := range packages {
835+
channelEntries, err := querier.GetChannelEntriesFromPackage(context.Background(), pkg)
836+
require.NoError(t, err)
837+
838+
uniqueChannels := make(map[string]struct{})
839+
var channels []string
840+
for _, ch := range channelEntries {
841+
uniqueChannels[ch.ChannelName] = struct{}{}
842+
}
843+
for k := range uniqueChannels {
844+
channels = append(channels, k)
845+
}
846+
require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
847+
}
848+
})
849+
}
850+
}

‎pkg/registry/types.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import (
1212
var (
1313
// ErrPackageNotInDatabase is an error that describes a package not found error when querying the registry
1414
ErrPackageNotInDatabase = errors.New("Package not in database")
15+
16+
// ErrBundleImageNotInDatabase is an error that describes a bundle image not found when querying the registry
17+
ErrBundleImageNotInDatabase = errors.New("Bundle Image not in database")
18+
19+
// ErrRemovingDefaultChannelDuringDeprecation is an error that describes a bundle deprecation causing the deletion
20+
// of the default channel
21+
ErrRemovingDefaultChannelDuringDeprecation = errors.New("Bundle deprecation causing default channel removal")
1522
)
1623

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

3542
const (
36-
GVKType = "olm.gvk"
37-
PackageType = "olm.package"
43+
GVKType = "olm.gvk"
44+
PackageType = "olm.package"
45+
DeprecatedType = "olm.deprecated"
3846
)
3947

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

215+
type DeprecatedProperty struct {
216+
// Whether the bundle is deprecated
217+
}
218+
207219
// Validate will validate GVK dependency type and return error(s)
208220
func (gd *GVKDependency) Validate() []error {
209221
errs := []error{}

‎pkg/sqlite/deprecate.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package sqlite
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/sirupsen/logrus"
8+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
9+
10+
"github.com/operator-framework/operator-registry/pkg/registry"
11+
)
12+
13+
type SQLDeprecator interface {
14+
Deprecate() error
15+
}
16+
17+
// BundleDeprecator removes bundles from the database
18+
type BundleDeprecator struct {
19+
store registry.Load
20+
bundles []string
21+
}
22+
23+
var _ SQLDeprecator = &BundleDeprecator{}
24+
25+
func NewSQLDeprecatorForBundles(store registry.Load, bundles []string) *BundleDeprecator {
26+
return &BundleDeprecator{
27+
store: store,
28+
bundles: bundles,
29+
}
30+
}
31+
32+
func (d *BundleDeprecator) Deprecate() error {
33+
log := logrus.WithField("bundles", d.bundles)
34+
35+
log.Info("deprecating bundles")
36+
37+
var errs []error
38+
39+
for _, bundlePath := range d.bundles {
40+
if err := d.store.DeprecateBundle(bundlePath); err != nil {
41+
if !errors.Is(err, registry.ErrBundleImageNotInDatabase) && !errors.Is(err, registry.ErrRemovingDefaultChannelDuringDeprecation) {
42+
return utilerrors.NewAggregate(append(errs, fmt.Errorf("error deprecating bundle %s: %s", bundlePath, err)))
43+
}
44+
errs = append(errs, fmt.Errorf("error deprecating bundle %s: %s", bundlePath, err))
45+
}
46+
}
47+
48+
return utilerrors.NewAggregate(errs)
49+
}

‎pkg/sqlite/load.go

+167
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,170 @@ func (s *sqlLoader) addBundleProperties(tx *sql.Tx, bundle *registry.Bundle) err
805805

806806
return nil
807807
}
808+
809+
func (s *sqlLoader) rmChannelEntry(tx *sql.Tx, csvName string) error {
810+
getEntryID := `SELECT entry_id FROM channel_entry WHERE operatorbundle_name=?`
811+
rows, err := tx.QueryContext(context.TODO(), getEntryID, csvName)
812+
if err != nil {
813+
return err
814+
}
815+
var entryIDs []int64
816+
for rows.Next() {
817+
var entryID sql.NullInt64
818+
rows.Scan(&entryID)
819+
entryIDs = append(entryIDs, entryID.Int64)
820+
}
821+
err = rows.Close()
822+
if err != nil {
823+
return err
824+
}
825+
826+
updateChannelEntry, err := tx.Prepare(`UPDATE channel_entry SET replaces=NULL WHERE replaces=?`)
827+
if err != nil {
828+
return err
829+
}
830+
for _, id := range entryIDs {
831+
if _, err := updateChannelEntry.Exec(id); err != nil {
832+
return err
833+
}
834+
}
835+
err = updateChannelEntry.Close()
836+
if err != nil {
837+
return err
838+
}
839+
840+
stmt, err := tx.Prepare("DELETE FROM channel_entry WHERE operatorbundle_name=?")
841+
if err != nil {
842+
return err
843+
}
844+
defer stmt.Close()
845+
846+
if _, err := stmt.Exec(csvName); err != nil {
847+
return err
848+
}
849+
850+
return nil
851+
}
852+
853+
func getTailFromBundle(tx *sql.Tx, name string) (bundles []string, err error) {
854+
getReplacesSkips := `SELECT replaces, skips FROM operatorbundle WHERE name=?`
855+
isDefaultChannelHead := `SELECT head_operatorbundle_name FROM channel
856+
INNER JOIN package ON channel.name = package.default_channel
857+
WHERE channel.head_operatorbundle_name = ?`
858+
859+
tail := make(map[string]struct{})
860+
next := name
861+
862+
for next != "" {
863+
rows, err := tx.QueryContext(context.TODO(), getReplacesSkips, next)
864+
if err != nil {
865+
return nil, err
866+
}
867+
var replaces sql.NullString
868+
var skips sql.NullString
869+
if rows.Next() {
870+
if err := rows.Scan(&replaces, &skips); err != nil {
871+
return nil, err
872+
}
873+
}
874+
rows.Close()
875+
if skips.Valid && skips.String != "" {
876+
for _, skip := range strings.Split(skips.String, ",") {
877+
tail[skip] = struct{}{}
878+
}
879+
}
880+
if replaces.Valid && replaces.String != "" {
881+
// check if replaces is the head of the defaultChannel
882+
// if it is, the defaultChannel will be removed
883+
// this is not allowed because we cannot know which channel to promote as the new default
884+
rows, err := tx.QueryContext(context.TODO(), isDefaultChannelHead, replaces.String)
885+
if err != nil {
886+
return nil, err
887+
}
888+
if rows.Next() {
889+
var defaultChannelHead sql.NullString
890+
err := rows.Scan(&defaultChannelHead)
891+
if err != nil {
892+
return nil, err
893+
}
894+
if defaultChannelHead.Valid {
895+
return nil, registry.ErrRemovingDefaultChannelDuringDeprecation
896+
}
897+
}
898+
next = replaces.String
899+
tail[replaces.String] = struct{}{}
900+
} else {
901+
next = ""
902+
}
903+
}
904+
var allTails []string
905+
906+
for k := range tail {
907+
allTails = append(allTails, k)
908+
}
909+
910+
return allTails, nil
911+
912+
}
913+
914+
func getBundleNameAndVersionForImage(tx *sql.Tx, path string) (string, string, error) {
915+
query := `SELECT name, version FROM operatorbundle WHERE bundlepath=? LIMIT 1`
916+
rows, err := tx.QueryContext(context.TODO(), query, path)
917+
if err != nil {
918+
return "", "", err
919+
}
920+
defer rows.Close()
921+
922+
var name sql.NullString
923+
var version sql.NullString
924+
if rows.Next() {
925+
if err := rows.Scan(&name, &version); err != nil {
926+
return "", "", err
927+
}
928+
}
929+
if name.Valid && version.Valid {
930+
return name.String, version.String, nil
931+
}
932+
return "", "", registry.ErrBundleImageNotInDatabase
933+
}
934+
935+
func (s *sqlLoader) DeprecateBundle(path string) error {
936+
tx, err := s.db.Begin()
937+
if err != nil {
938+
return err
939+
}
940+
defer func() {
941+
tx.Rollback()
942+
}()
943+
944+
name, version, err := getBundleNameAndVersionForImage(tx, path)
945+
if err != nil {
946+
return err
947+
}
948+
tailBundles, err := getTailFromBundle(tx, name)
949+
if err != nil {
950+
return err
951+
}
952+
953+
for _, bundle := range tailBundles {
954+
err := s.rmBundle(tx, bundle)
955+
if err != nil {
956+
return err
957+
}
958+
err = s.rmChannelEntry(tx, bundle)
959+
if err != nil {
960+
return err
961+
}
962+
}
963+
964+
deprecatedValue, err := json.Marshal(registry.DeprecatedProperty{})
965+
if err != nil {
966+
return err
967+
}
968+
err = s.addProperty(tx, registry.DeprecatedType, string(deprecatedValue), name, version, path)
969+
if err != nil {
970+
return err
971+
}
972+
973+
return tx.Commit()
974+
}

‎pkg/sqlite/load_test.go

+174
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,21 @@ func newUnstructuredCSV(t *testing.T, name, replaces string) *unstructured.Unstr
223223
return &unstructured.Unstructured{Object: out}
224224
}
225225

226+
func newUnstructuredCSVwithSkips(t *testing.T, name, replaces string, skips ...string) *unstructured.Unstructured {
227+
csv := &registry.ClusterServiceVersion{}
228+
csv.TypeMeta.Kind = "ClusterServiceVersion"
229+
csv.SetName(name)
230+
allSkips, err := json.Marshal(skips)
231+
require.NoError(t, err)
232+
replacesSkips := fmt.Sprintf(`{"replaces": "%s", "skips": %s}`, replaces, string(allSkips))
233+
t.Logf("%v", replacesSkips)
234+
csv.Spec = json.RawMessage(replacesSkips)
235+
236+
out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(csv)
237+
require.NoError(t, err)
238+
return &unstructured.Unstructured{Object: out}
239+
}
240+
226241
func newBundle(t *testing.T, name, pkgName string, channels []string, objs ...*unstructured.Unstructured) *registry.Bundle {
227242
bundle := registry.NewBundle(name, pkgName, channels, objs...)
228243

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

233248
return bundle
234249
}
250+
251+
func TestGetTailFromBundle(t *testing.T) {
252+
type fields struct {
253+
bundles []*registry.Bundle
254+
pkgs []registry.PackageManifest
255+
}
256+
type args struct {
257+
bundle string
258+
}
259+
type expected struct {
260+
err error
261+
tail []string
262+
}
263+
tests := []struct {
264+
description string
265+
fields fields
266+
args args
267+
expected expected
268+
}{
269+
{
270+
description: "GetTailFromBundle/RemoveDefaultChannelForbidden",
271+
fields: fields{
272+
bundles: []*registry.Bundle{
273+
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSV(t, "csv-a", "csv-b")),
274+
newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")),
275+
newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")),
276+
},
277+
pkgs: []registry.PackageManifest{
278+
{
279+
PackageName: "pkg-0",
280+
Channels: []registry.PackageChannel{
281+
{
282+
Name: "alpha",
283+
CurrentCSVName: "csv-a",
284+
},
285+
{
286+
Name: "stable",
287+
CurrentCSVName: "csv-b",
288+
},
289+
},
290+
DefaultChannelName: "stable",
291+
},
292+
},
293+
},
294+
args: args{
295+
bundle: "csv-a",
296+
},
297+
expected: expected{
298+
err: registry.ErrRemovingDefaultChannelDuringDeprecation,
299+
tail: nil,
300+
},
301+
},
302+
{
303+
description: "GetTailFromBundle/RemovingNonDefaultChannel",
304+
fields: fields{
305+
bundles: []*registry.Bundle{
306+
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSV(t, "csv-a", "csv-b")),
307+
newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")),
308+
newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")),
309+
},
310+
pkgs: []registry.PackageManifest{
311+
{
312+
PackageName: "pkg-0",
313+
Channels: []registry.PackageChannel{
314+
{
315+
Name: "alpha",
316+
CurrentCSVName: "csv-a",
317+
},
318+
{
319+
Name: "stable",
320+
CurrentCSVName: "csv-b",
321+
},
322+
},
323+
DefaultChannelName: "alpha",
324+
},
325+
},
326+
},
327+
args: args{
328+
bundle: "csv-a",
329+
},
330+
expected: expected{
331+
err: nil,
332+
tail: []string{
333+
"csv-b",
334+
"csv-c",
335+
},
336+
},
337+
},
338+
{
339+
description: "GetTailFromBundle/HandlesSkips",
340+
fields: fields{
341+
bundles: []*registry.Bundle{
342+
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSVwithSkips(t, "csv-a", "csv-b", "csv-d", "csv-e", "csv-f")),
343+
newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")),
344+
newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")),
345+
},
346+
pkgs: []registry.PackageManifest{
347+
{
348+
PackageName: "pkg-0",
349+
Channels: []registry.PackageChannel{
350+
{
351+
Name: "alpha",
352+
CurrentCSVName: "csv-a",
353+
},
354+
{
355+
Name: "stable",
356+
CurrentCSVName: "csv-b",
357+
},
358+
},
359+
DefaultChannelName: "alpha",
360+
},
361+
},
362+
},
363+
args: args{
364+
bundle: "csv-a",
365+
},
366+
expected: expected{
367+
err: nil,
368+
tail: []string{
369+
"csv-b",
370+
"csv-c",
371+
"csv-d",
372+
"csv-e",
373+
"csv-f",
374+
},
375+
},
376+
},
377+
}
378+
379+
for _, tt := range tests {
380+
t.Run(tt.description, func(t *testing.T) {
381+
db, cleanup := CreateTestDb(t)
382+
defer cleanup()
383+
store, err := NewSQLLiteLoader(db)
384+
require.NoError(t, err)
385+
err = store.Migrate(context.TODO())
386+
require.NoError(t, err)
387+
388+
for _, bundle := range tt.fields.bundles {
389+
// Throw away any errors loading bundles (not testing this)
390+
store.AddOperatorBundle(bundle)
391+
}
392+
393+
for _, pkg := range tt.fields.pkgs {
394+
// Throw away any errors loading packages (not testing this)
395+
store.AddPackageChannels(pkg)
396+
}
397+
tx, err := db.Begin()
398+
require.NoError(t, err)
399+
tail, err := getTailFromBundle(tx, tt.args.bundle)
400+
401+
require.Equal(t, tt.expected.err, err)
402+
t.Logf("tt.expected.tail %#v", tt.expected.tail)
403+
t.Logf("tail %#v", tail)
404+
require.ElementsMatch(t, tt.expected.tail, tail)
405+
})
406+
}
407+
408+
}

‎pkg/sqlite/query.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ func (s *SQLQuerier) GetBundleVersion(ctx context.Context, image string) (string
779779
if version.Valid {
780780
return version.String, nil
781781
}
782-
return "", fmt.Errorf("bundle %s not found", image)
782+
return "", nil
783783
}
784784

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

0 commit comments

Comments
 (0)
Please sign in to comment.