Skip to content

Commit

Permalink
Fix related image environment variable discovery
Browse files Browse the repository at this point in the history
The related image discovery feature currently only reads environment
variables from the manager container. This was causing an error when the
deployment labels or container name that was expected were not present.
This fixes that by collecting related images from all containers across
all deployments.

This change also enables users to use related images in other containers
since related images from everywhere will be considered.

Signed-off-by: Ryan King <[email protected]>
  • Loading branch information
ryantking committed Apr 13, 2022
1 parent 5d541d0 commit 58e91a5
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 64 deletions.
11 changes: 11 additions & 0 deletions changelog/fragments/fix-make-bundle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
entries:
- description: >
The related image discovery feature currently only reads environment
variables from the manager container. This was causing an error when the
deployment labels or container name that was expected were not present.
This fixes that by collecting related images from all containers across
all deployments.
This change also enables users to use related images in other containers
since related images from everywhere will be considered.
kind: "bugfix"
52 changes: 1 addition & 51 deletions internal/cmd/operator-sdk/generate/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
"github.com/operator-framework/operator-manifest-tools/pkg/image"
"github.com/operator-framework/operator-manifest-tools/pkg/imageresolver"
"github.com/operator-framework/operator-manifest-tools/pkg/pullspec"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"

metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics"
Expand Down Expand Up @@ -193,7 +191,7 @@ func (c bundleCmd) runManifests() (err error) {
c.println("Building a ClusterServiceVersion without an existing base")
}

relatedImages, err := c.findRelatedImages(col)
relatedImages, err := genutil.FindRelatedImages(col)
if err != nil {
return err
}
Expand Down Expand Up @@ -305,54 +303,6 @@ func (c bundleCmd) runMetadata() error {
return bundleMetadata.GenerateMetadata()
}

// findRelatedImages looks in the controller manager's environment for images used by the operator.
func (c bundleCmd) findRelatedImages(col *collector.Manifests) (map[string]string, error) {
const relatedImagePrefix = "RELATED_IMAGE_"
env, err := c.findManagerEnvironment(col)
if err != nil {
return nil, err
}
imageNames := make(map[string]string, len(env))
for _, envVar := range env {
if strings.HasPrefix(envVar.Name, relatedImagePrefix) {
if envVar.ValueFrom != nil {
return nil, fmt.Errorf("related images with valueFrom field unsupported, found in %s`", envVar.Name)
}

// transforms RELATED_IMAGE_This_IS_a_cool_image to this-is-a-cool-image
name := strings.ToLower(strings.Replace(strings.TrimPrefix(envVar.Name, relatedImagePrefix), "_", "-", -1))
imageNames[name] = envVar.Value
}
}

return imageNames, nil
}

// findManagerEnvironment returns the environment passed to the controller manager container.
func (c bundleCmd) findManagerEnvironment(col *collector.Manifests) ([]corev1.EnvVar, error) {
const (
managerLabel = "control-plane"
managerLabelValue = "controller-manager"
managerContainerName = "manager"
)

for _, deployment := range col.Deployments {
if val, ok := deployment.GetLabels()[managerLabel]; ok && val == managerLabelValue {
for _, container := range deployment.Spec.Template.Spec.Containers {
if container.Name == managerContainerName {
return container.Env, nil
}
}

return nil, fmt.Errorf("manager deployment does not have container named %q", managerContainerName)
}
}

return nil, fmt.Errorf(
"could not find manager deployment, should have label %s=%s", managerLabel, managerLabelValue,
)
}

