Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3349788

Browse files
committedJul 24, 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 3349788

File tree

15 files changed

+829
-7
lines changed

15 files changed

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

‎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(newIndexDeprecateCmd())
2829
}

‎cmd/opm/index/deprecate.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 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.
29+
`)
30+
31+
func newIndexDeprecateCmd() *cobra.Command {
32+
indexCmd := &cobra.Command{
33+
Use: "deprecate",
34+
Short: "Deprecate operator bundles from an index.",
35+
Long: "",
36+
PreRunE: func(cmd *cobra.Command, args []string) error {
37+
if debug, _ := cmd.Flags().GetBool("debug"); debug {
38+
logrus.SetLevel(logrus.DebugLevel)
39+
}
40+
return nil
41+
},
42+
RunE: runIndexDeprecateCmdFunc,
43+
}
44+
45+
indexCmd.Flags().Bool("debug", false, "enable debug logging")
46+
indexCmd.Flags().Bool("generate", false, "if enabled, just creates the dockerfile and saves it to local disk")
47+
indexCmd.Flags().StringP("out-dockerfile", "d", "", "if generating the dockerfile, this flag is used to (optionally) specify a dockerfile name")
48+
indexCmd.Flags().StringP("from-index", "f", "", "previous index to add to")
49+
indexCmd.Flags().StringSliceP("bundles", "b", nil, "comma separated list of bundles to add")
50+
if err := indexCmd.MarkFlagRequired("bundles"); err != nil {
51+
logrus.Panic("Failed to set required `bundles` flag for `index add`")
52+
}
53+
indexCmd.Flags().StringP("binary-image", "i", "", "container image for on-image `opm` command")
54+
indexCmd.Flags().StringP("container-tool", "c", "", "tool to interact with container images (save, build, etc.). One of: [docker, podman]")
55+
indexCmd.Flags().StringP("build-tool", "u", "", "tool to build container images. One of: [docker, podman]. Defaults to podman. Overrides part of container-tool.")
56+
indexCmd.Flags().StringP("pull-tool", "p", "", "tool to pull container images. One of: [none, docker, podman]. Defaults to none. Overrides part of container-tool.")
57+
indexCmd.Flags().StringP("tag", "t", "", "custom tag for container image being built")
58+
indexCmd.Flags().Bool("permissive", false, "allow registry load errors")
59+
if err := indexCmd.Flags().MarkHidden("debug"); err != nil {
60+
logrus.Panic(err.Error())
61+
}
62+
63+
return indexCmd
64+
}
65+
66+
func runIndexDeprecateCmdFunc(cmd *cobra.Command, args []string) error {
67+
generate, err := cmd.Flags().GetBool("generate")
68+
if err != nil {
69+
return err
70+
}
71+
72+
outDockerfile, err := cmd.Flags().GetString("out-dockerfile")
73+
if err != nil {
74+
return err
75+
}
76+
77+
fromIndex, err := cmd.Flags().GetString("from-index")
78+
if err != nil {
79+
return err
80+
}
81+
82+
bundles, err := cmd.Flags().GetStringSlice("bundles")
83+
if err != nil {
84+
return err
85+
}
86+
87+
binaryImage, err := cmd.Flags().GetString("binary-image")
88+
if err != nil {
89+
return err
90+
}
91+
92+
tag, err := cmd.Flags().GetString("tag")
93+
if err != nil {
94+
return err
95+
}
96+
97+
permissive, err := cmd.Flags().GetBool("permissive")
98+
if err != nil {
99+
return err
100+
}
101+
102+
pullTool, buildTool, err := getContainerTools(cmd)
103+
if err != nil {
104+
return err
105+
}
106+
107+
logger := logrus.WithFields(logrus.Fields{"bundles": bundles})
108+
109+
logger.Info("deprecating bundles from the index")
110+
111+
indexDeprecator := indexer.NewIndexDeprecator(
112+
containertools.NewContainerTool(buildTool, containertools.PodmanTool),
113+
containertools.NewContainerTool(pullTool, containertools.NoneTool),
114+
logger)
115+
116+
request := indexer.DeprecateFromIndexRequest{
117+
Generate: generate,
118+
FromIndex: fromIndex,
119+
BinarySourceImage: binaryImage,
120+
OutDockerfile: outDockerfile,
121+
Tag: tag,
122+
Bundles: bundles,
123+
Permissive: permissive,
124+
}
125+
126+
err = indexDeprecator.DeprecateFromIndex(request)
127+
if err != nil {
128+
return err
129+
}
130+
131+
return nil
132+
}

‎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
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

+37
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,40 @@ 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("hello 3 %s", err)
251+
}
252+
dbQuerier := sqlite.NewSQLLiteQuerierFromDb(db)
253+
254+
deprecator := sqlite.NewSQLDeprecatorForBundles(dbLoader, dbQuerier, request.Bundles)
255+
if err := deprecator.Deprecate(); err != nil {
256+
r.Logger.Debugf("unable to deprecate bundles from database: %s", err)
257+
if !request.Permissive {
258+
r.Logger.WithError(err).Error("permissive mode disabled")
259+
return err
260+
} else {
261+
r.Logger.WithError(err).Warn("permissive mode enabled")
262+
}
263+
logrus.WithError(err).Warn("permissive mode enabled")
264+
}
265+
266+
return nil
267+
}

‎pkg/registry/empty.go

+8
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ func (EmptyQuery) GetDependenciesForBundle(ctx context.Context, name, version, p
104104
return nil, errors.New("empty querier: cannot get dependencies for bundle")
105105
}
106106

107+
func (EmptyQuery) GetTailFromBundle(ctx context.Context, name string) (bundles []string, err error) {
108+
return nil, errors.New("empty querier: cannot get tail from bundle")
109+
}
110+
111+
func (EmptyQuery) GetBundleNameAndVersionForImage(ctx context.Context, path string) (string, string, error) {
112+
return "", "", errors.New("empty querier: cannot get name and version for bundle")
113+
}
114+
107115
var _ Query = &EmptyQuery{}
108116

109117
func NewEmptyQuerier() *EmptyQuery {

‎pkg/registry/interface.go

+5
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+
DeprecateBundleWithTail(csvName, version, path string, tailBundles []string) error
1516
ClearNonHeadBundles() error
1617
}
1718

@@ -55,6 +56,10 @@ type Query interface {
5556
ListBundles(ctx context.Context) (bundles []*api.Bundle, err error)
5657
// Get the list of dependencies for a bundle
5758
GetDependenciesForBundle(ctx context.Context, name, version, path string) (dependencies []*api.Dependency, err error)
59+
// Get the replacement chain for a bundle
60+
GetTailFromBundle(ctx context.Context, name string) (bundles []string, err error)
61+
// Get the name and version for a bundle path
62+
GetBundleNameAndVersionForImage(ctx context.Context, path string) (string, string, error)
5863
}
5964

6065
// GraphLoader generates a graph

‎pkg/registry/populator_test.go

+183-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,175 @@ 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, querier, 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+
Deprecated: true,
820+
})
821+
require.NoError(t, err)
822+
for _, bundle := range bundles {
823+
for _, prop := range bundle.Properties {
824+
if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
825+
deprecatedBundles = append(deprecatedBundles, bundle.BundlePath)
826+
}
827+
}
828+
}
829+
830+
require.ElementsMatch(t, tt.expected.deprecatedBundles, deprecatedBundles)
831+
832+
// Ensure remaining channels match
833+
packages, err := querier.ListPackages(context.Background())
834+
require.NoError(t, err)
835+
836+
for _, pkg := range packages {
837+
channelEntries, err := querier.GetChannelEntriesFromPackage(context.Background(), pkg)
838+
require.NoError(t, err)
839+
840+
uniqueChannels := make(map[string]struct{})
841+
var channels []string
842+
for _, ch := range channelEntries {
843+
uniqueChannels[ch.ChannelName] = struct{}{}
844+
}
845+
for k := range uniqueChannels {
846+
channels = append(channels, k)
847+
}
848+
require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
849+
}
850+
})
851+
}
852+
}

‎pkg/registry/types.go

+15-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,11 @@ type PackageProperty struct {
204212
Version string `json:"version" yaml:"version"`
205213
}
206214

215+
type DeprecatedProperty struct {
216+
// Whether the bundle is deprecated
217+
Deprecated bool `json:"deprecated" yaml:"deprecated"`
218+
}
219+
207220
// Validate will validate GVK dependency type and return error(s)
208221
func (gd *GVKDependency) Validate() []error {
209222
errs := []error{}

‎pkg/sqlite/deprecate.go

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

‎pkg/sqlite/deprecate_test.go

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package sqlite
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/operator-framework/operator-registry/pkg/registry"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestGetTailFromBundle(t *testing.T) {
12+
type fields struct {
13+
bundles []*registry.Bundle
14+
pkgs []registry.PackageManifest
15+
}
16+
type args struct {
17+
bundle string
18+
}
19+
type expected struct {
20+
err error
21+
tail []string
22+
}
23+
tests := []struct {
24+
description string
25+
fields fields
26+
args args
27+
expected expected
28+
}{
29+
{
30+
description: "GetTailFromBundle/RemoveDefaultChannelForbidden",
31+
fields: fields{
32+
bundles: []*registry.Bundle{
33+
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSV(t, "csv-a", "csv-b")),
34+
newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")),
35+
newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")),
36+
},
37+
pkgs: []registry.PackageManifest{
38+
{
39+
PackageName: "pkg-0",
40+
Channels: []registry.PackageChannel{
41+
{
42+
Name: "alpha",
43+
CurrentCSVName: "csv-a",
44+
},
45+
{
46+
Name: "stable",
47+
CurrentCSVName: "csv-b",
48+
},
49+
},
50+
DefaultChannelName: "stable",
51+
},
52+
},
53+
},
54+
args: args{
55+
bundle: "csv-a",
56+
},
57+
expected: expected{
58+
err: registry.ErrRemovingDefaultChannelDuringDeprecation,
59+
tail: nil,
60+
},
61+
},
62+
{
63+
description: "GetTailFromBundle/RemovingNonDefaultChannel",
64+
fields: fields{
65+
bundles: []*registry.Bundle{
66+
newBundle(t, "csv-a", "pkg-0", []string{"alpha"}, newUnstructuredCSV(t, "csv-a", "csv-b")),
67+
newBundle(t, "csv-b", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-b", "csv-c")),
68+
newBundle(t, "csv-c", "pkg-0", []string{"alpha", "stable"}, newUnstructuredCSV(t, "csv-c", "")),
69+
},
70+
pkgs: []registry.PackageManifest{
71+
{
72+
PackageName: "pkg-0",
73+
Channels: []registry.PackageChannel{
74+
{
75+
Name: "alpha",
76+
CurrentCSVName: "csv-a",
77+
},
78+
{
79+
Name: "stable",
80+
CurrentCSVName: "csv-b",
81+
},
82+
},
83+
DefaultChannelName: "alpha",
84+
},
85+
},
86+
},
87+
args: args{
88+
bundle: "csv-a",
89+
},
90+
expected: expected{
91+
err: nil,
92+
tail: []string{
93+
"csv-b",
94+
"csv-c",
95+
},
96+
},
97+
},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.description, func(t *testing.T) {
102+
db, cleanup := CreateTestDb(t)
103+
defer cleanup()
104+
store, err := NewSQLLiteLoader(db)
105+
require.NoError(t, err)
106+
err = store.Migrate(context.TODO())
107+
require.NoError(t, err)
108+
109+
for _, bundle := range tt.fields.bundles {
110+
// Throw away any errors loading bundles (not testing this)
111+
store.AddOperatorBundle(bundle)
112+
}
113+
114+
for _, pkg := range tt.fields.pkgs {
115+
// Throw away any errors loading packages (not testing this)
116+
store.AddPackageChannels(pkg)
117+
}
118+
119+
querier := NewSQLLiteQuerierFromDb(db)
120+
tail, err := querier.GetTailFromBundle(context.Background(), tt.args.bundle)
121+
122+
require.Equal(t, tt.expected.err, err)
123+
t.Logf("tt.expected.tail %#v", tt.expected.tail)
124+
t.Logf("tail %#v", tail)
125+
require.ElementsMatch(t, tt.expected.tail, tail)
126+
})
127+
}
128+
129+
}

‎pkg/sqlite/load.go

+76
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,79 @@ 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 (s *sqlLoader) DeprecateBundleWithTail(name, version, path string, tailBundles []string) error {
854+
tx, err := s.db.Begin()
855+
if err != nil {
856+
return err
857+
}
858+
defer func() {
859+
tx.Rollback()
860+
}()
861+
862+
for _, bundle := range tailBundles {
863+
err := s.rmBundle(tx, bundle)
864+
if err != nil {
865+
return err
866+
}
867+
err = s.rmChannelEntry(tx, bundle)
868+
if err != nil {
869+
return err
870+
}
871+
}
872+
873+
deprecatedValue, err := json.Marshal(registry.DeprecatedProperty{Deprecated: true})
874+
if err != nil {
875+
return err
876+
}
877+
err = s.addProperty(tx, registry.DeprecatedType, string(deprecatedValue), name, version, path)
878+
if err != nil {
879+
return err
880+
}
881+
882+
return tx.Commit()
883+
}

‎pkg/sqlite/query.go

+90-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/operator-framework/operator-registry/pkg/registry"
1414
)
1515

16+
var ImageDoesNotExistError = fmt.Errorf("")
17+
1618
type SQLQuerier struct {
1719
db *sql.DB
1820
}
@@ -779,7 +781,7 @@ func (s *SQLQuerier) GetBundleVersion(ctx context.Context, image string) (string
779781
if version.Valid {
780782
return version.String, nil
781783
}
782-
return "", fmt.Errorf("bundle %s not found", image)
784+
return "", nil
783785
}
784786

785787
func (s *SQLQuerier) GetBundlePathsForPackage(ctx context.Context, pkgName string) ([]string, error) {
@@ -1117,3 +1119,90 @@ func (s *SQLQuerier) GetPropertiesForBundle(ctx context.Context, name, version,
11171119

11181120
return
11191121
}
1122+
1123+
func (s *SQLQuerier) GetTailFromBundle(ctx context.Context, name string) (bundles []string, err error) {
1124+
1125+
getReplacesSkips := `SELECT replaces, skips FROM operatorbundle WHERE name=?`
1126+
isDefaultChannelHead := `SELECT head_operatorbundle_name FROM channel
1127+
INNER JOIN package ON channel.name = package.default_channel
1128+
WHERE channel.head_operatorbundle_name = ?`
1129+
1130+
nameQueue := make(chan string, 100)
1131+
tail := make(map[string]struct{})
1132+
1133+
nameQueue <- name
1134+
1135+
for {
1136+
select {
1137+
case next := <-nameQueue:
1138+
rows, err := s.db.QueryContext(ctx, getReplacesSkips, next)
1139+
if err != nil {
1140+
return nil, err
1141+
}
1142+
1143+
var replaces sql.NullString
1144+
var skips sql.NullString
1145+
if rows.Next() {
1146+
if err := rows.Scan(&replaces, &skips); err != nil {
1147+
return nil, err
1148+
}
1149+
}
1150+
rows.Close()
1151+
1152+
if skips.Valid && skips.String != "" {
1153+
tail[skips.String] = struct{}{}
1154+
}
1155+
if replaces.Valid && replaces.String != "" {
1156+
// check if replaces is the head of the defaultChannel
1157+
// if it is, the defaultChannel will be removed
1158+
// this is not allowed because we cannot know which channel to promote as the new default
1159+
rows, err := s.db.QueryContext(ctx, isDefaultChannelHead, replaces.String)
1160+
if err != nil {
1161+
return nil, err
1162+
}
1163+
if rows.Next() {
1164+
var defaultChannelHead sql.NullString
1165+
err := rows.Scan(&defaultChannelHead)
1166+
if err != nil {
1167+
return nil, err
1168+
}
1169+
if defaultChannelHead.Valid {
1170+
return nil, registry.ErrRemovingDefaultChannelDuringDeprecation
1171+
}
1172+
}
1173+
nameQueue <- replaces.String
1174+
tail[replaces.String] = struct{}{}
1175+
}
1176+
default:
1177+
var allTails []string
1178+
1179+
for k := range tail {
1180+
allTails = append(allTails, k)
1181+
}
1182+
1183+
return allTails, nil
1184+
}
1185+
}
1186+
1187+
}
1188+
1189+
func (s *SQLQuerier) GetBundleNameAndVersionForImage(ctx context.Context, path string) (string, string, error) {
1190+
query := `SELECT name, version FROM operatorbundle WHERE bundlepath=? LIMIT 1`
1191+
rows, err := s.db.QueryContext(ctx, query, path)
1192+
if err != nil {
1193+
return "", "", err
1194+
}
1195+
defer rows.Close()
1196+
1197+
var name sql.NullString
1198+
var version sql.NullString
1199+
if rows.Next() {
1200+
if err := rows.Scan(&name, &version); err != nil {
1201+
return "", "", err
1202+
}
1203+
}
1204+
if name.Valid && version.Valid {
1205+
return name.String, version.String, nil
1206+
}
1207+
return "", "", registry.ErrBundleImageNotInDatabase
1208+
}

0 commit comments

Comments
 (0)
Please sign in to comment.