Skip to content

Merge feature/run-bundle into master: Add FBC support to run bundle and run bundle-upgrade commands #5809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelog/fragments/sdk-fbc-run-bundle(-upgrade).yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
entries:
- description: >
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)
and [run bundle-upgrade](https://sdk.operatorframework.io/docs/cli/operator-sdk_run_bundle-upgrade/) so that
new indexes created by these subcommands are using the new format.
Users are able to pass in an index catalog with FBC format via the flag option `--index-image`.

# kind is one of:
# - addition
# - change
# - deprecation
# - removal
# - bugfix
kind: change

# Is this a breaking change?
breaking: false
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ require (
github.com/garyburd/redigo v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.1.0 // indirect
github.com/go-git/go-git/v5 v5.3.0 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
Expand All @@ -112,6 +115,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/uuid v3.3.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-migrate/migrate/v4 v4.6.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
Expand All @@ -134,7 +138,9 @@ require (
github.com/huandu/xstrings v1.3.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmoiron/sqlx v1.3.1 // indirect
github.com/joelanford/ignore v0.0.0-20210607151042-0d25dc18b62d // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand All @@ -149,10 +155,12 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.7 // indirect
github.com/mattn/go-sqlite3 v1.14.10 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.1.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
Expand Down Expand Up @@ -228,6 +236,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiserver v0.23.5 // indirect
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/operator-sdk/run/bundle/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ The main purpose of this command is to streamline running the bundle without hav

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.
This is an optional flag which will default to ` + "`quay.io/operator-framework/opm:latest`." + `
The index image provided should **NOT** already have the bundle.
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
as the annotations for channels are ignored but it is still a useful flag to have to validate the dependencies.
For example: It does not fail fast when the bundle version provided is <= ChannelHead.
`,
Args: cobra.ExactArgs(1),
PreRunE: func(*cobra.Command, []string) error { return cfg.Load() },
Expand Down
151 changes: 151 additions & 0 deletions internal/olm/fbcutil/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2022 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fbcutil

import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"

"github.com/operator-framework/operator-registry/alpha/action"
"github.com/operator-framework/operator-registry/alpha/declcfg"
declarativeconfig "github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/pkg/containertools"
registryutil "github.com/operator-framework/operator-sdk/internal/registry"
log "github.com/sirupsen/logrus"
)

const (
SchemaChannel = "olm.channel"
SchemaPackage = "olm.package"
DefaultChannel = "operator-sdk-run"
)

const (
// defaultIndexImageBase is the base for defaultIndexImage. It is necessary to separate
// them for string comparison when defaulting bundle add mode.
DefaultIndexImageBase = "quay.io/operator-framework/opm:"
// DefaultIndexImage is the index base image used if none is specified. It contains no bundles.
// TODO(v2.0.0): pin this image tag to a specific version.
DefaultIndexImage = DefaultIndexImageBase + "latest"
)

// BundleDeclcfg represents a minimal File-Based Catalog.
// This struct only consists of one Package, Bundle, and Channel blob. It is used to
// represent the bundle image in the File-Based Catalog format.
type BundleDeclcfg struct {
Package declcfg.Package
Channel declcfg.Channel
Bundle declcfg.Bundle
}

// FBCContext is a struct that stores all the required information while constructing
// a new File-Based Catalog on the fly. The fields from this struct are passed as
// parameters to Operator Registry API calls to generate declarative config objects.
type FBCContext struct {
Package string
ChannelName string
Refs []string
ChannelEntry declarativeconfig.ChannelEntry
}

// CreateFBC generates an FBC by creating bundle, package and channel blobs.
func (f *FBCContext) CreateFBC(ctx context.Context) (BundleDeclcfg, error) {
var bundleDC BundleDeclcfg
// Rendering the bundle image into a declarative config format.
declcfg, err := RenderRefs(ctx, f.Refs)
if err != nil {
return BundleDeclcfg{}, err
}

// Ensuring a valid bundle size.
if len(declcfg.Bundles) != 1 {
return BundleDeclcfg{}, fmt.Errorf("bundle image should contain exactly one bundle blob")
}

bundleDC.Bundle = declcfg.Bundles[0]

// generate package.
bundleDC.Package = declarativeconfig.Package{
Schema: SchemaPackage,
Name: f.Package,
DefaultChannel: f.ChannelName,
}

// generate channel.
bundleDC.Channel = declarativeconfig.Channel{
Schema: SchemaChannel,
Name: f.ChannelName,
Package: f.Package,
Entries: []declarativeconfig.ChannelEntry{f.ChannelEntry},
}

return bundleDC, nil
}

// ValidateAndStringify first converts the generated declarative config to a model and validates it.
// If the declarative config model is valid, it will convert the declarative config to a YAML string and return it.
func ValidateAndStringify(declcfg *declarativeconfig.DeclarativeConfig) (string, error) {
// validates and converts declarative config to model
_, err := declarativeconfig.ConvertToModel(*declcfg)
if err != nil {
return "", fmt.Errorf("error converting the declarative config to model: %v", err)
}

var buf bytes.Buffer
err = declarativeconfig.WriteYAML(*declcfg, &buf)
if err != nil {
return "", fmt.Errorf("error writing generated declarative config to JSON encoder: %v", err)
}

if buf.String() == "" {
return "", errors.New("file-based catalog contents cannot be empty")
}

return buf.String(), nil
}

// RenderRefs will invoke Operator Registry APIs and return a declarative config object representation
// of the references that are passed in as a string array.
func RenderRefs(ctx context.Context, refs []string) (*declarativeconfig.DeclarativeConfig, error) {
render := action.Render{
Refs: refs,
}

log.SetOutput(ioutil.Discard)
declcfg, err := render.Run(ctx)
log.SetOutput(os.Stdout)
if err != nil {
return nil, fmt.Errorf("error in rendering the bundle and index image: %v", err)
}

return declcfg, nil
}

// IsFBC will determine if an index image uses the File-Based Catalog or SQLite index image format.
// The default index image will adopt the File-Based Catalog format.
func IsFBC(ctx context.Context, indexImage string) (bool, error) {
// adding updates to the IndexImageCatalogCreator if it is an FBC image
catalogLabels, err := registryutil.GetImageLabels(ctx, nil, indexImage, false)
if err != nil {
return false, fmt.Errorf("get index image labels: %v", err)
}
_, hasFBCLabel := catalogLabels[containertools.ConfigsLocationLabel]

return hasFBCLabel || indexImage == DefaultIndexImage, nil
}
137 changes: 134 additions & 3 deletions internal/olm/operator/bundle/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ package bundle

import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"

"github.com/operator-framework/api/pkg/operators/v1alpha1"
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
log "github.com/sirupsen/logrus"
"github.com/spf13/pflag"

"github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/operator-registry/alpha/action"
declarativeconfig "github.com/operator-framework/operator-registry/alpha/declcfg"
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
fbcutil "github.com/operator-framework/operator-sdk/internal/olm/fbcutil"
"github.com/operator-framework/operator-sdk/internal/olm/operator"
"github.com/operator-framework/operator-sdk/internal/olm/operator/registry"
)
Expand All @@ -46,7 +53,7 @@ func NewInstall(cfg *operator.Configuration) Install {
}

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

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

// check if index image adopts File-Based Catalog or SQLite index image format
isFBCImage, err := fbcutil.IsFBC(ctx, i.IndexImageCatalogCreator.IndexImage)
if err != nil {
return fmt.Errorf("error in upgrading the bundle %q that was installed traditionally", i.IndexImageCatalogCreator.BundleImage)
}
i.IndexImageCatalogCreator.HasFBCLabel = isFBCImage

// set the field to true if FBC label is on the image or for a default index image.
if i.IndexImageCatalogCreator.HasFBCLabel {
if i.IndexImageCatalogCreator.BundleAddMode != "" {
return fmt.Errorf("specifying the bundle add mode is not supported for File-Based Catalog bundles and index images")
}
} else {
// index image is of the SQLite index format.
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)
log.Warn(deprecationMsg)
}

if i.IndexImageCatalogCreator.HasFBCLabel {
// FBC variables
f := &fbcutil.FBCContext{
Package: labels[registrybundle.PackageLabel],
Refs: []string{i.BundleImage},
ChannelEntry: declarativeconfig.ChannelEntry{
Name: csv.Name,
},
}

if _, hasChannelMetadata := labels[registrybundle.ChannelsLabel]; hasChannelMetadata {
f.ChannelName = strings.Split(labels[registrybundle.ChannelsLabel], ",")[0]
} else {
f.ChannelName = fbcutil.DefaultChannel
}

// generate an fbc if an fbc specific label is found on the image or for a default index image.
content, err := generateFBCContent(ctx, f, i.BundleImage, i.IndexImageCatalogCreator.IndexImage)
if err != nil {
return fmt.Errorf("error generating File-Based Catalog with bundle %q: %v", i.BundleImage, err)
}

i.IndexImageCatalogCreator.FBCContent = content
}

i.OperatorInstaller.PackageName = labels[registrybundle.PackageLabel]
i.OperatorInstaller.CatalogSourceName = operator.CatalogNameForPackage(i.OperatorInstaller.PackageName)
i.OperatorInstaller.StartingCSV = csv.Name
Expand All @@ -98,3 +148,84 @@ func (i *Install) setup(ctx context.Context) error {

return nil
}

// generateFBCContent creates a File-Based Catalog using the bundle image and index image from the run bundle command.
func generateFBCContent(ctx context.Context, f *fbcutil.FBCContext, bundleImage, indexImage string) (string, error) {
log.Infof("Creating a File-Based Catalog of the bundle %q", bundleImage)
// generate a File-Based Catalog representation of the bundle image
bundleDeclcfg, err := f.CreateFBC(ctx)
if err != nil {
return "", fmt.Errorf("error creating a File-Based Catalog with image %q: %v", bundleImage, err)
}

declcfg := &declarativeconfig.DeclarativeConfig{
Bundles: []declarativeconfig.Bundle{bundleDeclcfg.Bundle},
Packages: []declarativeconfig.Package{bundleDeclcfg.Package},
Channels: []declarativeconfig.Channel{bundleDeclcfg.Channel},
}

if indexImage != fbcutil.DefaultIndexImage { // non-default index image was specified.
// since an index image is specified, the bundle image will be added to the index image.
// addBundleToIndexImage will ensure that the bundle is not already present in the index image and error out if it does.
declcfg, err = addBundleToIndexImage(ctx, indexImage, bundleDeclcfg)
if err != nil {
return "", fmt.Errorf("error adding bundle image %q to index image %q: %v", bundleImage, indexImage, err)
}
}

// validate the declarative config and convert it to a string
var content string
if content, err = fbcutil.ValidateAndStringify(declcfg); err != nil {
return "", fmt.Errorf("error validating and converting the declarative config object to a string format: %v", err)
}

log.Infof("Generated a valid File-Based Catalog")

return content, nil
}

// addBundleToIndexImage adds the bundle to an existing index image if the bundle is not already present in the index image.
func addBundleToIndexImage(ctx context.Context, indexImage string, bundleDeclConfig fbcutil.BundleDeclcfg) (*declarativeconfig.DeclarativeConfig, error) {
log.Infof("Rendering a File-Based Catalog of the Index Image %q", indexImage)
log.SetOutput(ioutil.Discard)
render := action.Render{
Refs: []string{indexImage},
}

imageDeclConfig, err := render.Run(ctx)
log.SetOutput(os.Stdout)
if err != nil {
return nil, fmt.Errorf("error rendering the index image %q: %v", indexImage, err)
}

for _, bundle := range imageDeclConfig.Bundles {
if bundle.Name == bundleDeclConfig.Bundle.Name && bundle.Package == bundleDeclConfig.Bundle.Package {
return nil, fmt.Errorf("bundle %q already exists in the index image: %s", bundleDeclConfig.Bundle.Name, indexImage)
}
}

for _, channel := range imageDeclConfig.Channels {
if channel.Name == bundleDeclConfig.Channel.Name && channel.Package == bundleDeclConfig.Bundle.Package {
return nil, fmt.Errorf("channel %q already exists in the index image: %s", bundleDeclConfig.Channel.Name, indexImage)
}
}

var isPackagePresent bool
for _, pkg := range imageDeclConfig.Packages {
if pkg.Name == bundleDeclConfig.Package.Name {
isPackagePresent = true
break
}
}

imageDeclConfig.Bundles = append(imageDeclConfig.Bundles, bundleDeclConfig.Bundle)
imageDeclConfig.Channels = append(imageDeclConfig.Channels, bundleDeclConfig.Channel)

if !isPackagePresent {
imageDeclConfig.Packages = append(imageDeclConfig.Packages, bundleDeclConfig.Package)
}

log.Infof("Inserted the new bundle %q into the index image: %s", bundleDeclConfig.Bundle.Name, indexImage)

return imageDeclConfig, nil
}
Loading