// pinImages is used to replace all image tags in the given manifests with digests
func (c bundleCmd) pinImages(manifestPath string) error {
manifests, err := pullspec.FromDirectory(manifestPath, nil)
Expand Down
151 changes: 151 additions & 0 deletions internal/cmd/operator-sdk/generate/internal/relatedimages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2020 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 genutil

import (
"fmt"
"strings"

operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/operator-framework/operator-sdk/internal/generate/collector"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
)

// FindRelatedImages looks in the controller manager's environment for images used by the operator.
func FindRelatedImages(manifestCol *collector.Manifests) ([]operatorsv1alpha1.RelatedImage, error) {
col := relatedImageCollector{
relatedImages: []*relatedImage{},
relatedImagesByName: make(map[string][]*relatedImage),
relatedImagesByImageRef: make(map[string][]*relatedImage),
seenRelatedImages: sets.NewString(),
}

for _, deployment := range manifestCol.Deployments {
containers := append(deployment.Spec.Template.Spec.Containers, deployment.Spec.Template.Spec.InitContainers...)
for _, container := range containers {
// containerRef can just be the deployment if there's only one container
// otherwise we need {{ deployment.Name }}-{{ container.Name }}
containerRef := deployment.Name
if len(containers) > 1 {
containerRef += "-" + container.Name
}

if err := col.collectFromEnvironment(containerRef, container.Env); err != nil {
return nil, err
}
}
}

return col.collectedRelatedImages(), nil
}

const relatedImagePrefix = "RELATED_IMAGE_"

type relatedImage struct {
name string
imageRef string
containerRef string // If 1 container then {{deployment}} else {{deployment}}-{{container}}
}

type relatedImageCollector struct {
relatedImages []*relatedImage
relatedImagesByName map[string][]*relatedImage
relatedImagesByImageRef map[string][]*relatedImage
seenRelatedImages sets.String
}

func (c *relatedImageCollector) collectFromEnvironment(containerRef string, env []corev1.EnvVar) error {
for _, envVar := range env {
if strings.HasPrefix(envVar.Name, relatedImagePrefix) {
if envVar.ValueFrom != nil {
return fmt.Errorf("related images with valueFrom field unsupported, found in %s`", envVar.Name)
}

name := c.formatName(envVar.Name)
c.collect(name, envVar.Value, containerRef)
}
}

return nil
}

func (c *relatedImageCollector) collect(name, imageRef, containerRef string) {
// Don't add exact duplicates (same name and image)
key := name + "-" + imageRef
if c.seenRelatedImages.Has(key) {
return
}
c.seenRelatedImages.Insert(key)

relatedImg := relatedImage{
name: name,
imageRef: imageRef,
containerRef: containerRef,
}

c.relatedImages = append(c.relatedImages, &relatedImg)
if relatedImages, ok := c.relatedImagesByName[name]; ok {
c.relatedImagesByName[name] = append(relatedImages, &relatedImg)
} else {
c.relatedImagesByName[name] = []*relatedImage{&relatedImg}
}

if relatedImages, ok := c.relatedImagesByImageRef[imageRef]; ok {
c.relatedImagesByImageRef[imageRef] = append(relatedImages, &relatedImg)
} else {
c.relatedImagesByImageRef[imageRef] = []*relatedImage{&relatedImg}
}
}

func (c *relatedImageCollector) collectedRelatedImages() []operatorsv1alpha1.RelatedImage {
final := make([]operatorsv1alpha1.RelatedImage, 0, len(c.relatedImages))

for _, relatedImage := range c.relatedImages {
name := relatedImage.name

// Prefix the name with the containerRef on name collisions.
if len(c.relatedImagesByName[relatedImage.name]) > 1 {
name = relatedImage.containerRef + "-" + name
}

// Only add the related image to the final list if it's the first occurrence of an image.
// Blank out the name since the image is used multiple times and warn the user.
// Multiple containers using she same related image should use the same exact name.
if relatedImages := c.relatedImagesByImageRef[relatedImage.imageRef]; len(relatedImages) > 1 {
if relatedImages[0].name != relatedImage.name {
continue
}

name = ""
log.Warnf(
"warning: multiple related images with the same image ref, %q, and different names found."+
"The image will only be listed once with an empty name."+
"It is recmmended to either remove the duplicate or use the exact same name.",
relatedImage.name,
)
}

final = append(final, operatorsv1alpha1.RelatedImage{Name: name, Image: relatedImage.imageRef})
}

return final
}

// formatName transforms RELATED_IMAGE_This_IS_a_cool_image to this-is-a-cool-image
func (c *relatedImageCollector) formatName(name string) string {
return strings.ToLower(strings.Replace(strings.TrimPrefix(name, relatedImagePrefix), "_", "-", -1))
}
Loading

0 comments on commit 58e91a5

Please sign in to comment.