From b333571dc804468f72a5b68b17dbf91826755677 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Thu, 3 Sep 2020 12:14:07 -0400 Subject: [PATCH] internal/pkg/action/catalog_add.go: several improvements (#21) - When pulling images and image labels for an index image that is a manifest list, try all platforms rather than just the platform on which the `kubectl operator` command is running. This fixes an issue with using the `catalog add` command on macOS with manifest list-based index images - When using bundle injection use the presence of the database path label to determine if the image is an empty base index rather than hardcoding the known empty base index images. Also use the value from the database path label instead of hardcoding the database path. - Change display name and publisher labels to an alpha subdomain: * alpha.operators.operatorframework.io.index.display-name.v1 * alpha.operators.operatorframework.io.index.publisher.v1 - Adding an owner reference to the index image registry pod no longer requires an extra Update call to the API server --- go.mod | 2 + internal/pkg/action/catalog_add.go | 136 +++++++++++++++++------------ 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index 87a84e8..4068510 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/operator-framework/kubectl-operator go 1.13 require ( + github.com/containerd/containerd v1.3.2 + github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 github.com/operator-framework/api v0.3.7 github.com/operator-framework/operator-lifecycle-manager v0.0.0-20200521062108-408ca95d458f github.com/operator-framework/operator-registry v1.12.5 diff --git a/internal/pkg/action/catalog_add.go b/internal/pkg/action/catalog_add.go index 006178f..2ddd187 100644 --- a/internal/pkg/action/catalog_add.go +++ b/internal/pkg/action/catalog_add.go @@ -4,9 +4,16 @@ import ( "context" "encoding/json" "fmt" + "io" + "path" "strings" "time" + "github.com/containerd/containerd/archive/compression" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-registry/pkg/image" "github.com/operator-framework/operator-registry/pkg/image/containerdregistry" @@ -23,7 +30,13 @@ import ( "github.com/operator-framework/kubectl-operator/internal/pkg/catalog" ) -const grpcPort = "50051" +const ( + grpcPort = "50051" + dbPathLabel = "operators.operatorframework.io.index.database.v1" + alphaDisplayNameLabel = "alpha.operators.operatorframework.io.index.display-name.v1" + alphaPublisherLabel = "alpha.operators.operatorframework.io.index.publisher.v1" + defaultDatabasePath = "/database/index.db" +) type CatalogAdd struct { config *Configuration @@ -81,7 +94,7 @@ func (a *CatalogAdd) Run(ctx context.Context) (*v1alpha1.CatalogSource, error) { labels, err := a.labelsFor(ctx, a.IndexImage) if err != nil { - return nil, err + return nil, fmt.Errorf("get image labels: %v", err) } a.setDefaults(labels) @@ -96,20 +109,33 @@ func (a *CatalogAdd) Run(ctx context.Context) (*v1alpha1.CatalogSource, error) { } cs := catalog.Build(csKey, opts...) - if err := a.createCatalogSource(ctx, cs); err != nil { - return nil, err + if err := a.config.Client.Create(ctx, cs); err != nil { + return nil, fmt.Errorf("create catalogsource: %v", err) } var registryPod *corev1.Pod if len(a.InjectBundles) > 0 { - if registryPod, err = a.createRegistryPod(ctx, cs); err != nil { + dbPath, ok := labels[dbPathLabel] + if !ok { + // No database path label, so assume this is an index base image. + // Choose "semver" bundle add mode (if not explicitly set) and + // use the default database path. + if a.InjectBundleMode == "" { + a.InjectBundleMode = "semver" + } + dbPath = defaultDatabasePath + } + if a.InjectBundleMode == "" { + a.InjectBundleMode = "replaces" + } + if registryPod, err = a.createRegistryPod(ctx, cs, dbPath); err != nil { defer a.cleanup(cs) return nil, err } if err := a.updateCatalogSource(ctx, cs, registryPod); err != nil { defer a.cleanup(cs) - return nil, err + return nil, fmt.Errorf("update catalog source: %v", err) } } @@ -122,51 +148,61 @@ func (a *CatalogAdd) Run(ctx context.Context) (*v1alpha1.CatalogSource, error) { } func (a *CatalogAdd) labelsFor(ctx context.Context, indexImage string) (map[string]string, error) { - simpleRef := image.SimpleReference(indexImage) - if err := a.registry.Pull(ctx, simpleRef); err != nil { + ref := image.SimpleReference(indexImage) + if err := a.registry.Pull(ctx, ref); err != nil { return nil, fmt.Errorf("pull image: %v", err) } - labels, err := a.registry.Labels(ctx, simpleRef) + + ctx = namespaces.WithNamespace(ctx, namespaces.Default) + img, err := a.registry.Images().Get(ctx, ref.String()) if err != nil { - return nil, fmt.Errorf("get image labels: %v", err) + return nil, fmt.Errorf("get image from local registry: %v", err) } - return labels, nil + + manifest, err := images.Manifest(ctx, a.registry.Content(), img.Target, platforms.All) + if err != nil { + return nil, fmt.Errorf("resolve image manifest: %v", err) + } + + ra, err := a.registry.Content().ReaderAt(ctx, manifest.Config) + if err != nil { + return nil, fmt.Errorf("get image reader: %v", err) + } + defer ra.Close() + + decompressed, err := compression.DecompressStream(io.NewSectionReader(ra, 0, ra.Size())) + if err != nil { + return nil, fmt.Errorf("decompress image data: %v", err) + } + var imageMeta ocispec.Image + dec := json.NewDecoder(decompressed) + if err := dec.Decode(&imageMeta); err != nil { + return nil, fmt.Errorf("decode image metadata: %v", err) + } + return imageMeta.Config.Labels, nil } func (a *CatalogAdd) setDefaults(labels map[string]string) { if a.DisplayName == "" { - if v, ok := labels["operators.operatorframework.io.index.display-name"]; ok { + if v, ok := labels[alphaDisplayNameLabel]; ok { a.DisplayName = v } } if a.Publisher == "" { - if v, ok := labels["operators.operatorframework.io.index.publisher"]; ok { + if v, ok := labels[alphaPublisherLabel]; ok { a.Publisher = v } } - if a.InjectBundleMode == "" { - if strings.HasPrefix(a.IndexImage, "quay.io/operator-framework/upstream-opm-builder") { - a.InjectBundleMode = "semver" - } else { - a.InjectBundleMode = "replaces" - } - } -} - -func (a *CatalogAdd) createCatalogSource(ctx context.Context, cs *v1alpha1.CatalogSource) error { - if err := a.config.Client.Create(ctx, cs); err != nil { - return fmt.Errorf("create catalogsource: %v", err) - } - return nil } -func (a *CatalogAdd) createRegistryPod(ctx context.Context, cs *v1alpha1.CatalogSource) (*corev1.Pod, error) { +func (a *CatalogAdd) createRegistryPod(ctx context.Context, cs *v1alpha1.CatalogSource, dbPath string) (*corev1.Pod, error) { + dbDir := path.Dir(dbPath) command := []string{ "/bin/sh", "-c", - fmt.Sprintf(`mkdir -p /database && \ -/bin/opm registry add -d /database/index.db --mode=%s -b %s && \ -/bin/opm registry serve -d /database/index.db -p %s`, a.InjectBundleMode, strings.Join(a.InjectBundles, ","), grpcPort), + fmt.Sprintf(`mkdir -p %s && \ +/bin/opm registry add -d %s --mode=%s -b %s && \ +/bin/opm registry serve -d %s -p %s`, dbDir, dbPath, a.InjectBundleMode, strings.Join(a.InjectBundles, ","), dbPath, grpcPort), } pod := &corev1.Pod{ @@ -184,26 +220,15 @@ func (a *CatalogAdd) createRegistryPod(ctx context.Context, cs *v1alpha1.Catalog }, }, } + + if err := controllerutil.SetOwnerReference(cs, pod, a.config.Scheme); err != nil { + return nil, fmt.Errorf("set registry pod owner reference: %v", err) + } if err := a.config.Client.Create(ctx, pod); err != nil { return nil, fmt.Errorf("create registry pod: %v", err) } podKey := objectKeyForObject(pod) - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if err := a.config.Client.Get(ctx, podKey, pod); err != nil { - return fmt.Errorf("get registry pod: %v", err) - } - if err := controllerutil.SetOwnerReference(cs, pod, a.config.Scheme); err != nil { - return fmt.Errorf("set registry pod owner reference: %v", err) - } - if err := a.config.Client.Update(ctx, pod); err != nil { - return fmt.Errorf("update registry pod owner reference: %v", err) - } - return nil - }); err != nil { - return nil, err - } - if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) { if err := a.config.Client.Get(ctx, podKey, pod); err != nil { return false, err @@ -219,26 +244,25 @@ func (a *CatalogAdd) createRegistryPod(ctx context.Context, cs *v1alpha1.Catalog } func (a *CatalogAdd) updateCatalogSource(ctx context.Context, cs *v1alpha1.CatalogSource, pod *corev1.Pod) error { - cs.Spec.Address = fmt.Sprintf("%s:%s", pod.Status.PodIP, grpcPort) - injectedBundlesJSON, err := json.Marshal(a.InjectBundles) if err != nil { return fmt.Errorf("json marshal injected bundles: %v", err) } - cs.ObjectMeta.Annotations = map[string]string{ - "operators.operatorframework.io/index-image": a.IndexImage, - "operators.operatorframework.io/inject-bundle-mode": a.InjectBundleMode, - "operators.operatorframework.io/injected-bundles": string(injectedBundlesJSON), - } + csKey := objectKeyForObject(cs) if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { if err := a.config.Client.Get(ctx, csKey, cs); err != nil { return fmt.Errorf("get catalog source: %v", err) } - if err := a.config.Client.Update(ctx, cs); err != nil { - return fmt.Errorf("update catalog source: %v", err) + + cs.Spec.Address = fmt.Sprintf("%s:%s", pod.Status.PodIP, grpcPort) + cs.ObjectMeta.Annotations = map[string]string{ + "operators.operatorframework.io/index-image": a.IndexImage, + "operators.operatorframework.io/inject-bundle-mode": a.InjectBundleMode, + "operators.operatorframework.io/injected-bundles": string(injectedBundlesJSON), } - return nil + + return a.config.Client.Update(ctx, cs) }); err != nil { return err }