Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions pkg/cli/admin/release/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"path"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -711,6 +712,12 @@ func (o *NewOptions) Run(ctx context.Context) error {
}
}
ordered = filteredNames

if len(o.ToImageBaseTag) > 0 {
ordered = slices.DeleteFunc(ordered, func(s string) bool {
return s == o.ToImageBaseTag
})
}
}

if len(o.Mirror) > 0 {
Expand Down Expand Up @@ -1035,6 +1042,13 @@ func (o *NewOptions) extractManifests(is *imageapi.ImageStream, name string, met
}

if len(labels[annotationReleaseOperator]) == 0 {
if tag.Name == o.ToImageBaseTag {
if err := os.MkdirAll(dstDir, 0777); err != nil {
return false, err
}
klog.V(2).Infof("Image %s is the release base image, extracting for image-references", m.ImageRef)
return true, nil
}
klog.V(2).Infof("Image %s has no %s label, skipping", m.ImageRef, annotationReleaseOperator)
return false, nil
}
Expand Down
137 changes: 137 additions & 0 deletions pkg/cli/admin/release/new_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package release

import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"testing"

Expand All @@ -12,6 +17,138 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// createImageStream builds an ImageStream with tags for the given names.
func createImageStream(names ...string) *imageapi.ImageStream {
is := &imageapi.ImageStream{
TypeMeta: metav1.TypeMeta{
Kind: "ImageStream",
APIVersion: "image.openshift.io/v1",
},
}
for _, name := range names {
is.Spec.Tags = append(is.Spec.Tags, imageapi.TagReference{
Name: name,
From: &corev1.ObjectReference{
Kind: "DockerImage",
Name: "example.com/" + name + ":latest",
},
})
}
return is
}

// writeImageReferences creates an image-references file in dir with the given tag names.
func writeImageReferences(t *testing.T, dir string, names []string) {
t.Helper()
is := createImageStream(names...)
data, err := json.Marshal(is)
if err != nil {
t.Fatalf("failed to marshal image-references: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "image-references"), data, 0644); err != nil {
t.Fatalf("failed to write image-references: %v", err)
}
}

// tagNames returns the names of all tags in the image stream.
func tagNames(is *imageapi.ImageStream) []string {
var names []string
for _, tag := range is.Spec.Tags {
names = append(names, tag.Name)
}
return names
}

func TestPruneUnreferencedImageStreams(t *testing.T) {

t.Run("images referenced by operator image-references are kept", func(t *testing.T) {
dir := t.TempDir()
operatorDir := filepath.Join(dir, "my-operator")
if err := os.MkdirAll(operatorDir, 0777); err != nil {
t.Fatal(err)
}
writeImageReferences(t, operatorDir, []string{"helper-image"})

is := createImageStream("my-operator", "helper-image", "unreferenced-image")
metadata := map[string]imageData{
"my-operator": {Directory: operatorDir},
}

if err := pruneUnreferencedImageStreams(&bytes.Buffer{}, is, metadata, []string{"my-operator"}); err != nil {
t.Fatal(err)
}

names := tagNames(is)
if !slices.Contains(names, "my-operator") {
t.Error("expected my-operator to be kept (in include list)")
}
if !slices.Contains(names, "helper-image") {
t.Error("expected helper-image to be kept (referenced by operator image-references)")
}
if slices.Contains(names, "unreferenced-image") {
t.Error("expected unreferenced-image to be pruned")
}
})

t.Run("base image image-references prevents pruning", func(t *testing.T) {
dir := t.TempDir()

operatorDir := filepath.Join(dir, "my-operator")
if err := os.MkdirAll(operatorDir, 0777); err != nil {
t.Fatal(err)
}
writeImageReferences(t, operatorDir, []string{"operator-dep"})

baseDir := filepath.Join(dir, "cluster-version-operator")
if err := os.MkdirAll(baseDir, 0777); err != nil {
t.Fatal(err)
}
writeImageReferences(t, baseDir, []string{"cluster-update-console-plugin"})

is := createImageStream("cluster-version-operator", "my-operator", "operator-dep", "cluster-update-console-plugin")
metadata := map[string]imageData{
"my-operator": {Directory: operatorDir},
"cluster-version-operator": {Directory: baseDir},
}

if err := pruneUnreferencedImageStreams(&bytes.Buffer{}, is, metadata, []string{"cluster-version-operator", "my-operator"}); err != nil {
t.Fatal(err)
}

names := tagNames(is)
if !slices.Contains(names, "cluster-update-console-plugin") {
t.Error("expected cluster-update-console-plugin to be kept (referenced by base image image-references)")
}
if !slices.Contains(names, "operator-dep") {
t.Error("expected operator-dep to be kept (referenced by operator image-references)")
}
})

t.Run("without base image image-references the image is pruned", func(t *testing.T) {
dir := t.TempDir()

operatorDir := filepath.Join(dir, "my-operator")
if err := os.MkdirAll(operatorDir, 0777); err != nil {
t.Fatal(err)
}
writeImageReferences(t, operatorDir, []string{"operator-dep"})

is := createImageStream("cluster-version-operator", "my-operator", "operator-dep", "cluster-update-console-plugin")
metadata := map[string]imageData{
"my-operator": {Directory: operatorDir},
}

if err := pruneUnreferencedImageStreams(&bytes.Buffer{}, is, metadata, []string{"cluster-version-operator", "my-operator"}); err != nil {
t.Fatal(err)
}

names := tagNames(is)
if slices.Contains(names, "cluster-update-console-plugin") {
t.Error("expected cluster-update-console-plugin to be pruned (not referenced by any image-references)")
}
})
}

func TestMirrorImages(t *testing.T) {
ctx := context.Background()

Expand Down