Skip to content

Commit ea6c6a4

Browse files
committedApr 21, 2020
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 2bdcd07 commit ea6c6a4

File tree

10 files changed

+391
-0
lines changed

10 files changed

+391
-0
lines changed
 

‎cmd/opm/index/cmd.go

+1
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

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

‎cmd/opm/registry/cmd.go

+1
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

+69
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

+16
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

+77
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ImageIndexer struct {
4141
ImageReader containertools.ImageReader
4242
RegistryAdder registry.RegistryAdder
4343
RegistryDeleter registry.RegistryDeleter
44+
RegistryPruner registry.RegistryPruner
4445
ContainerTool containertools.ContainerTool
4546
Logger *logrus.Entry
4647
}
@@ -205,6 +206,82 @@ func (i ImageIndexer) DeleteFromIndex(request DeleteFromIndexRequest) error {
205206
return nil
206207
}
207208

209+
// PruneFromIndexRequest defines the parameters to send to the PruneFromIndex API
210+
type PruneFromIndexRequest struct {
211+
Generate bool
212+
Permissive bool
213+
BinarySourceImage string
214+
FromIndex string
215+
OutDockerfile string
216+
Tag string
217+
Packages []string
218+
}
219+
220+
func (i ImageIndexer) PruneFromIndex(request PruneFromIndexRequest) error {
221+
buildDir, outDockerfile, cleanup, err := buildContext(request.Generate, request.OutDockerfile)
222+
defer cleanup()
223+
if err != nil {
224+
return err
225+
}
226+
227+
// set a temp directory for unpacking an image
228+
// this is in its own function context so that the deferred cleanup runs before we do a docker build
229+
// which prevents the full contents of the previous image from being in the build context
230+
var databasePath string
231+
if err := func () error {
232+
tmpDir, err := ioutil.TempDir("./", tmpDirPrefix)
233+
if err != nil {
234+
235+
return err
236+
}
237+
defer os.RemoveAll(tmpDir)
238+
239+
databaseFile, err := i.getDatabaseFile(tmpDir, request.FromIndex)
240+
if err != nil {
241+
return err
242+
}
243+
// copy the index to the database folder in the build directory
244+
if databasePath, err = copyDatabaseTo(databaseFile, filepath.Join(buildDir, defaultDatabaseFolder)); err != nil {
245+
return err
246+
}
247+
return nil
248+
}(); err != nil {
249+
return err
250+
}
251+
252+
// Run opm registry prune on the database
253+
pruneFromRegistryReq := registry.PruneFromRegistryRequest{
254+
Packages: request.Packages,
255+
InputDatabase: databasePath,
256+
Permissive: request.Permissive,
257+
}
258+
259+
// Prune the bundles from the registry
260+
err = i.RegistryPruner.PruneFromRegistry(pruneFromRegistryReq)
261+
if err != nil {
262+
return err
263+
}
264+
265+
// generate the dockerfile
266+
dockerfile := i.DockerfileGenerator.GenerateIndexDockerfile(request.BinarySourceImage, databasePath)
267+
err = write(dockerfile, outDockerfile, i.Logger)
268+
if err != nil {
269+
return err
270+
}
271+
272+
if request.Generate {
273+
return nil
274+
}
275+
276+
// build the dockerfile
277+
err = build(outDockerfile, request.Tag, i.CommandRunner, i.Logger)
278+
if err != nil {
279+
return err
280+
}
281+
282+
return nil
283+
}
284+
208285
func (i ImageIndexer) getDatabaseFile(workingDir, fromIndex string) (string, error) {
209286
if fromIndex == "" {
210287
return path.Join(workingDir, defaultDatabaseFile), nil

‎pkg/lib/indexer/interfaces.go

+17
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,20 @@ func NewIndexExporter(containerTool containertools.ContainerTool, logger *logrus
6363
Logger: logger,
6464
}
6565
}
66+
67+
// IndexPruner prunes operators out of an index
68+
type IndexPruner interface {
69+
PruneFromIndex(PruneFromIndexRequest) error
70+
}
71+
72+
func NewIndexPruner(containerTool containertools.ContainerTool, logger *logrus.Entry) IndexPruner {
73+
return ImageIndexer{
74+
DockerfileGenerator: containertools.NewDockerfileGenerator(logger),
75+
CommandRunner: containertools.NewCommandRunner(containerTool, logger),
76+
LabelReader: containertools.NewLabelReader(containerTool, logger),
77+
RegistryPruner: registry.NewRegistryPruner(logger),
78+
ImageReader: containertools.NewImageReader(containerTool, logger),
79+
ContainerTool: containerTool,
80+
Logger: logger,
81+
}
82+
}

‎pkg/lib/registry/interfaces.go

+10
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

+52
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,55 @@ func (r RegistryUpdater) DeleteFromRegistry(request DeleteFromRegistryRequest) e
177177

178178
return nil
179179
}
180+
181+
type PruneFromRegistryRequest struct {
182+
Permissive bool
183+
InputDatabase string
184+
Packages []string
185+
}
186+
187+
func (r RegistryUpdater) PruneFromRegistry(request PruneFromRegistryRequest) error {
188+
db, err := sql.Open("sqlite3", request.InputDatabase)
189+
if err != nil {
190+
return err
191+
}
192+
defer db.Close()
193+
194+
dbLoader, err := sqlite.NewSQLLiteLoader(db)
195+
if err != nil {
196+
return err
197+
}
198+
if err := dbLoader.Migrate(context.TODO()); err != nil {
199+
return err
200+
}
201+
202+
// get all the packages
203+
lister := sqlite.NewSQLLiteQuerierFromDb(db)
204+
packages, err := lister.ListPackages(context.TODO())
205+
if err != nil {
206+
return err
207+
}
208+
209+
// make it inexpensive to find packages
210+
pkgMap := make(map[string]bool)
211+
for _, pkg := range request.Packages {
212+
pkgMap[pkg] = true
213+
}
214+
215+
// prune packages from registry
216+
for _, pkg := range packages {
217+
if _, found := pkgMap[pkg]; !found {
218+
remover := sqlite.NewSQLRemoverForPackages(dbLoader, pkg)
219+
if err := remover.Remove(); err != nil {
220+
err = fmt.Errorf("error deleting packages from database: %s", err)
221+
if !request.Permissive {
222+
logrus.WithError(err).Fatal("permissive mode disabled")
223+
return err
224+
}
225+
logrus.WithError(err).Warn("permissive mode enabled")
226+
}
227+
}
228+
}
229+
230+
return nil
231+
}

