Skip to content

Commit 0eb9ae4

Browse files
Implement File-Based Catalog support for run bundle and run bundle-upgrade subcommands (#5809)
* Implement File-Based Catalog support for `run bundle` and `run bundle-upgrade` command in SDK * Create a valid File-Based Catalog by generating bundle, package and channel blobs for a given bundle image * Generate a valid FBC if index image provided in the CLI is FBC - add bundle to index if bundle is not present in index * Infer the image type (SQLite/FBC) based on the image label and handle FBC scenarios accordingly * Validate the generated FBC and convert to a string format * Add a new registry pod container creation command to support FBC type and use new opm commands Signed-off-by: rashmigottipati <[email protected]>
1 parent 07b7a7f commit 0eb9ae4

File tree

13 files changed

+926
-73
lines changed

13 files changed

+926
-73
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
entries:
2+
- description: >
3+
Add support for File-Based Catalog to the subcommands [operator-sdk run bundle](https://sdk.operatorframework.io/docs/cli/operator-sdk_run_bundle/#m-docsclioperator-sdk_run_bundle)
4+
and [run bundle-upgrade](https://sdk.operatorframework.io/docs/cli/operator-sdk_run_bundle-upgrade/) so that
5+
new indexes created by these subcommands are using the new format.
6+
Users are able to pass in an index catalog with FBC format via the flag option `--index-image`.
7+
8+
# kind is one of:
9+
# - addition
10+
# - change
11+
# - deprecation
12+
# - removal
13+
# - bugfix
14+
kind: change
15+
16+
# Is this a breaking change?
17+
breaking: false

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ require (
103103
github.com/garyburd/redigo v1.6.0 // indirect
104104
github.com/ghodss/yaml v1.0.0 // indirect
105105
github.com/go-errors/errors v1.0.1 // indirect
106+
github.com/go-git/gcfg v1.5.0 // indirect
107+
github.com/go-git/go-billy/v5 v5.1.0 // indirect
108+
github.com/go-git/go-git/v5 v5.3.0 // indirect
106109
github.com/go-logr/zapr v1.2.0 // indirect
107110
github.com/go-openapi/jsonpointer v0.19.5 // indirect
108111
github.com/go-openapi/jsonreference v0.19.5 // indirect
@@ -112,6 +115,7 @@ require (
112115
github.com/gobwas/glob v0.2.3 // indirect
113116
github.com/gofrs/uuid v3.3.0+incompatible // indirect
114117
github.com/gogo/protobuf v1.3.2 // indirect
118+
github.com/golang-migrate/migrate/v4 v4.6.2 // indirect
115119
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
116120
github.com/golang/protobuf v1.5.2 // indirect
117121
github.com/google/btree v1.0.1 // indirect
@@ -134,7 +138,9 @@ require (
134138
github.com/huandu/xstrings v1.3.1 // indirect
135139
github.com/imdario/mergo v0.3.12 // indirect
136140
github.com/inconshreveable/mousetrap v1.0.0 // indirect
141+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
137142
github.com/jmoiron/sqlx v1.3.1 // indirect
143+
github.com/joelanford/ignore v0.0.0-20210607151042-0d25dc18b62d // indirect
138144
github.com/joho/godotenv v1.3.0 // indirect
139145
github.com/josharian/intern v1.0.0 // indirect
140146
github.com/json-iterator/go v1.1.12 // indirect
@@ -149,10 +155,12 @@ require (
149155
github.com/mattn/go-colorable v0.1.12 // indirect
150156
github.com/mattn/go-isatty v0.0.14 // indirect
151157
github.com/mattn/go-runewidth v0.0.7 // indirect
158+
github.com/mattn/go-sqlite3 v1.14.10 // indirect
152159
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
153160
github.com/mitchellh/copystructure v1.1.1 // indirect
154161
github.com/mitchellh/go-homedir v1.1.0 // indirect
155162
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
163+
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
156164
github.com/mitchellh/mapstructure v1.4.3 // indirect
157165
github.com/mitchellh/reflectwalk v1.0.1 // indirect
158166
github.com/moby/spdystream v0.2.0 // indirect
@@ -228,6 +236,7 @@ require (
228236
gopkg.in/inf.v0 v0.9.1 // indirect
229237
gopkg.in/ini.v1 v1.66.2 // indirect
230238
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
239+
gopkg.in/warnings.v0 v0.1.2 // indirect
231240
gopkg.in/yaml.v2 v2.4.0 // indirect
232241
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
233242
k8s.io/apiserver v0.23.5 // indirect

internal/cmd/operator-sdk/run/bundle/cmd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ The main purpose of this command is to streamline running the bundle without hav
3636
3737
The ` + "`--index-image`" + ` flag specifies an index image in which to inject the given bundle. It can be specified to resolve dependencies for a bundle.
3838
This is an optional flag which will default to ` + "`quay.io/operator-framework/opm:latest`." + `
39-
The index image provided should **NOT** already have the bundle.
39+
The index image provided should **NOT** already have the bundle. A limitation of the index image flag is that it does not check the upgrade graph
40+
as the annotations for channels are ignored but it is still a useful flag to have to validate the dependencies.
41+
For example: It does not fail fast when the bundle version provided is <= ChannelHead.
4042
`,
4143
Args: cobra.ExactArgs(1),
4244
PreRunE: func(*cobra.Command, []string) error { return cfg.Load() },

internal/olm/fbcutil/util.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2022 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package fbcutil
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"errors"
21+
"fmt"
22+
"io/ioutil"
23+
"os"
24+
25+
"github.com/operator-framework/operator-registry/alpha/action"
26+
"github.com/operator-framework/operator-registry/alpha/declcfg"
27+
declarativeconfig "github.com/operator-framework/operator-registry/alpha/declcfg"
28+
"github.com/operator-framework/operator-registry/pkg/containertools"
29+
registryutil "github.com/operator-framework/operator-sdk/internal/registry"
30+
log "github.com/sirupsen/logrus"
31+
)
32+
33+
const (
34+
SchemaChannel = "olm.channel"
35+
SchemaPackage = "olm.package"
36+
DefaultChannel = "operator-sdk-run"
37+
)
38+
39+
const (
40+
// defaultIndexImageBase is the base for defaultIndexImage. It is necessary to separate
41+
// them for string comparison when defaulting bundle add mode.
42+
DefaultIndexImageBase = "quay.io/operator-framework/opm:"
43+
// DefaultIndexImage is the index base image used if none is specified. It contains no bundles.
44+
// TODO(v2.0.0): pin this image tag to a specific version.
45+
DefaultIndexImage = DefaultIndexImageBase + "latest"
46+
)
47+
48+
// BundleDeclcfg represents a minimal File-Based Catalog.
49+
// This struct only consists of one Package, Bundle, and Channel blob. It is used to
50+
// represent the bundle image in the File-Based Catalog format.
51+
type BundleDeclcfg struct {
52+
Package declcfg.Package
53+
Channel declcfg.Channel
54+
Bundle declcfg.Bundle
55+
}
56+
57+
// FBCContext is a struct that stores all the required information while constructing
58+
// a new File-Based Catalog on the fly. The fields from this struct are passed as
59+
// parameters to Operator Registry API calls to generate declarative config objects.
60+
type FBCContext struct {
61+
Package string
62+
ChannelName string
63+
Refs []string
64+
ChannelEntry declarativeconfig.ChannelEntry
65+
}
66+
67+
// CreateFBC generates an FBC by creating bundle, package and channel blobs.
68+
func (f *FBCContext) CreateFBC(ctx context.Context) (BundleDeclcfg, error) {
69+
var bundleDC BundleDeclcfg
70+
// Rendering the bundle image into a declarative config format.
71+
declcfg, err := RenderRefs(ctx, f.Refs)
72+
if err != nil {
73+
return BundleDeclcfg{}, err
74+
}
75+
76+
// Ensuring a valid bundle size.
77+
if len(declcfg.Bundles) != 1 {
78+
return BundleDeclcfg{}, fmt.Errorf("bundle image should contain exactly one bundle blob")
79+
}
80+
81+
bundleDC.Bundle = declcfg.Bundles[0]
82+
83+
// generate package.
84+
bundleDC.Package = declarativeconfig.Package{
85+
Schema: SchemaPackage,
86+
Name: f.Package,
87+
DefaultChannel: f.ChannelName,
88+
}
89+
90+
// generate channel.
91+
bundleDC.Channel = declarativeconfig.Channel{
92+
Schema: SchemaChannel,
93+
Name: f.ChannelName,
94+
Package: f.Package,
95+
Entries: []declarativeconfig.ChannelEntry{f.ChannelEntry},
96+
}
97+
98+
return bundleDC, nil
99+
}
100+
101+
// ValidateAndStringify first converts the generated declarative config to a model and validates it.
102+
// If the declarative config model is valid, it will convert the declarative config to a YAML string and return it.
103+
func ValidateAndStringify(declcfg *declarativeconfig.DeclarativeConfig) (string, error) {
104+
// validates and converts declarative config to model
105+
_, err := declarativeconfig.ConvertToModel(*declcfg)
106+
if err != nil {
107+
return "", fmt.Errorf("error converting the declarative config to model: %v", err)
108+
}
109+
110+
var buf bytes.Buffer
111+
err = declarativeconfig.WriteYAML(*declcfg, &buf)
112+
if err != nil {
113+
return "", fmt.Errorf("error writing generated declarative config to JSON encoder: %v", err)
114+
}
115+
116+
if buf.String() == "" {
117+
return "", errors.New("file-based catalog contents cannot be empty")
118+
}
119+
120+
return buf.String(), nil
121+
}
122+
123+
// RenderRefs will invoke Operator Registry APIs and return a declarative config object representation
124+
// of the references that are passed in as a string array.
125+
func RenderRefs(ctx context.Context, refs []string) (*declarativeconfig.DeclarativeConfig, error) {
126+
render := action.Render{
127+
Refs: refs,
128+
}
129+
130+
log.SetOutput(ioutil.Discard)
131+
declcfg, err := render.Run(ctx)
132+
log.SetOutput(os.Stdout)
133+
if err != nil {
134+
return nil, fmt.Errorf("error in rendering the bundle and index image: %v", err)
135+
}
136+
137+
return declcfg, nil
138+
}
139+
140+
// IsFBC will determine if an index image uses the File-Based Catalog or SQLite index image format.
141+
// The default index image will adopt the File-Based Catalog format.
142+
func IsFBC(ctx context.Context, indexImage string) (bool, error) {
143+
// adding updates to the IndexImageCatalogCreator if it is an FBC image
144+
catalogLabels, err := registryutil.GetImageLabels(ctx, nil, indexImage, false)
145+
if err != nil {
146+
return false, fmt.Errorf("get index image labels: %v", err)
147+
}
148+
_, hasFBCLabel := catalogLabels[containertools.ConfigsLocationLabel]
149+
150+
return hasFBCLabel || indexImage == DefaultIndexImage, nil
151+
}

internal/olm/operator/bundle/install.go

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ package bundle
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"io/ioutil"
21+
"os"
1922
"strings"
2023

21-
"github.com/operator-framework/api/pkg/operators/v1alpha1"
22-
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
24+
log "github.com/sirupsen/logrus"
2325
"github.com/spf13/pflag"
2426

27+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
28+
"github.com/operator-framework/operator-registry/alpha/action"
29+
declarativeconfig "github.com/operator-framework/operator-registry/alpha/declcfg"
30+
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
31+
fbcutil "github.com/operator-framework/operator-sdk/internal/olm/fbcutil"
2532
"github.com/operator-framework/operator-sdk/internal/olm/operator"
2633
"github.com/operator-framework/operator-sdk/internal/olm/operator/registry"
2734
)
@@ -46,7 +53,7 @@ func NewInstall(cfg *operator.Configuration) Install {
4653
}
4754

4855
func (i *Install) BindFlags(fs *pflag.FlagSet) {
49-
fs.StringVar(&i.IndexImage, "index-image", registry.DefaultIndexImage, "index image in which to inject bundle")
56+
fs.StringVar(&i.IndexImage, "index-image", fbcutil.DefaultIndexImage, "index image in which to inject bundle")
5057
fs.Var(&i.InstallMode, "install-mode", "install mode")
5158

5259
// --mode is hidden so only users who know what they're doing can alter add mode.
@@ -87,6 +94,49 @@ func (i *Install) setup(ctx context.Context) error {
8794
return err
8895
}
8996

97+
// check if index image adopts File-Based Catalog or SQLite index image format
98+
isFBCImage, err := fbcutil.IsFBC(ctx, i.IndexImageCatalogCreator.IndexImage)
99+
if err != nil {
100+
return fmt.Errorf("error in upgrading the bundle %q that was installed traditionally", i.IndexImageCatalogCreator.BundleImage)
101+
}
102+
i.IndexImageCatalogCreator.HasFBCLabel = isFBCImage
103+
104+
// set the field to true if FBC label is on the image or for a default index image.
105+
if i.IndexImageCatalogCreator.HasFBCLabel {
106+
if i.IndexImageCatalogCreator.BundleAddMode != "" {
107+
return fmt.Errorf("specifying the bundle add mode is not supported for File-Based Catalog bundles and index images")
108+
}
109+
} else {
110+
// index image is of the SQLite index format.
111+
deprecationMsg := fmt.Sprintf("%s is a SQLite index image. SQLite based index images are being deprecated and will be removed in a future release, please migrate your catalogs to the new File-Based Catalog format", i.IndexImageCatalogCreator.IndexImage)
112+
log.Warn(deprecationMsg)
113+
}
114+
115+
if i.IndexImageCatalogCreator.HasFBCLabel {
116+
// FBC variables
117+
f := &fbcutil.FBCContext{
118+
Package: labels[registrybundle.PackageLabel],
119+
Refs: []string{i.BundleImage},
120+
ChannelEntry: declarativeconfig.ChannelEntry{
121+
Name: csv.Name,
122+
},
123+
}
124+
125+
if _, hasChannelMetadata := labels[registrybundle.ChannelsLabel]; hasChannelMetadata {
126+
f.ChannelName = strings.Split(labels[registrybundle.ChannelsLabel], ",")[0]
127+
} else {
128+
f.ChannelName = fbcutil.DefaultChannel
129+
}
130+
131+
// generate an fbc if an fbc specific label is found on the image or for a default index image.
132+
content, err := generateFBCContent(ctx, f, i.BundleImage, i.IndexImageCatalogCreator.IndexImage)
133+
if err != nil {
134+
return fmt.Errorf("error generating File-Based Catalog with bundle %q: %v", i.BundleImage, err)
135+
}
136+
137+
i.IndexImageCatalogCreator.FBCContent = content
138+
}
139+
90140
i.OperatorInstaller.PackageName = labels[registrybundle.PackageLabel]
91141
i.OperatorInstaller.CatalogSourceName = operator.CatalogNameForPackage(i.OperatorInstaller.PackageName)
92142
i.OperatorInstaller.StartingCSV = csv.Name
@@ -98,3 +148,84 @@ func (i *Install) setup(ctx context.Context) error {
98148

99149
return nil
100150
}
151+
152+
// generateFBCContent creates a File-Based Catalog using the bundle image and index image from the run bundle command.
153+
func generateFBCContent(ctx context.Context, f *fbcutil.FBCContext, bundleImage, indexImage string) (string, error) {
154+
log.Infof("Creating a File-Based Catalog of the bundle %q", bundleImage)
155+
// generate a File-Based Catalog representation of the bundle image
156+
bundleDeclcfg, err := f.CreateFBC(ctx)
157+
if err != nil {
158+
return "", fmt.Errorf("error creating a File-Based Catalog with image %q: %v", bundleImage, err)
159+
}
160+
161+
declcfg := &declarativeconfig.DeclarativeConfig{
162+
Bundles: []declarativeconfig.Bundle{bundleDeclcfg.Bundle},
163+
Packages: []declarativeconfig.Package{bundleDeclcfg.Package},
164+
Channels: []declarativeconfig.Channel{bundleDeclcfg.Channel},
165+
}
166+
167+
if indexImage != fbcutil.DefaultIndexImage { // non-default index image was specified.
168+
// since an index image is specified, the bundle image will be added to the index image.
169+
// addBundleToIndexImage will ensure that the bundle is not already present in the index image and error out if it does.
170+
declcfg, err = addBundleToIndexImage(ctx, indexImage, bundleDeclcfg)
171+
if err != nil {
172+
return "", fmt.Errorf("error adding bundle image %q to index image %q: %v", bundleImage, indexImage, err)
173+
}
174+
}
175+
176+
// validate the declarative config and convert it to a string
177+
var content string
178+
if content, err = fbcutil.ValidateAndStringify(declcfg); err != nil {
179+
return "", fmt.Errorf("error validating and converting the declarative config object to a string format: %v", err)
180+
}
181+
182+
log.Infof("Generated a valid File-Based Catalog")
183+
184+
return content, nil
185+
}
186+
187+
// addBundleToIndexImage adds the bundle to an existing index image if the bundle is not already present in the index image.
188+
func addBundleToIndexImage(ctx context.Context, indexImage string, bundleDeclConfig fbcutil.BundleDeclcfg) (*declarativeconfig.DeclarativeConfig, error) {
189+
log.Infof("Rendering a File-Based Catalog of the Index Image %q", indexImage)
190+
log.SetOutput(ioutil.Discard)
191+
render := action.Render{
192+
Refs: []string{indexImage},
193+
}
194+
195+
imageDeclConfig, err := render.Run(ctx)
196+
log.SetOutput(os.Stdout)
197+
if err != nil {
198+
return nil, fmt.Errorf("error rendering the index image %q: %v", indexImage, err)
199+
}
200+
201+
for _, bundle := range imageDeclConfig.Bundles {
202+
if bundle.Name == bundleDeclConfig.Bundle.Name && bundle.Package == bundleDeclConfig.Bundle.Package {
203+
return nil, fmt.Errorf("bundle %q already exists in the index image: %s", bundleDeclConfig.Bundle.Name, indexImage)
204+
}
205+
}
206+
207+
for _, channel := range imageDeclConfig.Channels {
208+
if channel.Name == bundleDeclConfig.Channel.Name && channel.Package == bundleDeclConfig.Bundle.Package {
209+
return nil, fmt.Errorf("channel %q already exists in the index image: %s", bundleDeclConfig.Channel.Name, indexImage)
210+
}
211+
}
212+
213+
var isPackagePresent bool
214+
for _, pkg := range imageDeclConfig.Packages {
215+
if pkg.Name == bundleDeclConfig.Package.Name {
216+
isPackagePresent = true
217+
break
218+
}
219+
}
220+
221+
imageDeclConfig.Bundles = append(imageDeclConfig.Bundles, bundleDeclConfig.Bundle)
222+
imageDeclConfig.Channels = append(imageDeclConfig.Channels, bundleDeclConfig.Channel)
223+
224+
if !isPackagePresent {
225+
imageDeclConfig.Packages = append(imageDeclConfig.Packages, bundleDeclConfig.Package)
226+
}
227+
228+
log.Infof("Inserted the new bundle %q into the index image: %s", bundleDeclConfig.Bundle.Name, indexImage)
229+
230+
return imageDeclConfig, nil
231+
}

0 commit comments

Comments
 (0)