Skip to content

Commit 14b53aa

Browse files
committed
feature: opm (index|registry) prune command
This makes it possible for a user to prune an index image or an operator database referencing a large collection of operator bundles.
1 parent 6a831e7 commit 14b53aa

File tree

10 files changed

+361
-0
lines changed

10 files changed

+361
-0
lines changed

cmd/opm/index/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ func AddCommand(parent *cobra.Command) {
2424
cmd.AddCommand(newIndexDeleteCmd())
2525
addIndexAddCmd(cmd)
2626
cmd.AddCommand(newIndexExportCmd())
27+
cmd.AddCommand(newIndexPruneCmd())
2728
}

cmd/opm/index/prune.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package index
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
"github.com/spf13/cobra"
6+
7+
"github.com/operator-framework/operator-registry/pkg/lib/indexer"
8+
)
9+
10+
func newIndexPruneCmd() *cobra.Command {
11+
indexCmd := &cobra.Command{
12+
Use: "prune",
13+
Short: "prune an index of all but specified packages",
14+
Long: `prune an index of all but specified packages`,
15+
16+
PreRunE: func(cmd *cobra.Command, args []string) error {
17+
if debug, _ := cmd.Flags().GetBool("debug"); debug {
18+
logrus.SetLevel(logrus.DebugLevel)
19+
}
20+
return nil
21+
},
22+
23+
RunE: runIndexPruneCmdFunc,
24+
}
25+
26+
indexCmd.Flags().Bool("debug", false, "enable debug logging")
27+
indexCmd.Flags().Bool("generate", false, "if enabled, just creates the dockerfile and saves it to local disk")
28+
indexCmd.Flags().StringP("out-dockerfile", "d", "", "if generating the dockerfile, this flag is used to (optionally) specify a dockerfile name")
29+
indexCmd.Flags().StringP("from-index", "f", "", "index to prune")
30+
if err := indexCmd.MarkFlagRequired("from-index"); err != nil {
31+
logrus.Panic("Failed to set required `from-index` flag for `index prune`")
32+
}
33+
indexCmd.Flags().StringSliceP("packages", "p", nil, "comma separated list of packages to keep")
34+
if err := indexCmd.MarkFlagRequired("packages"); err != nil {
35+
logrus.Panic("Failed to set required `packages` flag for `index prune`")
36+
}
37+
indexCmd.Flags().StringP("binary-image", "i", "", "container image for on-image `opm` command")
38+
indexCmd.Flags().StringP("container-tool", "c", "podman", "tool to interact with container images (save, build, etc.). One of: [docker, podman]")
39+
indexCmd.Flags().StringP("tag", "t", "", "custom tag for container image being built")
40+
indexCmd.Flags().Bool("permissive", false, "allow registry load errors")
41+
42+
if err := indexCmd.Flags().MarkHidden("debug"); err != nil {
43+
logrus.Panic(err.Error())
44+
}
45+
46+
return indexCmd
47+
48+
}
49+
50+
func runIndexPruneCmdFunc(cmd *cobra.Command, args []string) error {
51+
generate, err := cmd.Flags().GetBool("generate")
52+
if err != nil {
53+
return err
54+
}
55+
56+
outDockerfile, err := cmd.Flags().GetString("out-dockerfile")
57+
if err != nil {
58+
return err
59+
}
60+
61+
fromIndex, err := cmd.Flags().GetString("from-index")
62+
if err != nil {
63+
return err
64+
}
65+
66+
packages, err := cmd.Flags().GetStringSlice("packages")
67+
if err != nil {
68+
return err
69+
}
70+
71+
binaryImage, err := cmd.Flags().GetString("binary-image")
72+
if err != nil {
73+
return err
74+
}
75+
76+
containerTool, err := cmd.Flags().GetString("container-tool")
77+
if err != nil {
78+
return err
79+
}
80+
81+
tag, err := cmd.Flags().GetString("tag")
82+
if err != nil {
83+
return err
84+
}
85+
86+
permissive, err := cmd.Flags().GetBool("permissive")
87+
if err != nil {
88+
return err
89+
}
90+
91+
logger := logrus.WithFields(logrus.Fields{"packages": packages})
92+
93+
logger.Info("building the index")
94+
95+
indexPruner := indexer.NewIndexPruner(containerTool, logger)
96+
97+
request := indexer.PruneFromIndexRequest{
98+
Generate: generate,
99+
FromIndex: fromIndex,
100+
BinarySourceImage: binaryImage,
101+
OutDockerfile: outDockerfile,
102+
Packages: packages,
103+
Tag: tag,
104+
Permissive: permissive,
105+
}
106+
107+
err = indexPruner.PruneFromIndex(request)
108+
if err != nil {
109+
return err
110+
}
111+
112+
return nil
113+
}