‎test/e2e/opm_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ var (
3434
bundleTag3 = rand.String(6)
3535
indexTag1 = rand.String(6)
3636
indexTag2 = rand.String(6)
37+
indexTag3 = rand.String(6)
3738

3839
bundleImage = "quay.io/olmtest/e2e-bundle"
3940
indexImage1 = "quay.io/olmtest/e2e-index:" + indexTag1
4041
indexImage2 = "quay.io/olmtest/e2e-index:" + indexTag2
42+
indexImage3 = "quay.io/olmtest/e2e-index:" + indexTag3
4143
)
4244

4345
func inTemporaryBuildContext(f func() error) (rerr error) {
@@ -122,6 +124,24 @@ func buildFromIndexWith(containerTool string) error {
122124
return indexAdder.AddToIndex(request)
123125
}
124126

127+
// TODO(djzager): make this more complete than what should be a simple no-op
128+
func pruneIndexWith(containerTool string) error {
129+
logger := logrus.WithFields(logrus.Fields{"packages": packageName})
130+
indexAdder := indexer.NewIndexPruner(containertools.NewContainerTool(containerTool, containertools.NoneTool), logger)
131+
132+
request := indexer.PruneFromIndexRequest{
133+
Generate: false,
134+
FromIndex: indexImage2,
135+
BinarySourceImage: "",
136+
OutDockerfile: "",
137+
Tag: indexImage3,
138+
Packages: []string{packageName},
139+
Permissive: false,
140+
}
141+
142+
return indexAdder.PruneFromIndex(request)
143+
}
144+
125145
func pushWith(containerTool, image string) error {
126146
dockerpush := exec.Command(containerTool, "push", image)
127147
return dockerpush.Run()
@@ -216,6 +236,14 @@ var _ = Describe("opm", func() {
216236
err = pushWith(containerTool, indexImage2)
217237
Expect(err).NotTo(HaveOccurred())
218238

239+
By("pruning an index")
240+
err = pruneIndexWith(containerTool)
241+
Expect(err).NotTo(HaveOccurred())
242+
243+
By("pushing an index")
244+
err = pushWith(containerTool, indexImage3)
245+
Expect(err).NotTo(HaveOccurred())
246+
219247
By("exporting an index to disk")
220248
err = exportWith(containerTool)
221249
Expect(err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)
Please sign in to comment.