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.
  • Loading branch information
ryantking committed Apr 9, 2022
1 parent 831a71a commit dc99a42
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 87 deletions.
75 changes: 1 addition & 74 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 @@ -304,74 +302,3 @@ 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)
if err != nil {
return err
}
resolver, err := imageresolver.GetResolver(imageresolver.ResolverCrane, nil)
if err != nil {
return err
}
if err := image.Pin(resolver, manifests); err != nil {
return err
}

for _, manifest := range manifests {
if err := manifest.Dump(nil); err != nil {
return err
}
}

return nil
}
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 := deployment.Spec.Template.Spec.Containers
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 its 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 dc99a42

Please sign in to comment.