Skip to content

Commit c41b523

Browse files
committed
fix(deprecate): explicit pkg rm drops latest deprecated
- When using opm index|registry rm <pkg>, drop the "latest" related bundles from the deprecated table. This makes "un-deprecating" a bundle possible, which is useful if that bundle was deprecated by accident - Add a migration and logic on deprecatetruncate that drops truncated bundles (i.e. w/o assoc. channel_entry rows) from the deprecated table, clearing the "deprecation history" so that operations post package removal don't assume a truncated bundle should be deprecated See https://bugzilla.redhat.com/show_bug.cgi?id=1982781 for motivation Signed-off-by: Nick Hale <[email protected]> Upstream-repository: operator-registry Upstream-commit: 29e90dea6a1c2ef12075ca866284cd8f267c2bba
1 parent fb92eba commit c41b523

File tree

8 files changed

+435
-4
lines changed

8 files changed

+435
-4
lines changed

staging/operator-registry/pkg/lib/registry/registry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func (r RegistryUpdater) DeleteFromRegistry(request DeleteFromRegistryRequest) e
212212
}
213213
defer db.Close()
214214

215-
dbLoader, err := sqlite.NewSQLLiteLoader(db)
215+
dbLoader, err := sqlite.NewDeprecationAwareLoader(db)
216216
if err != nil {
217217
return err
218218
}

