diff --git a/pkg/cli/admin/release/new.go b/pkg/cli/admin/release/new.go index b2f0858100..87faa4e04e 100644 --- a/pkg/cli/admin/release/new.go +++ b/pkg/cli/admin/release/new.go @@ -15,6 +15,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" @@ -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 { @@ -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 } diff --git a/pkg/cli/admin/release/new_test.go b/pkg/cli/admin/release/new_test.go index 8391747a09..0103768885 100644 --- a/pkg/cli/admin/release/new_test.go +++ b/pkg/cli/admin/release/new_test.go @@ -1,7 +1,12 @@ package release import ( + "bytes" "context" + "encoding/json" + "os" + "path/filepath" + "slices" "strings" "testing" @@ -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()