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 b4fddc9 commit 66e204f
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 106 deletions.
107 changes: 1 addition & 106 deletions internal/cmd/operator-sdk/generate/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
"sigs.k8s.io/yaml"

operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics"
genutil "github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/generate/internal"
gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion"
Expand All @@ -35,7 +33,6 @@ import (
"github.com/operator-framework/operator-sdk/internal/registry"
"github.com/operator-framework/operator-sdk/internal/scorecard"
"github.com/operator-framework/operator-sdk/internal/util/bundleutil"
log "github.com/sirupsen/logrus"
)

const (
Expand Down Expand Up @@ -191,7 +188,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 @@ -296,105 +293,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) ([]operatorsv1alpha1.RelatedImage, error) {
relatedImages, relatedImageSources, err := c.collectRelatedImages(col)
if err != nil {
return nil, err
}

var (
finalRelatedImages = make([]operatorsv1alpha1.RelatedImage, 0, len(relatedImages))
skip = make(map[int]struct{})
)

for i := range relatedImages {
if _, ok := skip[i]; ok {
continue
}

var (
relatedImage = &relatedImages[i]
nameCollision bool
imageCollision bool
)

for j := range relatedImages {
if j <= i {
continue
}

otherRelatedImage := &relatedImages[j]
if relatedImage.Name == otherRelatedImage.Name && relatedImage.Image == otherRelatedImage.Image {
// Skip the other image exact duplicates
skip[j] = struct{}{}
} else if relatedImage.Name == otherRelatedImage.Name {
// Prefix the names when there is a collision, but different images
nameCollision = true
otherRelatedImage.Name = relatedImageSources[j] + "_" + otherRelatedImage.Name
} else if relatedImage.Image == otherRelatedImage.Image {
// Skip the other image when the images are the same
skip[j] = struct{}{}
imageCollision = true
}
}

if nameCollision {
// Prefix the name when there was a collision
relatedImage.Name = relatedImageSources[i] + "_" + relatedImage.Name
}
if imageCollision {
// Print a warning and zero out the name if there is an image collision.
log.Warnln(
"warning: multiple relaedImages with the same image 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 if it is needed.",
)

relatedImage.Name = ""
}

finalRelatedImages = append(finalRelatedImages, *relatedImage)
}

return finalRelatedImages, nil
}

// relatedImage is an intermediary struct used to track discovered related
// collectRelatedImages returns all related images defined in the environment of all deployments/containers.
// It also returns a list of {{delpoyment}}-{{container}} for each related image in the event that name collisions
// need to be resolved with additional unique identifiers.
func (c bundleCmd) collectRelatedImages(col *collector.Manifests) ([]operatorsv1alpha1.RelatedImage, []string, error) {
const relatedImagePrefix = "RELATED_IMAGE_"

var (
relatedImages []operatorsv1alpha1.RelatedImage
relatedImageSources []string
)

for _, deployment := range col.Deployments {
for _, container := range deployment.Spec.Template.Spec.Containers {
for _, envVar := range container.Env {
if strings.HasPrefix(envVar.Name, relatedImagePrefix) {
if envVar.ValueFrom != nil {
return nil, 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.TrimPrefix(envVar.Name, relatedImagePrefix)
name = strings.Replace(name, "_", "-", -1)
name = strings.ToLower(name)

relatedImages = append(relatedImages, operatorsv1alpha1.RelatedImage{
Name: name,
Image: envVar.Value,
})
relatedImageSources = append(relatedImageSources, deployment.Name+"-"+container.Name)
}
}
}
}

return relatedImages, relatedImageSources, 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 66e204f

Please sign in to comment.