staging/operator-registry/pkg/sqlite/load.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type MigratableLoader interface {
2828

2929
var _ MigratableLoader = &sqlLoader{}
3030

31-
func NewSQLLiteLoader(db *sql.DB, opts ...DbOption) (MigratableLoader, error) {
31+
func newSQLLoader(db *sql.DB, opts ...DbOption) (*sqlLoader, error) {
3232
options := defaultDBOptions()
3333
for _, o := range opts {
3434
o(options)
@@ -46,6 +46,10 @@ func NewSQLLiteLoader(db *sql.DB, opts ...DbOption) (MigratableLoader, error) {
4646
return &sqlLoader{db: db, migrator: migrator, enableAlpha: options.EnableAlpha}, nil
4747
}
4848

49+
func NewSQLLiteLoader(db *sql.DB, opts ...DbOption) (MigratableLoader, error) {
50+
return newSQLLoader(db, opts...)
51+
}
52+
4953
func (s *sqlLoader) Migrate(ctx context.Context) error {
5054
if s.migrator == nil {
5155
return fmt.Errorf("no migrator configured")
@@ -1462,6 +1466,13 @@ func (s *sqlLoader) DeprecateBundle(path string) error {
14621466
return err
14631467
}
14641468

1469+
// Clean up the deprecated table by dropping all truncated bundles
1470+
// (see pkg/sqlite/migrations/013_rm_truncated_deprecations.go for more details)
1471+
_, err = tx.Exec(`DELETE FROM deprecated WHERE deprecated.operatorbundle_name NOT IN (SELECT DISTINCT deprecated.operatorbundle_name FROM (deprecated INNER JOIN channel_entry ON deprecated.operatorbundle_name = channel_entry.operatorbundle_name))`)
1472+
if err != nil {
1473+
return err
1474+
}
1475+
14651476
return tx.Commit()
14661477
}
14671478

@@ -1536,3 +1547,46 @@ func (s *sqlLoader) deprecated(tx *sql.Tx, name string) (bool, error) {
15361547
// Ignore any deprecated bundles
15371548
return err == nil, err
15381549
}
1550+
1551+
// DeprecationAwareLoader understands how bundle deprecations are handled in SQLite and decorates
1552+
// the sqlLoader with proxy methods that handle deprecation related table housekeeping.
1553+
type DeprecationAwareLoader struct {
1554+
*sqlLoader
1555+
}
1556+
1557+
// NewDeprecationAwareLoader returns a new DeprecationAwareLoader.
1558+
func NewDeprecationAwareLoader(db *sql.DB, opts ...DbOption) (*DeprecationAwareLoader, error) {
1559+
loader, err := newSQLLoader(db, opts...)
1560+
if err != nil {
1561+
return nil, err
1562+
}
1563+
1564+
return &DeprecationAwareLoader{sqlLoader: loader}, nil
1565+
}
1566+
1567+
func (d *DeprecationAwareLoader) clearLastDeprecatedInPackage(pkg string) error {
1568+
tx, err := d.db.Begin()
1569+
if err != nil {
1570+
return err
1571+
}
1572+
defer func() {
1573+
tx.Rollback()
1574+
}()
1575+
1576+
// The last deprecated bundles for a package will still have "tombstone" records in channel_entry (among other tables).
1577+
// Use that info to relate the package to a set of rows in the deprecated table.
1578+
_, err = tx.Exec(`DELETE FROM deprecated WHERE deprecated.operatorbundle_name IN (SELECT DISTINCT deprecated.operatorbundle_name FROM (deprecated INNER JOIN channel_entry ON deprecated.operatorbundle_name = channel_entry.operatorbundle_name) WHERE channel_entry.package_name = ?)`, pkg)
1579+
if err != nil {
1580+
return err
1581+
}
1582+
1583+
return tx.Commit()
1584+
}
1585+
1586+
func (d *DeprecationAwareLoader) RemovePackage(pkg string) error {
1587+
if err := d.clearLastDeprecatedInPackage(pkg); err != nil {
1588+
return err
1589+
}
1590+
1591+
return d.sqlLoader.RemovePackage(pkg)
1592+
}

staging/operator-registry/pkg/sqlite/load_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,218 @@ func TestRMBundle(t *testing.T) {
264264
require.NoError(t, loader.rmBundle(tx, "non-existent"))
265265
}
266266

267+
func TestDeprecationAwareLoader(t *testing.T) {
268+
withBundleImage := func(image string, bundle *registry.Bundle) *registry.Bundle {
269+
bundle.BundleImage = image
270+
return bundle
271+
}
272+
type fields struct {
273+
bundles []*registry.Bundle
274+
pkgs []registry.PackageManifest
275+
deprecatedPaths []string
276+
}
277+
type args struct {
278+
pkg string
279+
}
280+
type expected struct {
281+
err error
282+
deprecated map[string]struct{}
283+
}
284+
tests := []struct {
285+
description string
286+
fields fields
287+
args args
288+
expected expected
289+
}{
290+
{
291+
description: "NoDeprecation",
292+
fields: fields{
293+
bundles: []*registry.Bundle{
294+
withBundleImage("quay.io/my/bundle-a", newBundle(t, "csv-a", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-a", ""))),
295+
},
296+
pkgs: []registry.PackageManifest{
297+
{
298+
PackageName: "pkg-0",
299+
Channels: []registry.PackageChannel{
300+
{
301+
Name: "stable",
302+
CurrentCSVName: "csv-a",
303+
},
304+
},
305+
DefaultChannelName: "stable",
306+
},
307+
},
308+
deprecatedPaths: []string{},
309+
},
310+
args: args{
311+
pkg: "pkg-0",
312+
},
313+
expected: expected{
314+
err: nil,
315+
deprecated: map[string]struct{}{},
316+
},
317+
},
318+
{
319+
description: "RemovePackage/DropsDeprecated",
320+
fields: fields{
321+
bundles: []*registry.Bundle{
322+
withBundleImage("quay.io/my/bundle-a", newBundle(t, "csv-a", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-a", ""))),
323+
withBundleImage("quay.io/my/bundle-aa", newBundle(t, "csv-aa", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-aa", "csv-a"))),
324+
},
325+
pkgs: []registry.PackageManifest{
326+
{
327+
PackageName: "pkg-0",
328+
Channels: []registry.PackageChannel{
329+
{
330+
Name: "stable",
331+
CurrentCSVName: "csv-aa",
332+
},
333+
},
334+
DefaultChannelName: "stable",
335+
},
336+
},
337+
deprecatedPaths: []string{
338+
"quay.io/my/bundle-a",
339+
},
340+
},
341+
args: args{
342+
pkg: "pkg-0",
343+
},
344+
expected: expected{
345+
err: nil,
346+
deprecated: map[string]struct{}{},
347+
},
348+
},
349+
{
350+
description: "RemovePackage/IgnoresOtherPackages",
351+
fields: fields{
352+
bundles: []*registry.Bundle{
353+
withBundleImage("quay.io/my/bundle-a", newBundle(t, "csv-a", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-a", ""))),
354+
withBundleImage("quay.io/my/bundle-aa", newBundle(t, "csv-aa", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-aa", "csv-a"))),
355+
withBundleImage("quay.io/my/bundle-b", newBundle(t, "csv-b", "pkg-1", []string{"stable"}, newUnstructuredCSV(t, "csv-b", ""))),
356+
withBundleImage("quay.io/my/bundle-bb", newBundle(t, "csv-bb", "pkg-1", []string{"stable"}, newUnstructuredCSV(t, "csv-bb", "csv-b"))),
357+
},
358+
pkgs: []registry.PackageManifest{
359+
{
360+
PackageName: "pkg-0",
361+
Channels: []registry.PackageChannel{
362+
{
363+
Name: "stable",
364+
CurrentCSVName: "csv-aa",
365+
},
366+
},
367+
DefaultChannelName: "stable",
368+
},
369+
{
370+
PackageName: "pkg-1",
371+
Channels: []registry.PackageChannel{
372+
{
373+
Name: "stable",
374+
CurrentCSVName: "csv-bb",
375+
},
376+
},
377+
DefaultChannelName: "stable",
378+
},
379+
},
380+
deprecatedPaths: []string{
381+
"quay.io/my/bundle-a",
382+
"quay.io/my/bundle-b",
383+
},
384+
},
385+
args: args{
386+
pkg: "pkg-0", // Should result in a alone being dropped from the deprecated table
387+
},
388+
expected: expected{
389+
err: nil,
390+
deprecated: map[string]struct{}{
391+
"csv-b": struct{}{},
392+
},
393+
},
394+
},
395+
{
396+
description: "DeprecateTruncate/DropsTruncated",
397+
fields: fields{
398+
bundles: []*registry.Bundle{
399+
withBundleImage("quay.io/my/bundle-a", newBundle(t, "csv-a", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-a", ""))),
400+
withBundleImage("quay.io/my/bundle-aa", newBundle(t, "csv-aa", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-aa", "csv-a"))),
401+
withBundleImage("quay.io/my/bundle-aaa", newBundle(t, "csv-aaa", "pkg-0", []string{"stable"}, newUnstructuredCSV(t, "csv-aaa", "csv-aa"))),
402+
},
403+
pkgs: []registry.PackageManifest{
404+
{
405+
PackageName: "pkg-0",
406+
Channels: []registry.PackageChannel{
407+
{
408+
Name: "stable",
409+
CurrentCSVName: "csv-aaa",
410+
},
411+
},
412+
DefaultChannelName: "stable",
413+
},
414+
},
415+
deprecatedPaths: []string{
416+
"quay.io/my/bundle-a",
417+
"quay.io/my/bundle-aa", // Should truncate a, dropping it from the deprecated table
418+
},
419+
},
420+
expected: expected{
421+
err: nil,
422+
deprecated: map[string]struct{}{
423+
"csv-aa": struct{}{}, // csv-b remains in the deprecated table since it has been truncated and hasn't been removed
424+
},
425+
},
426+
},
427+
}
428+
429+
for _, tt := range tests {
430+
t.Run(tt.description, func(t *testing.T) {
431+
db, cleanup := CreateTestDb(t)
432+
defer cleanup()
433+
store, err := NewDeprecationAwareLoader(db)
434+
require.NoError(t, err)
435+
err = store.Migrate(context.TODO())
436+
require.NoError(t, err)
437+
438+
for _, bundle := range tt.fields.bundles {
439+
require.NoError(t, store.AddOperatorBundle(bundle))
440+
}
441+
442+
for _, pkg := range tt.fields.pkgs {
443+
require.NoError(t, store.AddPackageChannels(pkg))
444+
}
445+
446+
for _, deprecatedPath := range tt.fields.deprecatedPaths {
447+
require.NoError(t, store.DeprecateBundle(deprecatedPath))
448+
}
449+
450+
if tt.args.pkg != "" {
451+
err = store.RemovePackage(tt.args.pkg)
452+
if tt.expected.err != nil {
453+
require.Error(t, err)
454+
} else {
455+
require.NoError(t, err)
456+
}
457+
}
458+
459+
tx, err := db.Begin()
460+
require.NoError(t, err)
461+
462+
rows, err := tx.Query(`SELECT operatorbundle_name FROM deprecated`)
463+
require.NoError(t, err)
464+
require.NotNil(t, rows)
465+
466+
var bundleName string
467+
for rows.Next() {
468+
require.NoError(t, rows.Scan(&bundleName))
469+
_, ok := tt.expected.deprecated[bundleName]
470+
require.True(t, ok, "bundle shouldn't be in the deprecated table: %s", bundleName)
471+
delete(tt.expected.deprecated, bundleName)
472+
}
473+
474+
require.Len(t, tt.expected.deprecated, 0, "not all expected bundles exist in deprecated table: %v", tt.expected.deprecated)
475+
})
476+
}
477+
}
478+
267479
func TestGetTailFromBundle(t *testing.T) {
268480
type fields struct {
269481
bundles []*registry.Bundle
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
)
7+
8+
const RmTruncatedDeprecationsMigrationKey = 13
9+
10+
// Register this migration
11+
func init() {
12+
registerMigration(RmTruncatedDeprecationsMigrationKey, rmTruncatedDeprecationsMigration)
13+
}
14+
15+
var rmTruncatedDeprecationsMigration = &Migration{
16+
Id: RmTruncatedDeprecationsMigrationKey,
17+
Up: func(ctx context.Context, tx *sql.Tx) error {
18+
19+
// Delete deprecation history for all bundles that no longer exist in the channel_entries table
20+
// These bundles have been truncated by more recent deprecations and would only confuse future operations on an index;
21+
// e.g. adding a previously truncated bundle to a package removed via `opm index|registry rm` would lead to that bundle
22+
// being deprecated
23+
_, err := tx.ExecContext(ctx, `DELETE FROM deprecated WHERE deprecated.operatorbundle_name NOT IN (SELECT DISTINCT deprecated.operatorbundle_name FROM (deprecated INNER JOIN channel_entry ON deprecated.operatorbundle_name = channel_entry.operatorbundle_name))`)
24+
25+
return err
26+
},
27+
Down: func(ctx context.Context, tx *sql.Tx) error {
28+
// No-op
29+
return nil
30+
},
31+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package migrations_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/operator-framework/operator-registry/pkg/sqlite/migrations"
11+
)
12+
13+
func TestRmTruncatedDeprecations(t *testing.T) {
14+
db, migrator, cleanup := CreateTestDbAt(t, migrations.RmTruncatedDeprecationsMigrationKey-1)
15+
defer cleanup()
16+
17+
// Insert fixtures to satisfy foreign key constraints
18+
insertBundle := "INSERT INTO operatorbundle(name, version, bundlepath, csv) VALUES (?, ?, ?, ?)"
19+
insertChannel := "INSERT INTO channel(name, package_name, head_operatorbundle_name) VALUES (?, ?, ?)"
20+
insertChannelEntry := "INSERT INTO channel_entry(entry_id, channel_name, package_name, operatorbundle_name) VALUES (?, ?, ?, ?)"
21+
insertDeprecated := "INSERT INTO deprecated(operatorbundle_name) VALUES (?)"
22+
23+
// Add a deprecated bundle
24+
_, err := db.Exec(insertBundle, "operator.v1.0.0", "1.0.0", "quay.io/operator:v1.0.0", "operator.v1.0.0's csv")
25+
require.NoError(t, err)
26+
_, err = db.Exec(insertChannel, "stable", "apple", "operator.v1.0.0")
27+
require.NoError(t, err)
28+
_, err = db.Exec(insertChannelEntry, 0, "stable", "apple", "operator.v1.0.0")
29+
require.NoError(t, err)
30+
_, err = db.Exec(insertDeprecated, "operator.v1.0.0")
31+
require.NoError(t, err)
32+
33+
// Add a truncated bundle; i.e. doesn't exist in the channel_entry table
34+
_, err = db.Exec(insertDeprecated, "operator.v1.0.0-pre")
35+
36+
// This migration should delete all bundles that are not referenced by the channel_entry table
37+
require.NoError(t, migrator.Up(context.Background(), migrations.Only(migrations.RmTruncatedDeprecationsMigrationKey)))
38+
39+
deprecated, err := db.Query("SELECT * FROM deprecated")
40+
require.NoError(t, err)
41+
defer deprecated.Close()
42+
43+
require.True(t, deprecated.Next(), "failed to detect deprecated bundle")
44+
var name sql.NullString
45+
require.NoError(t, deprecated.Scan(&name))
46+
require.True(t, name.Valid)
47+
require.Equal(t, "operator.v1.0.0", name.String)
48+
require.False(t, deprecated.Next(), "incorrect number of deprecated bundles")
49+
}

vendor/github.com/operator-framework/operator-registry/pkg/lib/registry/registry.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)