Repository: GoogleContainerTools/skaffold
Pull request #7591 (2022-06-27 07:49:31)

fix: [v1] do not replace every string matching image names
This is a cherry pick from another PR (#7282) I submitted to the `main` branch.


barata: FYI @aaron-prindle @tejal29 @MarlonGamez 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac36c70a0..50da27dec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# v1.39.1 Release - 06/24/2022
+# v1.39.1 Release - 06/28/2022
 **Linux amd64**
 `curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/v1.39.1/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin`
 
@@ -21,6 +21,7 @@ Note: This release is patch to v1.39.1 which fixes issue helm debug issue for cl
 
 Fixes:
 * Revert Helm 3.0 and Helm 3.1 Deployer changes [#7582](https://github.com/GoogleContainerTools/skaffold/issues/7582)
+* fix: index out of range error [#7593](https://github.com/GoogleContainerTools/skaffold/pull/7593)
 
 # v1.39.0 Release - 06/23/2022
 **Linux amd64**
diff --git a/pkg/skaffold/build/cache/details.go b/pkg/skaffold/build/cache/details.go
index 9cc805ee4..5dbc0d7e0 100644
--- a/pkg/skaffold/build/cache/details.go
+++ b/pkg/skaffold/build/cache/details.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
 type cacheDetails interface {
@@ -91,7 +90,7 @@ func (d needsRemoteTagging) Hash() string {
 func (d needsRemoteTagging) Tag(ctx context.Context, c *cache, platforms platform.Resolver) error {
 	fqn := d.tag + "@" + d.digest // Tag is not important. We just need the registry and the digest to locate the image.
 	matched := platforms.GetPlatforms(strings.Split(filepath.Base(d.tag), ":")[0])
-	return docker.AddRemoteTag(fqn, d.tag, c.cfg, util.ConvertToV1Platform(matched.Platforms[0]))
+	return docker.AddRemoteTag(fqn, d.tag, c.cfg, matched.Platforms)
 }
 
 // Found locally. Needs pushing
diff --git a/pkg/skaffold/build/gcb/cloud_build.go b/pkg/skaffold/build/gcb/cloud_build.go
index 88bcfa483..0ecf00dbf 100644
--- a/pkg/skaffold/build/gcb/cloud_build.go
+++ b/pkg/skaffold/build/gcb/cloud_build.go
@@ -298,7 +298,7 @@ func (b *Builder) getDigest(cb *cloudbuild.Build, defaultToTag string, platforms
 	// The build steps pushed the image directly like when we use Jib.
 	// Retrieve the digest for that tag.
 	// TODO(dgageot): I don't think GCB can push to an insecure registry.
-	return docker.RemoteDigest(defaultToTag, b.cfg, util.ConvertToV1Platform(platforms.Platforms[0]))
+	return docker.RemoteDigest(defaultToTag, b.cfg, platforms.Platforms)
 }
 
 func (b *Builder) getLogs(ctx context.Context, c *cstorage.Client, offset int64, bucket, objectName string) (io.ReadCloser, error) {
diff --git a/pkg/skaffold/docker/remote.go b/pkg/skaffold/docker/remote.go
index afa53dd5f..b0f56a610 100644
--- a/pkg/skaffold/docker/remote.go
+++ b/pkg/skaffold/docker/remote.go
@@ -24,9 +24,11 @@ import (
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
 	"github.com/google/go-containerregistry/pkg/v1/tarball"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 
 	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
 // for testing
@@ -36,9 +38,9 @@ var (
 	remoteIndex  = remote.Index
 )
 
-func AddRemoteTag(src, target string, cfg Config, platform *v1.Platform) error {
+func AddRemoteTag(src, target string, cfg Config, platforms []specs.Platform) error {
 	log.Entry(context.TODO()).Debugf("attempting to add tag %s to src %s", target, src)
-	img, err := getRemoteImage(src, cfg, platform)
+	img, err := getRemoteImage(src, cfg, platforms)
 	if err != nil {
 		return fmt.Errorf("getting image: %w", err)
 	}
@@ -51,13 +53,13 @@ func AddRemoteTag(src, target string, cfg Config, platform *v1.Platform) error {
 	return remote.Write(targetRef, img, remote.WithAuthFromKeychain(primaryKeychain))
 }
 
-func getRemoteDigest(identifier string, cfg Config, platform *v1.Platform) (string, error) {
+func getRemoteDigest(identifier string, cfg Config, platforms []specs.Platform) (string, error) {
 	idx, err := getRemoteIndex(identifier, cfg)
 	if err == nil {
 		return digest(idx)
 	}
 
-	img, err := getRemoteImage(identifier, cfg, platform)
+	img, err := getRemoteImage(identifier, cfg, platforms)
 	if err != nil {
 		return "", fmt.Errorf("getting image: %w", err)
 	}
@@ -66,8 +68,8 @@ func getRemoteDigest(identifier string, cfg Config, platform *v1.Platform) (stri
 }
 
 // RetrieveRemoteConfig retrieves the remote config file for an image
-func RetrieveRemoteConfig(identifier string, cfg Config, platform *v1.Platform) (*v1.ConfigFile, error) {
-	img, err := getRemoteImage(identifier, cfg, platform)
+func RetrieveRemoteConfig(identifier string, cfg Config, platforms []specs.Platform) (*v1.ConfigFile, error) {
+	img, err := getRemoteImage(identifier, cfg, platforms)
 	if err != nil {
 		return nil, err
 	}
@@ -76,7 +78,7 @@ func RetrieveRemoteConfig(identifier string, cfg Config, platform *v1.Platform)
 }
 
 // Push pushes the tarball image
-func Push(tarPath, tag string, cfg Config, platform *v1.Platform) (string, error) {
+func Push(tarPath, tag string, cfg Config, platforms []specs.Platform) (string, error) {
 	t, err := name.NewTag(tag, name.WeakValidation)
 	if err != nil {
 		return "", fmt.Errorf("parsing tag %q: %w", tag, err)
@@ -91,10 +93,16 @@ func Push(tarPath, tag string, cfg Config, platform *v1.Platform) (string, error
 		return "", fmt.Errorf("%s %q: %w", sErrors.PushImageErr, t, err)
 	}
 
-	return getRemoteDigest(tag, cfg, platform)
+	return getRemoteDigest(tag, cfg, platforms)
 }
 
-func getRemoteImage(identifier string, cfg Config, platform *v1.Platform) (v1.Image, error) {
+func getRemoteImage(identifier string, cfg Config, platforms []specs.Platform) (v1.Image, error) {
+	// for multi-arch images, we can only get the image for one platform at a time.
+	// If the platform is not specified then it'll fetch the image for the host architecture.
+	// To avoid discrepencies, we fail image fetch for multi-platform images. These should instead be accessed with the `remoteIndex` method.
+	if len(platforms) > 1 {
+		return nil, fmt.Errorf("cannot fetch remote image %q for multiple platforms %q", identifier, platforms)
+	}
 	ref, err := parseReference(identifier, cfg)
 	if err != nil {
 		return nil, err
@@ -102,8 +110,8 @@ func getRemoteImage(identifier string, cfg Config, platform *v1.Platform) (v1.Im
 	options := []remote.Option{
 		remote.WithAuthFromKeychain(primaryKeychain),
 	}
-	if platform != nil {
-		options = append(options, remote.WithPlatform(*platform))
+	if len(platforms) == 1 {
+		options = append(options, remote.WithPlatform(util.ConvertToV1Platform(platforms[0])))
 	}
 
 	return remoteImage(ref, options...)
diff --git a/pkg/skaffold/kubernetes/manifest/images.go b/pkg/skaffold/kubernetes/manifest/images.go
index 3d2e1bb74..8a8ee85a9 100644
--- a/pkg/skaffold/kubernetes/manifest/images.go
+++ b/pkg/skaffold/kubernetes/manifest/images.go
@@ -29,6 +29,8 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 )
 
+const imageField = "image"
+
 type ResourceSelectorImages struct {
 	allowlist map[apimachinery.GroupKind]latest.ResourceFilter
 	denylist  map[apimachinery.GroupKind]latest.ResourceFilter
@@ -62,9 +64,12 @@ func (rsi *ResourceSelectorImages) allowByGroupKind(gk apimachinery.GroupKind) b
 }
 
 func (rsi *ResourceSelectorImages) allowByNavpath(gk apimachinery.GroupKind, navpath string, k string) (string, bool) {
+	matchedConfigConnectorImage := false
+
 	for _, w := range ConfigConnectorResourceSelector {
-		if w.Matches(gk.Group, gk.Kind) {
-			return "", true
+		if k == imageField && w.Matches(gk.Group, gk.Kind) {
+			matchedConfigConnectorImage = true
+			break
 		}
 	}
 
@@ -80,11 +85,10 @@ func (rsi *ResourceSelectorImages) allowByNavpath(gk apimachinery.GroupKind, nav
 	}
 
 	if rf, ok := rsi.allowlist[gk]; ok {
+		matchedConfigConnectorImage = false
+
 		for _, allowpath := range rf.Image {
-			if allowpath == ".*" {
-				if k != "image" {
-					return "", false
-				}
+			if allowpath == ".*" && k == imageField {
 				return "", true
 			}
 			if navpath == allowpath {
@@ -92,7 +96,7 @@ func (rsi *ResourceSelectorImages) allowByNavpath(gk apimachinery.GroupKind, nav
 			}
 		}
 	}
-	return "", false
+	return "", matchedConfigConnectorImage
 }
 
 // GetImages gathers a map of base image names to the image with its tag
@@ -107,7 +111,7 @@ type imageSaver struct {
 }
 
 func (is *imageSaver) Visit(gk apimachinery.GroupKind, navpath string, o map[string]interface{}, k string, v interface{}, rs ResourceSelector) bool {
-	if k != "image" {
+	if k != imageField {
 		return true
 	}
 
diff --git a/pkg/skaffold/util/platform.go b/pkg/skaffold/util/platform.go
index a572afe8f..3355c66ef 100644
--- a/pkg/skaffold/util/platform.go
+++ b/pkg/skaffold/util/platform.go
@@ -21,6 +21,6 @@ import (
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
-func ConvertToV1Platform(platform specs.Platform) *v1.Platform {
-	return &v1.Platform{Architecture: platform.Architecture, OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: platform.Variant}
+func ConvertToV1Platform(platform specs.Platform) v1.Platform {
+	return v1.Platform{Architecture: platform.Architecture, OS: platform.OS, OSVersion: platform.OSVersion, OSFeatures: platform.OSFeatures, Variant: platform.Variant}
 }


Test patch:
diff --git a/pkg/skaffold/build/cache/lookup_test.go b/pkg/skaffold/build/cache/lookup_test.go
index bd6cef8b8..704d42ed2 100644
--- a/pkg/skaffold/build/cache/lookup_test.go
+++ b/pkg/skaffold/build/cache/lookup_test.go
@@ -24,7 +24,7 @@ import (
 	"testing"
 
 	"github.com/docker/docker/client"
-	v1 "github.com/google/go-containerregistry/pkg/v1"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
@@ -197,7 +197,7 @@ func TestLookupRemote(t *testing.T) {
 	}
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
-			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ *v1.Platform) (string, error) {
+			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ []specs.Platform) (string, error) {
 				switch {
 				case identifier == "tag":
 					return "digest", nil
diff --git a/pkg/skaffold/build/cache/retrieve_test.go b/pkg/skaffold/build/cache/retrieve_test.go
index 6b1780d98..ded16d610 100644
--- a/pkg/skaffold/build/cache/retrieve_test.go
+++ b/pkg/skaffold/build/cache/retrieve_test.go
@@ -24,7 +24,7 @@ import (
 	"testing"
 
 	"github.com/docker/docker/api/types"
-	v1 "github.com/google/go-containerregistry/pkg/v1"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
@@ -219,7 +219,7 @@ func TestCacheBuildRemote(t *testing.T) {
 			return dockerDaemon, nil
 		})
 		t.Override(&docker.DefaultAuthHelper, stubAuth{})
-		t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ *v1.Platform) (string, error) {
+		t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ []specs.Platform) (string, error) {
 			switch ref {
 			case "artifact1:tag1":
 				return "sha256:51ae7fa00c92525c319404a3a6d400e52ff9372c5a39cb415e0486fe425f3165", nil
@@ -304,7 +304,7 @@ func TestCacheFindMissing(t *testing.T) {
 			return dockerDaemon, nil
 		})
 		t.Override(&docker.DefaultAuthHelper, stubAuth{})
-		t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ *v1.Platform) (string, error) {
+		t.Override(&docker.RemoteDigest, func(ref string, _ docker.Config, _ []specs.Platform) (string, error) {
 			switch ref {
 			case "artifact1:tag1":
 				return "sha256:51ae7fa00c92525c319404a3a6d400e52ff9372c5a39cb415e0486fe425f3165", nil
diff --git a/pkg/skaffold/build/jib/gradle_test.go b/pkg/skaffold/build/jib/gradle_test.go
index 06013c9a1..6deb8944a 100644
--- a/pkg/skaffold/build/jib/gradle_test.go
+++ b/pkg/skaffold/build/jib/gradle_test.go
@@ -28,7 +28,6 @@ import (
 	"testing"
 	"time"
 
-	cv1 "github.com/google/go-containerregistry/pkg/v1"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
@@ -150,7 +149,7 @@ func TestBuildJibGradleToRegistry(t *testing.T) {
 			t.NewTempDir().Touch("build.gradle").Chdir()
 			t.Override(&gradleBuildArgsFunc, getGradleBuildArgsFuncFake(t, MinimumJibGradleVersion))
 			t.Override(&util.DefaultExecCommand, test.commands)
-			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ *cv1.Platform) (string, error) {
+			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ []v1.Platform) (string, error) {
 				if identifier == "img:tag" {
 					return "digest", nil
 				}
diff --git a/pkg/skaffold/build/jib/maven_test.go b/pkg/skaffold/build/jib/maven_test.go
index 4e981da48..ecfb2ae2b 100644
--- a/pkg/skaffold/build/jib/maven_test.go
+++ b/pkg/skaffold/build/jib/maven_test.go
@@ -28,7 +28,6 @@ import (
 	"testing"
 	"time"
 
-	cv1 "github.com/google/go-containerregistry/pkg/v1"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
@@ -144,7 +143,7 @@ func TestBuildJibMavenToRegistry(t *testing.T) {
 			t.Override(&mavenBuildArgsFunc, getMavenBuildArgsFuncFake(t, MinimumJibMavenVersion))
 			t.NewTempDir().Touch("pom.xml").Chdir()
 			t.Override(&util.DefaultExecCommand, test.commands)
-			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ *cv1.Platform) (string, error) {
+			t.Override(&docker.RemoteDigest, func(identifier string, _ docker.Config, _ []v1.Platform) (string, error) {
 				if identifier == "img:tag" {
 					return "digest", nil
 				}
diff --git a/pkg/skaffold/kubernetes/manifest/images_test.go b/pkg/skaffold/kubernetes/manifest/images_test.go
index 7ca1cf5dc..f517072c5 100644
--- a/pkg/skaffold/kubernetes/manifest/images_test.go
+++ b/pkg/skaffold/kubernetes/manifest/images_test.go
@@ -20,8 +20,12 @@ import (
 	"context"
 	"testing"
 
+	apimachinery "k8s.io/apimachinery/pkg/runtime/schema"
+
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/warnings"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/yaml"
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
@@ -203,6 +207,470 @@ spec:
 	})
 }
 
+func TestReplaceImagesOnConfigConnectorResources(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  location: EU
+  image: this-should-not-change
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: gcr.io/k8s-skaffold/image-name:TAG
+spec:
+  location: EU
+  image: this-should-not-change
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(TransformAllowlist, TransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnConfigConnectorResourcesUsingNonDefaultField(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: this-should-not-change
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: gcr.io/k8s-skaffold/image-name:TAG
+  image: this-should-not-change
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformAllowlist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformAllowlist {
+			newTransformAllowlist[k] = v
+		}
+		newTransformAllowlist[apimachinery.GroupKind{Group: "storage.cnrm.cloud.google.com", Kind: "StorageBucket"}] =
+			latest.ResourceFilter{GroupKind: "StorageBucket.storage.cnrm.cloud.google.com", Image: []string{".spec.template.spec.restartPolicy"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(newTransformAllowlist, TransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnConfigConnectorResourcesUsingWildcardAndNonDefaultField(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: this-should-not-change
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: gcr.io/k8s-skaffold/image-name:TAG
+spec:
+  template:
+    spec:
+      restartPolicy: gcr.io/k8s-skaffold/image-name:TAG
+  image: this-should-not-change
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformAllowlist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformAllowlist {
+			newTransformAllowlist[k] = v
+		}
+		newTransformAllowlist[apimachinery.GroupKind{Group: "storage.cnrm.cloud.google.com", Kind: "StorageBucket"}] =
+			latest.ResourceFilter{GroupKind: "StorageBucket.storage.cnrm.cloud.google.com", Image: []string{".*", ".spec.template.spec.restartPolicy"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(newTransformAllowlist, TransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnConfigConnectorDeniedResources(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  location: EU
+  image: image-name
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  location: EU
+  image: gcr.io/k8s-skaffold/image-name:TAG
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformDenylist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformDenylist {
+			newTransformDenylist[k] = v
+		}
+		newTransformDenylist[apimachinery.GroupKind{Group: "storage.cnrm.cloud.google.com", Kind: "StorageBucket"}] =
+			latest.ResourceFilter{GroupKind: "StorageBucket.storage.cnrm.cloud.google.com", Image: []string{".metadata.labels.image"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(TransformAllowlist, newTransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnConfigConnectorAllDeniedResources(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  location: EU
+  image: image-name
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: storage.cnrm.cloud.google.com/v1beta1
+kind: StorageBucket
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  location: EU
+  image: image-name
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformDenylist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformDenylist {
+			newTransformDenylist[k] = v
+		}
+		newTransformDenylist[apimachinery.GroupKind{Group: "storage.cnrm.cloud.google.com", Kind: "StorageBucket"}] =
+			latest.ResourceFilter{GroupKind: "StorageBucket.storage.cnrm.cloud.google.com", Image: []string{".*"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(TransformAllowlist, newTransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnPredefinedAllowedResourcesUsingNonDefaultField(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: this-should-not-change
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: gcr.io/k8s-skaffold/image-name:TAG
+  image: this-should-not-change
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformAllowlist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformAllowlist {
+			newTransformAllowlist[k] = v
+		}
+		newTransformAllowlist[apimachinery.GroupKind{Group: "batch", Kind: "Job"}] =
+			latest.ResourceFilter{GroupKind: "Job.batch", Image: []string{".spec.template.spec.restartPolicy"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(newTransformAllowlist, TransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnPredefinedAllowedResources(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: this-should-not-change
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: gcr.io/k8s-skaffold/image-name:TAG
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: this-should-not-change
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(TransformAllowlist, TransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
+func TestReplaceImagesOnPredefinedDeniedResources(t *testing.T) {
+	manifests := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: image-name
+`)}
+
+	builds := []graph.Artifact{{
+		ImageName: "image-name",
+		Tag:       "gcr.io/k8s-skaffold/image-name:TAG",
+	}}
+
+	expected := ManifestList{[]byte(`
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: image-name
+  labels:
+    app.kubernetes.io/name: image-name
+    this.should.not.change: image-name
+    image: image-name
+spec:
+  template:
+    spec:
+      restartPolicy: image-name
+  image: gcr.io/k8s-skaffold/image-name:TAG
+`)}
+
+	testutil.Run(t, "", func(t *testutil.T) {
+		fakeWarner := &warnings.Collect{}
+		t.Override(&warnings.Printf, fakeWarner.Warnf)
+
+		newTransformDenylist := make(map[apimachinery.GroupKind]latest.ResourceFilter)
+		for k, v := range TransformDenylist {
+			newTransformDenylist[k] = v
+		}
+		newTransformDenylist[apimachinery.GroupKind{Group: "batch", Kind: "Job"}] =
+			latest.ResourceFilter{GroupKind: "Job.batch", Image: []string{".metadata.labels.image"}}
+		resultManifest, err := manifests.ReplaceImages(context.TODO(), builds, NewResourceSelectorImages(TransformAllowlist, newTransformDenylist))
+
+		t.CheckNoError(err)
+
+		expectedYaml := make(map[string]interface{})
+		yaml.Unmarshal(expected[0], &expectedYaml)
+		resultYaml := make(map[string]interface{})
+		yaml.Unmarshal(resultManifest[0], &resultYaml)
+
+		t.CheckMapsMatch(expectedYaml, resultYaml)
+	})
+}
+
 func TestReplaceEmptyManifest(t *testing.T) {
 	manifests := ManifestList{[]byte(""), []byte("  ")}
 	expected := ManifestList{}