Skip to content
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

feat(opm): add unprivileged registry add #213

Merged
merged 6 commits into from
Apr 2, 2020
Merged
Changes from 1 commit
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
Next Next commit
feat(opm): add unprivileged registry add
- Add the ability to pull and unpack bundle images without using to an external tool or daemon
- Introduce a new set of bundle image oriented interfaces to decouple image interaction from shelling out to external tools
- Add testdata dir and dot-import ginkgo
- Add pull and unpack unit tests
- Deprecate registry add container-tool opt
- Add skip-tls option to registry add
njhale committed Apr 2, 2020
commit 8ae46699fed3666e41dc379bf75264fbd6a28af2
17 changes: 10 additions & 7 deletions cmd/opm/registry/add.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package registry

import (
"github.com/operator-framework/operator-registry/pkg/lib/registry"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/operator-framework/operator-registry/pkg/lib/registry"
)

func newRegistryAddCmd() *cobra.Command {
@@ -27,7 +27,12 @@ func newRegistryAddCmd() *cobra.Command {
rootCmd.Flags().StringP("database", "d", "bundles.db", "relative path to database file")
rootCmd.Flags().StringSliceP("bundle-images", "b", []string{}, "comma separated list of links to bundle image")
rootCmd.Flags().Bool("permissive", false, "allow registry load errors")
rootCmd.Flags().StringP("container-tool", "c", "podman", "tool to interact with container images (save, build, etc.). One of: [docker, podman]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should consider making this a hidden flag in case anyone was relying on it existing (instead of throwing an error about not knowing the flag).

If we're confident no one is relying on it yet, we can remove.

Copy link
Member Author

@njhale njhale Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benluddy and I were discussing this in an earlier review. Within Red Hat, I know there's some unreleased automation making use of the default value (not specifying --container-tool). However, I do think it's safer to use a hidden flag. Can we meet in the middle with a hidden flag that logs a warning when specified and continues with the daemonless implementation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it a deprecated flag, which is both hidden and gives a warning when used.

rootCmd.Flags().Bool("skip-tls", false, "skip TLS certificate verification for container image registries while pulling bundles")

rootCmd.Flags().StringP("container-tool", "c", "", "")
if err := rootCmd.Flags().MarkDeprecated("container-tool", "ignored in favor of standalone image manipulation"); err != nil {
logrus.Panic(err.Error())
}

return rootCmd
}
@@ -37,7 +42,6 @@ func addFunc(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}

fromFilename, err := cmd.Flags().GetString("database")
if err != nil {
return err
@@ -46,8 +50,7 @@ func addFunc(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}

containerTool, err := cmd.Flags().GetString("container-tool")
skipTLS, err := cmd.Flags().GetBool("skip-tls")
if err != nil {
return err
}
@@ -56,7 +59,7 @@ func addFunc(cmd *cobra.Command, args []string) error {
Bundles: bundleImages,
InputDatabase: fromFilename,
Permissive: permissive,
ContainerTool: containerTool,
SkipTLS: skipTLS,
}

logger := logrus.WithFields(logrus.Fields{"bundles": bundleImages})
23 changes: 21 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -3,27 +3,44 @@ module github.com/operator-framework/operator-registry
go 1.13

require (
github.com/Microsoft/hcsshim v0.8.7 // indirect
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6
github.com/blang/semver v3.5.0+incompatible
github.com/containerd/containerd v1.3.2
github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
github.com/docker/docker-credential-helpers v0.6.3 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/ghodss/yaml v1.0.0
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang-migrate/migrate/v4 v4.6.2
github.com/golang/mock v1.3.1
github.com/golang/protobuf v1.3.2
github.com/google/go-cmp v0.4.0 // indirect
github.com/grpc-ecosystem/grpc-health-probe v0.2.1-0.20181220223928-2bf0a5b182db
github.com/mattn/go-sqlite3 v1.10.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.10.1
github.com/onsi/gomega v1.7.0
github.com/opencontainers/image-spec v1.0.1
github.com/opencontainers/runc v0.1.1 // indirect
github.com/operator-framework/api v0.1.1
github.com/otiai10/copy v1.0.2
github.com/pkg/errors v0.8.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d // indirect
golang.org/x/mod v0.2.0
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/grpc v1.24.0
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
google.golang.org/grpc v1.23.1
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.17.3
k8s.io/apiextensions-apiserver v0.17.3
@@ -32,3 +49,5 @@ require (
k8s.io/klog v1.0.0
k8s.io/kubectl v0.17.3
)

replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way to resolve this? we just got these all cleaned up...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just not run an in-process docker registry, which I expect would make the tests more complex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a test dependency, which I think means it's not required by dependents to use the operator-registry module.

141 changes: 141 additions & 0 deletions go.sum

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions pkg/image/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package image

import (
"context"
)

// Registry knows how to Pull and Unpack Operator Bundle images to the filesystem.
// Note: In the future, Registry will know how to Build and Push Operator Bundle images as well.
type Registry interface {
// Pull fetches and stores an image by reference.
Pull(ctx context.Context, ref string) error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I like is using custom types for specificity such a type ImageReference string and then in the interface it would take in a ref ImageReference type as the arg. I'm not sure how appropriate it is here but something worth noting as it makes the code more readable IMO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't insist on it, but I also appreciate the extra compile-time safety of derived types for this kind of thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay -- I'll change this


// Push uploads an image to the remote registry of its reference.
// If the referenced image does not exist in the registry, an error is returned.
// Push(ctx context.Context, ref string) error

// Unpack writes the unpackaged content of an image to a directory.
// If the referenced image does not exist in the registry, an error is returned.
Unpack(ctx context.Context, ref, dir string) error

// Pack creates and stores an image based on the given reference and returns a reference to the new image.
// If the referenced image does not exist in the registry, a new image is created from scratch.
// If it exists, it's used as the base image.
// Pack(ctx context.Context, ref string, from io.Reader) (next string, err error)
}
141 changes: 141 additions & 0 deletions pkg/image/unprivileged/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package unprivileged

import (
"os"
"path/filepath"
"sync"

contentlocal "github.com/containerd/containerd/content/local"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/platforms"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)

type RegistryConfig struct {
Log *logrus.Entry
ResolverConfigDir string
DBPath string
CacheDir string
PreserveCache bool
SkipTLS bool
}

func (r *RegistryConfig) apply(options []RegistryOption) {
for _, option := range options {
option(r)
}
}

func (r *RegistryConfig) complete() error {
if err := os.Mkdir(r.CacheDir, os.ModePerm); err != nil && !os.IsExist(err) {
return err
}

if r.DBPath == "" {
r.DBPath = filepath.Join(r.CacheDir, "metadata.db")
}

return nil
}

func defaultConfig() *RegistryConfig {
config := &RegistryConfig{
Log: logrus.NewEntry(logrus.New()),
ResolverConfigDir: "",
CacheDir: "cache",
}

return config
}

func NewRegistry(options ...RegistryOption) (*Registry, error) {
config := defaultConfig()
config.apply(options)
if err := config.complete(); err != nil {
return nil, err
}

cs, err := contentlocal.NewStore(config.CacheDir)
if err != nil {
return nil, err
}

bdb, err := bolt.Open(config.DBPath, 0644, nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least we got bolt in here somehow!

We don't expect to have endianness concerns with this because this is a generated always at runtime and never persisted, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it's always generated at runtime unless you really want to use an existing file.

if err != nil {
return nil, err
}

var (
once sync.Once
closed bool
)
close := func() error {
defer func() {
once.Do(func() {
closed = true
})
}()
if closed {
// Already closed, no-op
return nil
}

if err := bdb.Close(); err != nil {
return err
}

if config.PreserveCache {
return nil
}
return os.RemoveAll(config.CacheDir)
}

resolver, err := NewResolver(config.ResolverConfigDir, config.SkipTLS)
if err != nil {
return nil, err
}

r := &Registry{
Store: newStore(metadata.NewDB(bdb, cs, nil)),

log: config.Log,
resolver: resolver,
platform: platforms.Only(platforms.DefaultSpec()),

close: close,
}
return r, nil
}

type RegistryOption func(config *RegistryConfig)

func WithLog(log *logrus.Entry) RegistryOption {
return func(config *RegistryConfig) {
config.Log = log
}
}

func WithResolverConfigDir(path string) RegistryOption {
return func(config *RegistryConfig) {
config.ResolverConfigDir = path
}
}

func WithCacheDir(dir string) RegistryOption {
return func(config *RegistryConfig) {
config.CacheDir = dir
}
}

func PreserveCache(preserve bool) RegistryOption {
return func(config *RegistryConfig) {
config.PreserveCache = preserve
}
}

func SkipTLS(skip bool) RegistryOption {
return func(config *RegistryConfig) {
config.SkipTLS = skip
}
}
142 changes: 142 additions & 0 deletions pkg/image/unprivileged/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package unprivileged

import (
"archive/tar"
"context"
"io"
"os"

"github.com/containerd/containerd/archive"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
)

type Registry struct {
Store

log *logrus.Entry
resolver remotes.Resolver
platform platforms.MatchComparer

close func() error
}

// Pull fetches and stores an image by reference.
func (r *Registry) Pull(ctx context.Context, ref string) error {
// Set the default namespace if unset
ctx = ensureNamespace(ctx)

name, root, err := r.resolver.Resolve(ctx, ref)
if err != nil {
return err
}
r.log.Infof("resolved name: %s", name)

fetcher, err := r.resolver.Fetcher(ctx, name)
if err != nil {
return err
}

if err := r.fetch(ctx, fetcher, root); err != nil {
return err
}

img := images.Image{
Name: ref,
Target: root,
}
if _, err = r.Images().Create(ctx, img); err != nil {
if errdefs.IsAlreadyExists(err) {
_, err = r.Images().Update(ctx, img)
}
}

return err
}

// Unpack writes the unpackaged content of an image to a directory.
// If the referenced image does not exist in the registry, an error is returned.
func (r *Registry) Unpack(ctx context.Context, ref, dir string) error {
// Set the default namespace if unset
ctx = ensureNamespace(ctx)

img, err := r.Images().Get(ctx, ref)
if err != nil {
return err
}

manifest, err := images.Manifest(ctx, r.Content(), img.Target, r.platform)
if err != nil {
return err
}

if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}

for _, layer := range manifest.Layers {
r.log.Infof("unpacking layer: %v", layer)
if err := r.unpackLayer(ctx, layer, dir); err != nil {
return err
}
}

return nil
}

func (r *Registry) Close() error {
return r.close()
}

func (r *Registry) fetch(ctx context.Context, fetcher remotes.Fetcher, root ocispec.Descriptor) error {
visitor := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
r.log.WithField("digest", desc.Digest).Info("fetched")
r.log.Debug(desc)
return nil, nil
})

handler := images.Handlers(
visitor,
remotes.FetchHandler(r.Content(), fetcher),
images.ChildrenHandler(r.Content()),
)

return images.Dispatch(ctx, handler, nil, root)
}

func (r *Registry) unpackLayer(ctx context.Context, layer ocispec.Descriptor, dir string) error {
ra, err := r.Content().ReaderAt(ctx, layer)
if err != nil {
return err
}
defer ra.Close()

// TODO(njhale): Chunk layer reading
decompressed, err := compression.DecompressStream(io.NewSectionReader(ra, 0, ra.Size()))
if err != nil {
return err
}
_, err = archive.Apply(ctx, dir, decompressed, archive.WithFilter(adjustPerms))

return err
}

func ensureNamespace(ctx context.Context) context.Context {
if _, namespaced := namespaces.Namespace(ctx); !namespaced {
return namespaces.WithNamespace(ctx, namespaces.Default)
}
return ctx
}

func adjustPerms(h *tar.Header) (bool, error) {
h.Uid = os.Getuid()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.Getuid and os.Getgid return -1 on Windows, are there any portability implications?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. I think there could be and I'm fairly certain opm doesn't work on windows as it stands. There are already multiple failures in the travis windows tests (our config just isn't picking them up).

h.Gid = os.Getgid()

return true, nil
}
113 changes: 113 additions & 0 deletions pkg/image/unprivileged/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package unprivileged

import (
"context"
"fmt"
"math/rand"
"os"
"testing"
"time"

"github.com/docker/distribution/configuration"
"github.com/docker/distribution/registry"
_ "github.com/docker/distribution/registry/storage/driver/filesystem"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/phayes/freeport"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"golang.org/x/mod/sumdb/dirhash"
)

func setupRegistry(t *testing.T, ctx context.Context, rootDir string) string {
dockerPort, err := freeport.GetFreePort()
require.NoError(t, err)

config := &configuration.Configuration{}
config.HTTP.Addr = fmt.Sprintf(":%d", dockerPort)
if rootDir != "" {
config.Storage = map[string]configuration.Parameters{"filesystem": map[string]interface{}{
"rootdirectory": rootDir,
}}
} else {
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
}
config.HTTP.DrainTimeout = time.Duration(2) * time.Second

dockerRegistry, err := registry.NewRegistry(context.Background(), config)
require.NoError(t, err)

go func() {
require.NoError(t, dockerRegistry.ListenAndServe())
}()

// Return the registry host string
return fmt.Sprintf("127.0.0.1:%d", dockerPort)
}

func dirChecksum(t *testing.T, dir string) string {
sum, err := dirhash.HashDir(dir, "", dirhash.DefaultHash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's handy!

require.NoError(t, err)
return sum
}

func TestPullAndUnpack(t *testing.T) {
type args struct {
dockerRootDir string
img string
}
type expected struct {
checksum string
}
tests := []struct {
description string
args args
expected expected
}{
{
description: "ByTag",
args: args{
dockerRootDir: "testdata/golden",
img: "/olmtest/kiali:1.4.2",
},
expected: expected{
checksum: dirChecksum(t, "testdata/golden/bundles/kiali"),
},
},
{
description: "ByDigest",
args: args{
dockerRootDir: "testdata/golden",
img: "/olmtest/kiali@sha256:a1bec450c104ceddbb25b252275eb59f1f1e6ca68e0ced76462042f72f7057d8",
},
expected: expected{
checksum: dirChecksum(t, "testdata/golden/bundles/kiali"),
},
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
ctx, close := context.WithCancel(context.Background())
defer close()

host := setupRegistry(t, ctx, tt.args.dockerRootDir)
r, err := NewRegistry(
WithLog(logrus.New().WithField("test", t.Name())),
WithCacheDir(fmt.Sprintf("cache-%x", rand.Int())),
)
require.NoError(t, err)

ref := host + tt.args.img
require.NoError(t, r.Pull(ctx, ref))

// Copy golden manifests to a temp dir
dir := "kiali-unpacked"
require.NoError(t, r.Unpack(ctx, ref, dir))

checksum := dirChecksum(t, dir)
require.Equal(t, tt.expected.checksum, checksum)

require.NoError(t, r.Close())
require.NoError(t, os.RemoveAll(dir))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to defer this to increase the odds it happens on test failures, unless this was an intentional decision to help debug failures?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional, but I can put it in a defer.

})
}
}
93 changes: 93 additions & 0 deletions pkg/image/unprivileged/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package unprivileged

import (
"crypto/tls"
"net"
"net/http"
"time"

"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/registry"
)

func NewResolver(configDir string, insecure bool) (remotes.Resolver, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 5 * time.Second,
}
if insecure {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: insecure,
}
}
client := http.DefaultClient
client.Transport = transport

cfg, err := loadConfig(configDir)
if err != nil {
return nil, err
}

opts := docker.ResolverOptions{
Client: client,
Credentials: credential(cfg),
}

return docker.NewResolver(opts), nil
}

func credential(cfg *configfile.ConfigFile) func(string) (string, string, error) {
return func(hostname string) (string, string, error) {
hostname = resolveHostname(hostname)
auth, err := cfg.GetAuthConfig(hostname)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
if auth.Username == "" && auth.Password == "" {
return "", "", nil
}

return auth.Username, auth.Password, nil
}
}

func loadConfig(dir string) (*configfile.ConfigFile, error) {
if dir == "" {
dir = config.Dir()
}

cfg, err := config.Load(dir)
if err != nil {
return nil, err
}

if !cfg.ContainsAuth() {
cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
}

return cfg, nil
}

// resolveHostname resolves Docker specific hostnames
func resolveHostname(hostname string) string {
switch hostname {
case registry.IndexHostname, registry.IndexName, registry.DefaultV2Registry.Host:
return registry.IndexServer
}
return hostname
}
32 changes: 32 additions & 0 deletions pkg/image/unprivileged/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package unprivileged

import (
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
)

type Store interface {
Images() images.Store
Content() content.Store
}

type store struct {
cs content.Store
is images.Store
}

func newStore(db *metadata.DB) *store {
return &store{
cs: db.ContentStore(),
is: metadata.NewImageStore(db),
}
}

func (s *store) Content() content.Store {
return s.cs
}

func (s *store) Images() images.Store {
return s.is
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
annotations:
operators.operatorframework.io.bundle.channel.default.v1: stable
operators.operatorframework.io.bundle.channels.v1: stable,alpha
operators.operatorframework.io.bundle.manifests.v1: manifests/
operators.operatorframework.io.bundle.mediatype.v1: registry+v1
operators.operatorframework.io.bundle.metadata.v1: metadata/
operators.operatorframework.io.bundle.package.v1: kiali
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 3363,
"digest": "sha256:d9c98a446dd77b5e7cb1394748c2279d83668a519b2badda1259cd64cb3beb3f"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 5916,
"digest": "sha256:1c019331644e2e826b6b8f11328b31a1c2f4aa2ae6ee225f0ab19a14af086d2b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 296,
"digest": "sha256:8b19d5ea2c56e663fb886d9ca273dd1f8e20b107c9b6817dedd35aac90e16494"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":null,"Image":"sha256:f5598e8676f5fbb7c85a2f2626a6cde6424e0f44698d02a00889ebd923db79be","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{"operators.operatorframework.io.bundle.channel.default.v1":"stable","operators.operatorframework.io.bundle.channels.v1":"stable,alpha","operators.operatorframework.io.bundle.manifests.v1":"manifests/","operators.operatorframework.io.bundle.mediatype.v1":"registry+v1","operators.operatorframework.io.bundle.metadata.v1":"metadata/","operators.operatorframework.io.bundle.package.v1":"kiali"}},"container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) COPY file:a1e0f7c6072f05896c09dfcb6121e50bd7b917537071a52e025a36f6835114e3 in /metadata/annotations.yaml "],"Image":"sha256:f5598e8676f5fbb7c85a2f2626a6cde6424e0f44698d02a00889ebd923db79be","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{"operators.operatorframework.io.bundle.channel.default.v1":"stable","operators.operatorframework.io.bundle.channels.v1":"stable,alpha","operators.operatorframework.io.bundle.manifests.v1":"manifests/","operators.operatorframework.io.bundle.mediatype.v1":"registry+v1","operators.operatorframework.io.bundle.metadata.v1":"metadata/","operators.operatorframework.io.bundle.package.v1":"kiali"}},"created":"2020-02-05T16:35:22.8067069Z","docker_version":"19.03.5","history":[{"created":"2020-02-04T20:56:57.4520019Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1","empty_layer":true},{"created":"2020-02-04T20:56:57.5851175Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/","empty_layer":true},{"created":"2020-02-04T20:56:57.7324625Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/","empty_layer":true},{"created":"2020-02-04T20:56:57.8842075Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.package.v1=kiali","empty_layer":true},{"created":"2020-02-04T20:56:58.0092179Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.channels.v1=stable,alpha","empty_layer":true},{"created":"2020-02-04T20:56:58.1288671Z","created_by":"/bin/sh -c #(nop) LABEL operators.operatorframework.io.bundle.channel.default.v1=stable","empty_layer":true},{"created":"2020-02-05T16:35:22.6653939Z","created_by":"/bin/sh -c #(nop) COPY multi:d75d8f6730f9aa238ebf37309a5edbe313f740c7926cfe1f927b8296b6640f15 in /manifests/ "},{"created":"2020-02-05T16:35:22.8067069Z","created_by":"/bin/sh -c #(nop) COPY file:a1e0f7c6072f05896c09dfcb6121e50bd7b917537071a52e025a36f6835114e3 in /metadata/annotations.yaml "}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:4aab058a370e65782555d3047f6bf04e0f80da44423831643609e2998679ef40","sha256:39c8e2383b0115c11801e09a64d5d8f1d656ceeb292683fbb1707fc319058666"]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:1c019331644e2e826b6b8f11328b31a1c2f4aa2ae6ee225f0ab19a14af086d2b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:8b19d5ea2c56e663fb886d9ca273dd1f8e20b107c9b6817dedd35aac90e16494
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:d9c98a446dd77b5e7cb1394748c2279d83668a519b2badda1259cd64cb3beb3f
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:a1bec450c104ceddbb25b252275eb59f1f1e6ca68e0ced76462042f72f7057d8
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:a1bec450c104ceddbb25b252275eb59f1f1e6ca68e0ced76462042f72f7057d8
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:a1bec450c104ceddbb25b252275eb59f1f1e6ca68e0ced76462042f72f7057d8
1 change: 0 additions & 1 deletion pkg/lib/indexer/indexer.go
Original file line number Diff line number Diff line change
@@ -89,7 +89,6 @@ func (i ImageIndexer) AddToIndex(request AddToIndexRequest) error {
Bundles: request.Bundles,
InputDatabase: databaseFile,
Permissive: request.Permissive,
ContainerTool: i.ContainerTool,
}

// Add the bundles to the registry
6 changes: 3 additions & 3 deletions pkg/lib/indexer/indexer_test.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ func TestGetBundlesToExport(t *testing.T) {
"quay.io/olmtest/example-bundle:etcdoperator.v0.6.1"}
sort.Strings(expected)

db, err := sql.Open("sqlite3", "./test/bundles.db")
db, err := sql.Open("sqlite3", "./testdata/bundles.db")
if err != nil {
t.Fatalf("opening db: %s", err)
}
@@ -42,7 +42,7 @@ func TestGetBundlesToExport(t *testing.T) {
}

func TestGeneratePackageYaml(t *testing.T) {
db, err := sql.Open("sqlite3", "./test/bundles.db")
db, err := sql.Open("sqlite3", "./testdata/bundles.db")
if err != nil {
t.Fatalf("opening db: %s", err)
}
@@ -59,7 +59,7 @@ func TestGeneratePackageYaml(t *testing.T) {
}

var expected pregistry.PackageManifest
expectedBytes, _ := ioutil.ReadFile("./test/package.yaml")
expectedBytes, _ := ioutil.ReadFile("./testdata/package.yaml")
err = yaml.Unmarshal(expectedBytes, &expected)
if err != nil {
t.Fatalf("unmarshaling: %s", err)
File renamed without changes.
File renamed without changes.
45 changes: 41 additions & 4 deletions pkg/lib/registry/registry.go
Original file line number Diff line number Diff line change
@@ -4,10 +4,15 @@ import (
"context"
"database/sql"
"fmt"
"io/ioutil"
"os"

"github.com/sirupsen/logrus"
utilerrors "k8s.io/apimachinery/pkg/util/errors"

"github.com/operator-framework/operator-registry/pkg/image"
"github.com/operator-framework/operator-registry/pkg/image/unprivileged"
"github.com/operator-framework/operator-registry/pkg/registry"
"github.com/operator-framework/operator-registry/pkg/sqlite"
)

@@ -17,8 +22,8 @@ type RegistryUpdater struct {

type AddToRegistryRequest struct {
Permissive bool
SkipTLS bool
InputDatabase string
ContainerTool string
Bundles []string
}

@@ -39,9 +44,21 @@ func (r RegistryUpdater) AddToRegistry(request AddToRegistryRequest) error {
return err
}

for _, bundleImage := range request.Bundles {
loader := sqlite.NewSQLLoaderForImage(dbLoader, bundleImage, request.ContainerTool)
if err := loader.Populate(); err != nil {
reg, err := unprivileged.NewRegistry(
unprivileged.SkipTLS(request.SkipTLS),
)
if err != nil {
return err
}
defer func() {
if err := reg.Close(); err != nil {
r.Logger.WithError(err).Warn("error closing local image registry")
}
}()

// TODO(njhale): Parallelize this once bundle add is commutative
for _, ref := range request.Bundles {
if err := populate(context.TODO(), dbLoader, reg, ref); err != nil {
err = fmt.Errorf("error loading bundle from image: %s", err)
if !request.Permissive {
r.Logger.WithError(err).Error("permissive mode disabled")
@@ -55,6 +72,26 @@ func (r RegistryUpdater) AddToRegistry(request AddToRegistryRequest) error {
return utilerrors.NewAggregate(errs) // nil if no errors
}

func populate(ctx context.Context, loader registry.Load, reg image.Registry, ref string) error {
workingDir, err := ioutil.TempDir("./", "bundle_tmp")
if err != nil {
return err
}
defer os.RemoveAll(workingDir)

if err = reg.Pull(ctx, ref); err != nil {
return err
}

if err = reg.Unpack(ctx, ref, workingDir); err != nil {
return err
}

populator := registry.NewDirectoryPopulator(loader, workingDir, ref)

return populator.Populate()
}

type DeleteFromRegistryRequest struct {
Permissive bool
InputDatabase string
3 changes: 3 additions & 0 deletions pkg/registry/csv.go
Original file line number Diff line number Diff line change
@@ -17,6 +17,9 @@ import (
)

const (
// Name of the CSV's kind
clusterServiceVersionKind = "ClusterServiceVersion"

// Name of the section under which the list of owned and required list of
// CRD(s) is specified inside an operator manifest.
customResourceDefinitions = "customresourcedefinitions"
5 changes: 5 additions & 0 deletions pkg/registry/interface.go
Original file line number Diff line number Diff line change
@@ -56,3 +56,8 @@ type Query interface {
type GraphLoader interface {
Generate() (*Package, error)
}

// RegistryPopulator populates a registry.
type RegistryPopulator interface {
Populate() error
}
256 changes: 256 additions & 0 deletions pkg/registry/populator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package registry

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/yaml"
)

// DirectoryPopulator loads an unpacked operator bundle from a directory into the database.
type DirectoryPopulator struct {
loader Load
from string
ref string
}

func NewDirectoryPopulator(loader Load, from, ref string) *DirectoryPopulator {
return &DirectoryPopulator{
loader: loader,
from: from,
ref: ref,
}
}

func (i *DirectoryPopulator) Populate() error {
path := i.from
manifests := filepath.Join(path, "manifests")
metadata := filepath.Join(path, "metadata")
// Get annotations file
log := logrus.WithFields(logrus.Fields{"dir": i.from, "file": metadata, "load": "annotations"})
files, err := ioutil.ReadDir(metadata)
if err != nil {
return fmt.Errorf("unable to read directory %s: %s", metadata, err)
}

// Look for the metadata and manifests sub-directories to find the annotations.yaml file that will inform how the
// manifests of the bundle should be loaded into the database.
annotationsFile := &AnnotationsFile{}
for _, f := range files {
fileReader, err := os.Open(filepath.Join(metadata, f.Name()))
if err != nil {
return fmt.Errorf("unable to read file %s: %s", f.Name(), err)
}
decoder := yaml.NewYAMLOrJSONDecoder(fileReader, 30)
err = decoder.Decode(&annotationsFile)
if err != nil || *annotationsFile == (AnnotationsFile{}) {
continue
} else {
log.Info("found annotations file searching for csv")
}
}

if *annotationsFile == (AnnotationsFile{}) {
return fmt.Errorf("Could not find annotations.yaml file")
}

err = i.loadManifests(manifests, annotationsFile)
if err != nil {
return err
}

return nil
}

func (i *DirectoryPopulator) loadManifests(manifests string, annotationsFile *AnnotationsFile) error {
log := logrus.WithFields(logrus.Fields{"dir": i.from, "file": manifests, "load": "bundle"})

csv, err := i.findCSV(manifests)
if err != nil {
return err
}

if csv.Object == nil {
return fmt.Errorf("csv is empty: %s", err)
}

log.Info("found csv, loading bundle")

// TODO: Check channels against what's in the database vs in the bundle csv

bundle, err := loadBundle(csv.GetName(), manifests)
if err != nil {
return fmt.Errorf("error loading objs in directory: %s", err)
}

if bundle == nil || bundle.Size() == 0 {
return fmt.Errorf("no bundle objects found")
}

// set the bundleimage on the bundle
bundle.BundleImage = i.ref

if err := bundle.AllProvidedAPIsInBundle(); err != nil {
return fmt.Errorf("error checking provided apis in bundle %s: %s", bundle.Name, err)
}

bcsv, err := bundle.ClusterServiceVersion()
if err != nil {
return fmt.Errorf("error getting csv from bundle %s: %s", bundle.Name, err)
}

packageManifest, err := translateAnnotationsIntoPackage(annotationsFile, bcsv)
if err != nil {
return fmt.Errorf("Could not translate annotations file into packageManifest %s", err)
}

if err := i.loadOperatorBundle(packageManifest, bundle); err != nil {
return fmt.Errorf("Error adding package %s", err)
}

// Finally let's delete all the old bundles
if err = i.loader.ClearNonDefaultBundles(packageManifest.PackageName); err != nil {
return fmt.Errorf("Error deleting previous bundles: %s", err)
}

return nil
}

// loadBundle takes the directory that a CSV is in and assumes the rest of the objects in that directory
// are part of the bundle.
func loadBundle(csvName string, dir string) (*Bundle, error) {
log := logrus.WithFields(logrus.Fields{"dir": dir, "load": "bundle"})
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}

var errs []error
bundle := &Bundle{}
for _, f := range files {
log = log.WithField("file", f.Name())
if f.IsDir() {
log.Info("skipping directory")
continue
}

if strings.HasPrefix(f.Name(), ".") {
log.Info("skipping hidden file")
continue
}

log.Info("loading bundle file")
path := filepath.Join(dir, f.Name())
fileReader, err := os.Open(path)
if err != nil {
errs = append(errs, fmt.Errorf("unable to load file %s: %s", path, err))
continue
}

decoder := yaml.NewYAMLOrJSONDecoder(fileReader, 30)
obj := &unstructured.Unstructured{}
if err = decoder.Decode(obj); err != nil {
logrus.WithError(err).Debugf("could not decode file contents for %s", path)
continue
}

// Don't include other CSVs in the bundle
if obj.GetKind() == "ClusterServiceVersion" && obj.GetName() != csvName {
continue
}

if obj.Object != nil {
bundle.Add(obj)
}
}

return bundle, utilerrors.NewAggregate(errs)
}

// findCSV looks through the bundle directory to find a csv
func (i *DirectoryPopulator) findCSV(manifests string) (*unstructured.Unstructured, error) {
log := logrus.WithFields(logrus.Fields{"dir": i.from, "find": "csv"})

files, err := ioutil.ReadDir(manifests)
if err != nil {
return nil, fmt.Errorf("unable to read directory %s: %s", manifests, err)
}

var errs []error
for _, f := range files {
log = log.WithField("file", f.Name())
if f.IsDir() {
log.Info("skipping directory")
continue
}

if strings.HasPrefix(f.Name(), ".") {
log.Info("skipping hidden file")
continue
}

path := filepath.Join(manifests, f.Name())
fileReader, err := os.Open(path)
if err != nil {
errs = append(errs, fmt.Errorf("unable to read file %s: %s", path, err))
continue
}

dec := yaml.NewYAMLOrJSONDecoder(fileReader, 30)
unst := &unstructured.Unstructured{}
if err := dec.Decode(unst); err != nil {
continue
}

if unst.GetKind() != clusterServiceVersionKind {
continue
}

return unst, nil

}

errs = append(errs, fmt.Errorf("no csv found in bundle"))
return nil, utilerrors.NewAggregate(errs)
}

// loadOperatorBundle adds the package information to the loader's store
func (i *DirectoryPopulator) loadOperatorBundle(manifest PackageManifest, bundle *Bundle) error {
if manifest.PackageName == "" {
return nil
}

if err := i.loader.AddBundlePackageChannels(manifest, bundle); err != nil {
return fmt.Errorf("error loading bundle into db: %s", err)
}

return nil
}

// translateAnnotationsIntoPackage attempts to translate the channels.yaml file at the given path into a package.yaml
func translateAnnotationsIntoPackage(annotations *AnnotationsFile, csv *ClusterServiceVersion) (PackageManifest, error) {
manifest := PackageManifest{}

channels := []PackageChannel{}
for _, ch := range annotations.GetChannels() {
channels = append(channels,
PackageChannel{
Name: ch,
CurrentCSVName: csv.GetName(),
})
}

manifest = PackageManifest{
PackageName: annotations.GetName(),
DefaultChannelName: annotations.GetDefaultChannelName(),
Channels: channels,
}

return manifest, nil
}
55 changes: 27 additions & 28 deletions test/e2e/bundle_image_test.go
Original file line number Diff line number Diff line change
@@ -6,11 +6,10 @@ import (
"os/exec"
"time"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"github.com/stretchr/testify/assert"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
@@ -25,11 +24,11 @@ import (
var builderCmd string

const (
imageDirectory = "image-bundle/"
imageDirectory = "testdata/image-bundle/"
)

func Logf(format string, a ...interface{}) {
fmt.Fprintf(ginkgo.GinkgoWriter, "INFO: "+format+"\n", a...)
fmt.Fprintf(GinkgoWriter, "INFO: "+format+"\n", a...)
}

// checks command that it exists in $PATH, isn't a directory, and has executable permissions set
@@ -59,7 +58,7 @@ func checkCommand(filename string) string {
}

func init() {
logrus.SetOutput(ginkgo.GinkgoWriter)
logrus.SetOutput(GinkgoWriter)

if builderCmd = checkCommand("docker"); builderCmd != "" {
return
@@ -680,10 +679,10 @@ spec:
func buildContainer(tag, dockerfilePath, context string) {
cmd := exec.Command(builderCmd, "build", "-t", tag, "-f", dockerfilePath, context)
err := cmd.Run()
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())
}

var _ = ginkgo.Describe("Launch bundle", func() {
var _ = Describe("Launch bundle", func() {
namespace := "default"
initImage := "init-operator-manifest:test"
bundleImage := "bundle-image:test"
@@ -704,12 +703,12 @@ var _ = ginkgo.Describe("Launch bundle", func() {
Data: getConfigMapDataSection(),
}

ginkgo.Context("Deploy bundle job", func() {
ginkgo.It("should populate specified configmap", func() {
Context("Deploy bundle job", func() {
It("should populate specified configmap", func() {
// these permissions are only necessary for the e2e (and not OLM using the feature)
ginkgo.By("configuring configmap service account")
By("configuring configmap service account")
kubeclient, err := client.NewKubeClient("", logrus.StandardLogger())
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

_, err = kubeclient.RbacV1().Roles(namespace).Create(&rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
@@ -724,7 +723,7 @@ var _ = ginkgo.Describe("Launch bundle", func() {
},
},
})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

_, err = kubeclient.RbacV1().RoleBindings(namespace).Create(&rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
@@ -745,19 +744,19 @@ var _ = ginkgo.Describe("Launch bundle", func() {
Name: "olm-dev-configmap-access",
},
})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("building required images")
buildContainer(initImage, imageDirectory+"Dockerfile.serve", "../../bin")
buildContainer(bundleImage, imageDirectory+"Dockerfile.bundle", imageDirectory)
By("building required images")
buildContainer(initImage, imageDirectory+"serve.Dockerfile", "../../bin")
buildContainer(bundleImage, imageDirectory+"bundle.Dockerfile", imageDirectory)

ginkgo.By("creating a batch job")
By("creating a batch job")
bundleDataConfigMap, job, err := configmap.LaunchBundleImage(kubeclient, bundleImage, initImage, namespace)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

// wait for job to complete
jobWatcher, err := kubeclient.BatchV1().Jobs(namespace).Watch(metav1.ListOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

done := make(chan struct{})
quit := make(chan struct{})
@@ -794,22 +793,22 @@ var _ = ginkgo.Describe("Launch bundle", func() {
Logf("Job complete")

bundleDataConfigMap, err = kubeclient.CoreV1().ConfigMaps(namespace).Get(bundleDataConfigMap.GetName(), metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
assert.EqualValues(ginkgo.GinkgoT(), correctConfigMap.Annotations, bundleDataConfigMap.Annotations)
assert.EqualValues(ginkgo.GinkgoT(), correctConfigMap.Data, bundleDataConfigMap.Data)
Expect(err).NotTo(HaveOccurred())
assert.EqualValues(GinkgoT(), correctConfigMap.Annotations, bundleDataConfigMap.Annotations)
assert.EqualValues(GinkgoT(), correctConfigMap.Data, bundleDataConfigMap.Data)

// clean up, perhaps better handled elsewhere
err = kubeclient.CoreV1().ConfigMaps(namespace).Delete(bundleDataConfigMap.GetName(), &metav1.DeleteOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

// job deletion does not clean up underlying pods (but using kubectl will do the clean up)
pods, err := kubeclient.CoreV1().Pods(namespace).List(metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", job.GetName())})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())
err = kubeclient.CoreV1().Pods(namespace).Delete(pods.Items[0].GetName(), &metav1.DeleteOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

err = kubeclient.BatchV1().Jobs(namespace).Delete(job.GetName(), &metav1.DeleteOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())
})
})
})
10 changes: 5 additions & 5 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
@@ -3,11 +3,11 @@ package e2e_test
import (
"testing"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestE2e(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "E2e Suite")
func TestE2E(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "E2E Suite")
}
49 changes: 25 additions & 24 deletions test/e2e/opm_test.go
Original file line number Diff line number Diff line change
@@ -8,14 +8,15 @@ import (
"os/exec"
"path/filepath"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
"github.com/operator-framework/operator-registry/pkg/lib/indexer"
"github.com/operator-framework/operator-registry/pkg/sqlite"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/otiai10/copy"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/rand"

"github.com/operator-framework/operator-registry/pkg/lib/bundle"
"github.com/operator-framework/operator-registry/pkg/lib/indexer"
"github.com/operator-framework/operator-registry/pkg/sqlite"
)

var (
@@ -156,53 +157,53 @@ func initialize() error {
return loader.Populate()
}

var _ = ginkgo.Describe("opm", func() {
var _ = Describe("opm", func() {
IncludeSharedSpecs := func(containerTool string) {
ginkgo.BeforeEach(func() {
BeforeEach(func() {
dockerUsername := os.Getenv("DOCKER_USERNAME")
dockerPassword := os.Getenv("DOCKER_PASSWORD")

if dockerUsername == "" || dockerPassword == "" {
ginkgo.Skip("registry credentials are not available")
Skip("registry credentials are not available")
}

dockerlogin := exec.Command(containerTool, "login", "-u", dockerUsername, "-p", dockerPassword, "quay.io")
err := dockerlogin.Run()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Error logging into quay.io")
Expect(err).NotTo(HaveOccurred(), "Error logging into quay.io")
})

ginkgo.It("builds and manipulates bundle and index images", func() {
ginkgo.By("building bundles")
It("builds and manipulates bundle and index images", func() {
By("building bundles")
err := buildBundlesWith(containerTool)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("pushing bundles")
By("pushing bundles")
err = pushBundles(containerTool)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("building an index")
By("building an index")
err = buildIndexWith(containerTool)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("pushing an index")
By("pushing an index")
err = pushWith(containerTool, indexImage)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("exporting an index to disk")
By("exporting an index to disk")
err = exportWith(containerTool)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())

ginkgo.By("loading manifests from a directory")
By("loading manifests from a directory")
err = initialize()
gomega.Expect(err).NotTo(gomega.HaveOccurred())
Expect(err).NotTo(HaveOccurred())
})
}

ginkgo.Context("using docker", func() {
Context("using docker", func() {
IncludeSharedSpecs("docker")
})

ginkgo.Context("using podman", func() {
Context("using podman", func() {
IncludeSharedSpecs("podman")
})
})
File renamed without changes.
21 changes: 21 additions & 0 deletions test/e2e/testdata/image-bundle/manifests/kiali.crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: kialis.kiali.io
labels:
app: kiali-operator
spec:
group: kiali.io
names:
kind: Kiali
listKind: KialiList
plural: kialis
singular: kiali
scope: Namespaced
subresources:
status: {}
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: monitoringdashboards.monitoring.kiali.io
labels:
app: kiali
spec:
group: monitoring.kiali.io
names:
kind: MonitoringDashboard
listKind: MonitoringDashboardList
plural: monitoringdashboards
singular: monitoringdashboard
scope: Namespaced
version: v1alpha1
7 changes: 7 additions & 0 deletions test/e2e/testdata/image-bundle/manifests/kiali.package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
packageName: kiali
channels:
- name: alpha
currentCSV: kiali-operator.v1.4.2
- name: stable
currentCSV: kiali-operator.v1.4.2
defaultChannel: stable
File renamed without changes.