diff --git a/pkg/lib/image.go b/pkg/lib/image.go index 6db84e9e..b806dc92 100644 --- a/pkg/lib/image.go +++ b/pkg/lib/image.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "regexp" "sort" "strings" @@ -119,11 +120,16 @@ func mapChartImages(rootDir string, values map[string]string, sh *shell.Session, } } +// placeholderRE matches a shell-style ${...} template placeholder. +var placeholderRE = regexp.MustCompile(`\$\{[^}]*\}`) + func collectImages(obj map[string]any, images map[string]string, srcGK string) { for k, v := range obj { if k == "image" { if s, ok := v.(string); ok && strings.ContainsRune(s, ':') { - images[s] = srcGK + for _, img := range expandVersionedImage(s, obj) { + images[img] = srcGK + } } } else if m, ok := v.(map[string]any); ok { collectImages(m, images, srcGK) @@ -137,6 +143,34 @@ func collectImages(obj map[string]any, images map[string]string, srcGK string) { } } +// expandVersionedImage expands a ${...} placeholder in an image reference using +// the sibling "availableVersions" list found on the same object (kubestash addon +// Function resources carry a "v0.29.0_${DB_VERSION}"-style tag alongside the list +// of versions the image is published for). If the reference has no placeholder, +// or no usable availableVersions is present, it is returned unchanged; unexpanded +// placeholders are dropped later by ListImages/GroupImages. +func expandVersionedImage(image string, obj map[string]any) []string { + if !placeholderRE.MatchString(image) { + return []string{image} + } + + versions, ok := obj["availableVersions"].([]any) + if !ok || len(versions) == 0 { + return []string{image} + } + + out := make([]string, 0, len(versions)) + for _, v := range versions { + if ver, ok := v.(string); ok { + out = append(out, placeholderRE.ReplaceAllString(image, ver)) + } + } + if len(out) == 0 { + return []string{image} + } + return out +} + func GroupImages(images map[string]string) map[string][]string { result := map[string][]string{} for img, srcGK := range images { diff --git a/pkg/lib/image_test.go b/pkg/lib/image_test.go new file mode 100644 index 00000000..e7b931bb --- /dev/null +++ b/pkg/lib/image_test.go @@ -0,0 +1,103 @@ +/* +Copyright AppsCode Inc. and Contributors + +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 lib + +import ( + "sort" + "testing" +) + +func TestCollectImagesExpandsVersionedImage(t *testing.T) { + // Shape of a rendered kubestash addons.kubestash.com/v1alpha1 Function. + fn := map[string]any{ + "spec": map[string]any{ + "availableVersions": []any{"16.4", "17.2", "18.2"}, + "image": "ghcr.io/kubedb/postgres-restic-plugin:v0.29.0_${DB_VERSION}", + "args": []any{ + "physical-restore", + "--namespace=${namespace:=default}", + }, + }, + } + + images := map[string]string{} + collectImages(fn, images, "Function.addons.kubestash.com") + + got := make([]string, 0, len(images)) + for img := range images { + got = append(got, img) + } + sort.Strings(got) + + want := []string{ + "ghcr.io/kubedb/postgres-restic-plugin:v0.29.0_16.4", + "ghcr.io/kubedb/postgres-restic-plugin:v0.29.0_17.2", + "ghcr.io/kubedb/postgres-restic-plugin:v0.29.0_18.2", + } + if len(got) != len(want) { + t.Fatalf("expected %d images, got %d: %v", len(want), len(got), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("image[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestExpandVersionedImage(t *testing.T) { + cases := []struct { + name string + image string + obj map[string]any + want []string + }{ + { + name: "no placeholder returned unchanged", + image: "ghcr.io/kubedb/postgres-restic-plugin:v0.29.0", + obj: map[string]any{"availableVersions": []any{"16.4"}}, + want: []string{"ghcr.io/kubedb/postgres-restic-plugin:v0.29.0"}, + }, + { + name: "placeholder but no availableVersions kept as-is", + image: "repo/img:v1_${DB_VERSION}", + obj: map[string]any{}, + want: []string{"repo/img:v1_${DB_VERSION}"}, + }, + { + name: "generic placeholder expanded", + image: "repo/img:${VER}", + obj: map[string]any{"availableVersions": []any{"1.0", "2.0"}}, + want: []string{"repo/img:1.0", "repo/img:2.0"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := expandVersionedImage(tc.image, tc.obj) + sort.Strings(got) + sort.Strings(tc.want) + if len(got) != len(tc.want) { + t.Fatalf("got %v, want %v", got, tc.want) + } + for i := range tc.want { + if got[i] != tc.want[i] { + t.Errorf("got[%d] = %q, want %q", i, got[i], tc.want[i]) + } + } + }) + } +}