cmd/opm/registry/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func NewOpmRegistryCmd() *cobra.Command {
2323
rootCmd.AddCommand(newRegistryServeCmd())
2424
rootCmd.AddCommand(newRegistryAddCmd())
2525
rootCmd.AddCommand(newRegistryRmCmd())
26+
rootCmd.AddCommand(newRegistryPruneCmd())
2627

2728
return rootCmd
2829
}

cmd/opm/registry/prune.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package registry
2+
3+
import (
4+
"github.com/operator-framework/operator-registry/pkg/lib/registry"
5+
6+
"github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func newRegistryPruneCmd() *cobra.Command {
11+
rootCmd := &cobra.Command{
12+
Use: "prune",
13+
Short: "prune an operator registry DB of all but specified packages",
14+
Long: `prune an operator registry DB of all but specified packages`,
15+
16+
PreRunE: func(cmd *cobra.Command, args []string) error {
17+
if debug, _ := cmd.Flags().GetBool("debug"); debug {
18+
logrus.SetLevel(logrus.DebugLevel)
19+
}
20+
return nil
21+
},
22+
23+
RunE: runRegistryPruneCmdFunc,
24+
}
25+
26+
rootCmd.Flags().Bool("debug", false, "enable debug logging")
27+
rootCmd.Flags().StringP("database", "d", "bundles.db", "relative path to database file")
28+
rootCmd.Flags().StringSliceP("packages", "p", []string{}, "comma separated list of package names to be kept")
29+
if err := rootCmd.MarkFlagRequired("packages"); err != nil {
30+
logrus.Panic("Failed to set required `packages` flag for `registry rm`")
31+
}
32+
rootCmd.Flags().Bool("permissive", false, "allow registry load errors")
33+
34+
return rootCmd
35+
}
36+
37+
func runRegistryPruneCmdFunc(cmd *cobra.Command, args []string) error {
38+
fromFilename, err := cmd.Flags().GetString("database")
39+
if err != nil {
40+
return err
41+
}
42+
packages, err := cmd.Flags().GetStringSlice("packages")
43+
if err != nil {
44+
return err
45+
}
46+
permissive, err := cmd.Flags().GetBool("permissive")
47+
if err != nil {
48+
return err
49+
}
50+
51+
request := registry.PruneFromRegistryRequest{
52+
Packages: packages,
53+
InputDatabase: fromFilename,
54+
Permissive: permissive,
55+
}
56+
57+
logger := logrus.WithFields(logrus.Fields{"packages": packages})
58+
59+
logger.Info("pruning from the registry")
60+
61+
registryPruner := registry.NewRegistryPruner(logger)
62+
63+
err = registryPruner.PruneFromRegistry(request)
64+
if err != nil {
65+
return err
66+
}
67+
68+
return nil
69+
}

docs/design/opm-tooling.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ Great! The existing `test-registry.db` file is updated. Now we have a registry t
3434

3535
Calling this on our existing test registry removes all versions of the prometheus operator entirely from the database.
3636

37+
#### prune
38+
39+
`opm` supports specifying which packages should be kept in an operator database. For example:
40+
41+
`opm registry prune -p "prometheus" -d "test-registry.db"`
42+
43+
Would remove all but the `prometheus` package from the operator database.
44+
3745
#### serve
3846

3947
`opm` also includes a command to connect to an existing database and serve a `gRPC` API that handles requests for data about the registry:
@@ -93,6 +101,14 @@ Like `opm registry rm`, this command will remove all versions an entire operator
93101

94102
This will result in the tagged container image `quay.io/operator-framework/monitoring-index:1.0.2` with a registry that no longer contains the `prometheus` operator at all.
95103

104+
#### prune
105+
106+
`opm index prune` allows the user to specify which operator packages should be maintained in an index. For example:
107+
108+
`opm index prune -p "prometheus" --from-index quay.io/operator-framework/example-index:1.0.0 --tag quay.io/operator-framework/example-index:1.0.1`
109+
110+
Would remove all but the `prometheus` package from the index.
111+
96112
#### export
97113

98114
`opm index export` will export a package from an index image into a directory. The format of this directory will match the appregistry manifest format: containing all versions of the package in the index along with a `package.yaml` file. This command takes an `--index` flag that points to an index image, a `--package` flag that states a package name, an optional `--download-folder` as the export location (default is `./downloaded`), and just as the other index commands it takes a `--container-tool` flag. Ex:

pkg/lib/indexer/indexer.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type ImageIndexer struct {
3737
ImageReader containertools.ImageReader
3838
RegistryAdder registry.RegistryAdder
3939
RegistryDeleter registry.RegistryDeleter
40+
RegistryPruner registry.RegistryPruner
4041
ContainerTool string
4142
Logger *logrus.Entry
4243
}
@@ -440,3 +441,57 @@ func generatePackageYaml(dbQuerier pregistry.Query, packageName, downloadPath st
440441

441442
return utilerrors.NewAggregate(errs)
442443
}
444+
445+
// PruneFromIndexRequest defines the parameters to send to the PruneFromIndex API
446+
type PruneFromIndexRequest struct {
447+
Generate bool
448+
Permissive bool
449+
BinarySourceImage string
450+
FromIndex string
451+
OutDockerfile string
452+
Tag string
453+
Packages []string
454+
}
455+
456+
func (i ImageIndexer) PruneFromIndex(request PruneFromIndexRequest) error {
457+
// set a temp directory
458+
tmpDir, err := ioutil.TempDir("./", tmpDirPrefix)
459+
if err != nil {
460+
return err
461+
}
462+
defer os.RemoveAll(tmpDir)
463+
464+
databaseFile, err := i.getDatabaseFile(tmpDir, request.FromIndex)
465+
if err != nil {
466+
return err
467+
}
468+
workingDir := path.Dir(databaseFile)
469+
470+
// Run opm registry prune on the database
471+
pruneFromRegistryReq := registry.PruneFromRegistryRequest{
472+
Packages: request.Packages,
473+
InputDatabase: databaseFile,
474+
Permissive: request.Permissive,
475+
}
476+
477+
// Prune the bundles from the registry
478+
err = i.RegistryPruner.PruneFromRegistry(pruneFromRegistryReq)
479+
if err != nil {
480+
return err
481+
}
482+
483+
// write the dockerfile to disk if generate is set, otherwise shell out to build the image
484+
if request.Generate {
485+
err = i.generateDockerfile(request.BinarySourceImage, request.OutDockerfile, databaseFile)
486+
if err != nil {
487+
return err
488+
}
489+
} else {
490+
err = i.buildDockerfile(request.BinarySourceImage, workingDir, request.Tag)
491+
if err != nil {
492+
return err
493+
}
494+
}
495+
496+
return nil
497+
}

pkg/lib/indexer/interfaces.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,19 @@ func NewIndexExporter(containerTool string, logger *logrus.Entry) IndexExporter
6464
Logger: logger,
6565
}
6666
}
67+
68+
// IndexPruner prunes operators out of an index
69+
type IndexPruner interface {
70+
PruneFromIndex(PruneFromIndexRequest) error
71+
}
72+
73+
func NewIndexPruner(containerTool string, logger *logrus.Entry) IndexPruner {
74+
return ImageIndexer{
75+
DockerfileGenerator: containertools.NewDockerfileGenerator(containerTool, logger),
76+
CommandRunner: containertools.NewCommandRunner(containerTool, logger),
77+
LabelReader: containertools.NewLabelReader(containerTool, logger),
78+
ImageReader: containertools.NewImageReader(containerTool, logger),
79+
ContainerTool: containerTool,
80+
Logger: logger,
81+
}
82+
}

pkg/lib/registry/interfaces.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ func NewRegistryDeleter(logger *logrus.Entry) RegistryDeleter {
2626
Logger: logger,
2727
}
2828
}
29+
30+
type RegistryPruner interface {
31+
PruneFromRegistry(PruneFromRegistryRequest) error
32+
}
33+
34+
func NewRegistryPruner(logger *logrus.Entry) RegistryPruner {
35+
return RegistryUpdater{
36+
Logger: logger,
37+
}
38+
}

pkg/lib/registry/registry.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,55 @@ func (r RegistryUpdater) DeleteFromRegistry(request DeleteFromRegistryRequest) e
135135

136136
return nil
137137
}
138+
139+
type PruneFromRegistryRequest struct {
140+
Permissive bool
141+
InputDatabase string
142+
Packages []string
143+
}
144+
145+
func (r RegistryUpdater) PruneFromRegistry(request PruneFromRegistryRequest) error {
146+
db, err := sql.Open("sqlite3", request.InputDatabase)
147+
if err != nil {
148+
return err
149+
}
150+
defer db.Close()
151+
152+
dbLoader, err := sqlite.NewSQLLiteLoader(db)
153+
if err != nil {
154+
return err
155+
}
156+
if err := dbLoader.Migrate(context.TODO()); err != nil {
157+
return err
158+
}
159+
160+
// get all the packages
161+
lister := sqlite.NewSQLLiteQuerierFromDb(db)
162+
packages, err := lister.ListPackages(context.TODO())
163+
if err != nil {
164+
return err
165+
}
166+
167+
// make it inexpensive to find packages
168+
pkgMap := make(map[string]bool)
169+
for _, pkg := range request.Packages {
170+
pkgMap[pkg] = true
171+
}
172+
173+
// prune packages from registry
174+
for _, pkg := range packages {
175+
if _, found := pkgMap[pkg]; !found {
176+
remover := sqlite.NewSQLRemoverForPackages(dbLoader, pkg)
177+
if err := remover.Remove(); err != nil {
178+
err = fmt.Errorf("error deleting packages from database: %s", err)
179+
if !request.Permissive {
180+
logrus.WithError(err).Fatal("permissive mode disabled")
181+
return err
182+
}
183+
logrus.WithError(err).Warn("permissive mode enabled")
184+
}
185+
}
186+
}
187+
188+
return nil
189+
}

0 commit comments

Comments
 (0)