Repository: replicatedhq/kots
Pull request #187 (2019-12-09 22:17:40)

Revert "Revert "never write images to files when pulling, upload them immediately""
Reverts replicatedhq/kots#186, restores replicatedhq/kots#183


Patch:
diff --git a/go.mod b/go.mod
index e4d1d4db0..ea1f25f3f 100644
--- a/go.mod
+++ b/go.mod
@@ -79,7 +79,6 @@ require (
 	k8s.io/code-generator v0.0.0-20181117043124-c2090bec4d9b // indirect
 	k8s.io/helm v2.14.3+incompatible
 	sigs.k8s.io/controller-runtime v0.2.0-beta.2
-	sigs.k8s.io/controller-tools v0.2.0-beta.2 // indirect
 	sigs.k8s.io/kustomize/v3 v3.1.0
 	sigs.k8s.io/yaml v1.1.0
 )
diff --git a/kotskinds/apis/kots/v1beta1/license_types.go b/kotskinds/apis/kots/v1beta1/license_types.go
index ec098e330..c3488dc99 100644
--- a/kotskinds/apis/kots/v1beta1/license_types.go
+++ b/kotskinds/apis/kots/v1beta1/license_types.go
@@ -107,7 +107,6 @@ type LicenseSpec struct {
 	LicenseID         string                      `json:"licenseID"`
 	LicenseType       string                      `json:"licenseType,omitempty"`
 	IsAirgapSupported bool                        `json:"isAirgapSupported,omitempty"`
-	IsGitOpsSupported bool                        `json:"isGitOpsSupported,omitempty"`
 	Entitlements      map[string]EntitlementField `json:"entitlements,omitempty"`
 }
 
diff --git a/pkg/image/builder.go b/pkg/image/builder.go
index 8be9c55b8..e678fba7f 100644
--- a/pkg/image/builder.go
+++ b/pkg/image/builder.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"os"
+	"path"
 	"path/filepath"
 	"strings"
 
@@ -24,6 +25,7 @@ import (
 	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"gopkg.in/yaml.v2"
+	kustomizeimage "sigs.k8s.io/kustomize/v3/pkg/image"
 )
 
 var imagePolicy = []byte(`{
@@ -42,8 +44,9 @@ type RegistryAuth struct {
 	Password string
 }
 
-func SaveImages(srcRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, reportWriter io.Writer, imagesDir string, upstreamDir string) error {
+func CopyImages(srcRegistry, destRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, reportWriter io.Writer, upstreamDir string) ([]kustomizeimage.Image, error) {
 	savedImages := make(map[string]bool)
+	newImages := []kustomizeimage.Image{}
 
 	err := filepath.Walk(upstreamDir,
 		func(path string, info os.FileInfo, err error) error {
@@ -60,19 +63,20 @@ func SaveImages(srcRegistry registry.RegistryOptions, appSlug string, log *logge
 				return err
 			}
 
-			err = saveImagesToFiles(srcRegistry, appSlug, log, imagesDir, reportWriter, contents, savedImages)
+			newImagesSubset, err := copyImagesBetweenRegistries(srcRegistry, destRegistry, appSlug, log, reportWriter, contents, savedImages)
 			if err != nil {
-				return errors.Wrap(err, "failed to extract images")
+				return errors.Wrapf(err, "failed to copy images mentioned in %s", path)
 			}
 
+			newImages = append(newImages, newImagesSubset...)
 			return nil
 		})
 
 	if err != nil {
-		return errors.Wrap(err, "failed to walk upstream dir")
+		return nil, errors.Wrap(err, "failed to walk upstream dir")
 	}
 
-	return nil
+	return newImages, nil
 }
 
 func GetPrivateImages(upstreamDir string) ([]string, []*k8sdoc.Doc, error) {
@@ -162,18 +166,23 @@ func GetObjectsWithImages(upstreamDir string) ([]*k8sdoc.Doc, error) {
 	return objects, nil
 }
 
-func saveImagesToFiles(srcRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, imagesDir string, reportWriter io.Writer, fileData []byte, savedImages map[string]bool) error {
+func copyImagesBetweenRegistries(srcRegistry, destRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, reportWriter io.Writer, fileData []byte, savedImages map[string]bool) ([]kustomizeimage.Image, error) {
+	newImages := []kustomizeimage.Image{}
 	err := listImagesInFile(fileData, func(images []string, doc *k8sdoc.Doc) error {
 		for _, image := range images {
 			if _, saved := savedImages[image]; saved {
 				continue
 			}
 
-			log.ChildActionWithSpinner("Pulling image %s", image)
-			err := saveOneImage(srcRegistry, imagesDir, image, appSlug, reportWriter)
+			log.ChildActionWithSpinner("Transferring image %s", image)
+			newImage, err := copyOneImage(srcRegistry, destRegistry, image, appSlug, reportWriter, log)
 			if err != nil {
 				log.FinishChildSpinner()
-				return errors.Wrap(err, "failed to save image")
+				return errors.Wrapf(err, "failed to transfer image %s", image)
+			}
+
+			if newImage != nil {
+				newImages = append(newImages, newImage...)
 			}
 
 			log.FinishChildSpinner()
@@ -183,7 +192,7 @@ func saveImagesToFiles(srcRegistry registry.RegistryOptions, appSlug string, log
 		return nil
 	})
 
-	return err
+	return newImages, err
 }
 
 type processImagesFunc func([]string, *k8sdoc.Doc) error
@@ -209,41 +218,21 @@ func listImagesInFile(contents []byte, handler processImagesFunc) error {
 	return nil
 }
 
-func saveOneImage(srcRegistry registry.RegistryOptions, imagesDir string, image string, appSlug string, reportWriter io.Writer) error {
-	imageRef, err := imageRefImage(image)
-	if err != nil {
-		return errors.Wrap(err, "failed to parse image ref")
-	}
-
-	imageFormat := "docker-archive"
-	pathInBundle := imageRef.pathInBundle(imageFormat)
-	archiveName := filepath.Join(imagesDir, pathInBundle)
-	destDir := filepath.Dir(archiveName)
-
-	if err := os.MkdirAll(destDir, 0755); err != nil {
-		return errors.Wrap(err, "failed to create destination dir")
-	}
-
+func copyOneImage(srcRegistry, destRegistry registry.RegistryOptions, image string, appSlug string, reportWriter io.Writer, log *logger.Logger) ([]kustomizeimage.Image, error) {
 	policy, err := signature.NewPolicyFromBytes(imagePolicy)
 	if err != nil {
-		return errors.Wrap(err, "failed to read default policy")
+		return nil, errors.Wrap(err, "failed to read default policy")
 	}
 	policyContext, err := signature.NewPolicyContext(policy)
 	if err != nil {
-		return errors.Wrap(err, "failed to create policy")
-	}
-
-	destStr := fmt.Sprintf("%s:%s", imageFormat, archiveName)
-	destRef, err := alltransports.ParseImageName(destStr)
-	if err != nil {
-		return errors.Wrapf(err, "failed to parse local image name: %s", destStr)
+		return nil, errors.Wrap(err, "failed to create policy")
 	}
 
 	sourceCtx := &types.SystemContext{}
 
 	isPrivate, err := isPrivateImage(image)
 	if err != nil {
-		return errors.Wrapf(err, "failed to check if image is private: %s", image)
+		return nil, errors.Wrap(err, "failed to check if image is private")
 	}
 
 	sourceImage := image
@@ -254,14 +243,27 @@ func saveOneImage(srcRegistry registry.RegistryOptions, imagesDir string, image
 		}
 		rewritten, err := rewritePrivateImage(srcRegistry, image, appSlug)
 		if err != nil {
-			return errors.Wrapf(err, "failed to rewrite private image %s", image)
+			return nil, errors.Wrap(err, "failed to rewrite private image")
 		}
 
 		sourceImage = rewritten
 	}
 	srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", sourceImage))
 	if err != nil {
-		return errors.Wrap(err, "failed to parse source image name")
+		return nil, errors.Wrapf(err, "failed to parse source image name %s", sourceImage)
+	}
+
+	destCtx := &types.SystemContext{
+		DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
+	}
+	destCtx.DockerAuthConfig = &types.DockerAuthConfig{
+		Username: destRegistry.Username,
+		Password: destRegistry.Password,
+	}
+
+	destRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", DestRef(destRegistry, image)))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to parse dest image name %s", DestRef(destRegistry, image))
 	}
 
 	_, err = copy.Image(context.Background(), policyContext, destRef, srcRef, &copy.Options{
@@ -269,14 +271,57 @@ func saveOneImage(srcRegistry registry.RegistryOptions, imagesDir string, image
 		SignBy:                "",
 		ReportWriter:          reportWriter,
 		SourceCtx:             sourceCtx,
-		DestinationCtx:        nil,
+		DestinationCtx:        destCtx,
 		ForceManifestMIMEType: "",
 	})
 	if err != nil {
-		return errors.Wrap(err, "failed to copy image")
+		log.Info("failed to copy image directly with error %q, attempting fallback transfer method", err.Error())
+		// direct image copy failed
+		// attempt to download image to a temp directory, and then upload it from there
+		// this implicitly causes an image format conversion
+
+		// make a temp directory
+		tempDir, err := ioutil.TempDir("", "temp-image-pull")
+		if err != nil {
+			return nil, errors.Wrapf(err, "temp directory %s not created", tempDir)
+		}
+		defer os.RemoveAll(tempDir)
+
+		destPath := path.Join(tempDir, "temp-archive-image")
+		destStr := fmt.Sprintf("docker-archive:%s", destPath)
+		localRef, err := alltransports.ParseImageName(destStr)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to parse local image name: %s", destStr)
+		}
+
+		// copy image from remote to local
+		_, err = copy.Image(context.Background(), policyContext, localRef, srcRef, &copy.Options{
+			RemoveSignatures:      true,
+			SignBy:                "",
+			ReportWriter:          reportWriter,
+			SourceCtx:             sourceCtx,
+			DestinationCtx:        nil,
+			ForceManifestMIMEType: "",
+		})
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to download image")
+		}
+
+		// copy image from local to remote
+		_, err = copy.Image(context.Background(), policyContext, destRef, localRef, &copy.Options{
+			RemoveSignatures:      true,
+			SignBy:                "",
+			ReportWriter:          reportWriter,
+			SourceCtx:             nil,
+			DestinationCtx:        destCtx,
+			ForceManifestMIMEType: "",
+		})
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to push image")
+		}
 	}
 
-	return nil
+	return buildImageAlts(destRegistry, image)
 }
 
 func imageRefImage(image string) (*ImageRef, error) {
diff --git a/pkg/image/image.go b/pkg/image/image.go
index bb54fa246..45a610602 100644
--- a/pkg/image/image.go
+++ b/pkg/image/image.go
@@ -3,6 +3,7 @@ package image
 import (
 	"fmt"
 	"path"
+	"strings"
 
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/pkg/docker/registry"
@@ -43,3 +44,77 @@ func ImageInfoFromFile(registry registry.RegistryOptions, nameParts []string) (k
 
 	return image, nil
 }
+
+// DestRef returns the location to push the image to on the dest registry
+func DestRef(registry registry.RegistryOptions, srcImage string) string {
+	imageParts := strings.Split(srcImage, "/")
+	lastPart := imageParts[len(imageParts)-1]
+
+	image := fmt.Sprintf("%s/%s/%s", registry.Endpoint, registry.Namespace, lastPart)
+	return image
+}
+
+// stripImageTag removes the tag or digest from an image
+func stripImageTag(image string) string {
+	// grab last section of image name
+	imageParts := strings.Split(image, "/")
+	lastPart := imageParts[len(imageParts)-1]
+
+	// strip tag (like 'img:tag') or digest (like 'img@sha256:sha')
+	lastPart = strings.Split(lastPart, "@")[0]
+	lastPart = strings.Split(lastPart, ":")[0]
+	imageParts[len(imageParts)-1] = lastPart
+
+	// rejoin parts of image name
+	image = strings.Join(imageParts, "/")
+	return image
+}
+
+// destImageName returns the name of the image on the dest registry (without tag or digest)
+func destImageName(registry registry.RegistryOptions, srcImage string) string {
+	imageParts := strings.Split(srcImage, "/")
+	lastPart := imageParts[len(imageParts)-1]
+	lastPart = stripImageTag(lastPart)
+
+	image := fmt.Sprintf("%s/%s/%s", registry.Endpoint, registry.Namespace, lastPart)
+	return image
+}
+
+func buildImageAlts(destRegistry registry.RegistryOptions, image string) ([]kustomizeimage.Image, error) {
+	imgParts := strings.Split(image, "/")
+	if len(imgParts) == 1 {
+		// this means the image is something like "redis", which refers to "docker.io/library/redis"
+		imgParts = append([]string{"docker.io", "library"}, imgParts...)
+	} else if len(imgParts) == 2 {
+		// this means the image is something like "kotsadm/kotsadm-api", which refers to "docker.io/kotsadm/kotsadm-api"
+		imgParts = append([]string{"docker.io"}, imgParts...)
+	}
+
+	// if the last substring doesn't contain ':', it is untagged and needs 'latest' appended
+	// otherwise, split it on '@' and then ':'
+	if strings.Contains(imgParts[len(imgParts)-1], ":") {
+		imgParts = append(imgParts[:len(imgParts)-1], strings.Split(imgParts[len(imgParts)-1], "@")...)
+		imgParts = append(imgParts[:len(imgParts)-1], strings.Split(imgParts[len(imgParts)-1], ":")...)
+	} else {
+		imgParts = append(imgParts, "latest")
+	}
+
+	imageInfo, err := ImageInfoFromFile(destRegistry, imgParts)
+	if err != nil {
+		return nil, errors.Wrap(err, "get image info")
+	}
+
+	newName := destImageName(destRegistry, image)
+	imageWithoutTag := stripImageTag(image)
+
+	var newImages []kustomizeimage.Image
+	firstImage := kustomizeimage.Image{
+		Name:    imageWithoutTag,
+		NewName: newName,
+		NewTag:  imageInfo.NewTag,
+		Digest:  imageInfo.Digest,
+	}
+	newImages = append(newImages, firstImage)
+
+	return newImages, nil
+}
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index 19dfc6d09..9ccb0c4ec 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -184,9 +184,21 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 				writeUpstreamImageOptions.SourceRegistry.Username = fetchOptions.License.Spec.LicenseID
 				writeUpstreamImageOptions.SourceRegistry.Password = fetchOptions.License.Spec.LicenseID
 			}
-			if err := u.WriteUpstreamImages(writeUpstreamImageOptions); err != nil {
+
+			if pullOptions.RewriteImageOptions.Host != "" {
+				writeUpstreamImageOptions.DestRegistry = registry.RegistryOptions{
+					Endpoint:  pullOptions.RewriteImageOptions.Host,
+					Namespace: pullOptions.RewriteImageOptions.Namespace,
+					Username:  pullOptions.RewriteImageOptions.Username,
+					Password:  pullOptions.RewriteImageOptions.Password,
+				}
+			}
+
+			newImages, err := u.CopyUpstreamImages(writeUpstreamImageOptions)
+			if err != nil {
 				return "", errors.Wrap(err, "failed to write upstream images")
 			}
+			images = newImages
 		}
 
 		// If the request includes a rewrite image options host name, then also
@@ -213,9 +225,14 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 				pushUpstreamImageOptions.ReplicatedRegistry.Username = fetchOptions.License.Spec.LicenseID
 				pushUpstreamImageOptions.ReplicatedRegistry.Password = fetchOptions.License.Spec.LicenseID
 			}
-			rewrittenImages, err := u.TagAndPushUpstreamImages(pushUpstreamImageOptions)
-			if err != nil {
-				return "", errors.Wrap(err, "failed to push upstream images")
+
+			// only run the TagAndPushImagesFromFiles code if the "copy directly" code hasn't already run
+			var rewrittenImages []image.Image
+			if images == nil {
+				rewrittenImages, err = u.TagAndPushUpstreamImages(pushUpstreamImageOptions)
+				if err != nil {
+					return "", errors.Wrap(err, "failed to push upstream images")
+				}
 			}
 
 			findObjectsOptions := upstream.FindObjectsWithImagesOptions{
@@ -246,7 +263,10 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 			if err != nil {
 				return "", errors.Wrap(err, "create pull secret")
 			}
-			images = rewrittenImages
+
+			if rewrittenImages != nil {
+				images = rewrittenImages
+			}
 			objects = affectedObjects
 		}
 	} else if fetchOptions.License != nil {
diff --git a/pkg/pull/signature.go b/pkg/pull/signature.go
index ab6e6cd75..1a9b44c99 100644
--- a/pkg/pull/signature.go
+++ b/pkg/pull/signature.go
@@ -123,10 +123,7 @@ func verifyLicenseData(outerLicense *kotsv1beta1.License, innerLicense *kotsv1be
 		return errors.New("\"LicenseType\" field has changed")
 	}
 	if outerLicense.Spec.IsAirgapSupported != innerLicense.Spec.IsAirgapSupported {
-		return errors.New("\"IsAirgapSupported\" field has changed")
-	}
-	if outerLicense.Spec.IsGitOpsSupported != innerLicense.Spec.IsGitOpsSupported {
-		return errors.New("\"IsGitOpsSupported\" field has changed")
+		return errors.New("\"isAirgapSupported\" field has changed")
 	}
 
 	// Check entitlements
diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go
index 63704d6b3..ae6f74bea 100644
--- a/pkg/rewrite/rewrite.go
+++ b/pkg/rewrite/rewrite.go
@@ -90,29 +90,7 @@ func Rewrite(rewriteOptions RewriteOptions) error {
 				Endpoint:      replicatedRegistryInfo.Registry,
 				ProxyEndpoint: replicatedRegistryInfo.Proxy,
 			},
-		}
-		if fetchOptions.License != nil {
-			writeUpstreamImageOptions.AppSlug = fetchOptions.License.Spec.AppSlug
-			writeUpstreamImageOptions.SourceRegistry.Username = fetchOptions.License.Spec.LicenseID
-			writeUpstreamImageOptions.SourceRegistry.Password = fetchOptions.License.Spec.LicenseID
-		}
-		if err := u.WriteUpstreamImages(writeUpstreamImageOptions); err != nil {
-			return errors.Wrap(err, "failed to write upstream images")
-		}
-
-		// If the request includes a rewrite image options host name, then also
-		// push the images
-		pushUpstreamImageOptions := upstream.PushUpstreamImageOptions{
-			RootDir:      rewriteOptions.RootDir,
-			ImagesDir:    imagesDirFromOptions(u, rewriteOptions),
-			CreateAppDir: rewriteOptions.CreateAppDir,
-			Log:          log,
-			ReplicatedRegistry: registry.RegistryOptions{
-				Endpoint:      replicatedRegistryInfo.Registry,
-				ProxyEndpoint: replicatedRegistryInfo.Proxy,
-			},
-			ReportWriter: rewriteOptions.ReportWriter,
-			DestinationRegistry: registry.RegistryOptions{
+			DestRegistry: registry.RegistryOptions{
 				Endpoint:  rewriteOptions.RegistryEndpoint,
 				Namespace: rewriteOptions.RegistryNamespace,
 				Username:  rewriteOptions.RegistryUsername,
@@ -120,14 +98,16 @@ func Rewrite(rewriteOptions RewriteOptions) error {
 			},
 		}
 		if fetchOptions.License != nil {
-			pushUpstreamImageOptions.ReplicatedRegistry.Username = fetchOptions.License.Spec.LicenseID
-			pushUpstreamImageOptions.ReplicatedRegistry.Password = fetchOptions.License.Spec.LicenseID
+			writeUpstreamImageOptions.AppSlug = fetchOptions.License.Spec.AppSlug
+			writeUpstreamImageOptions.SourceRegistry.Username = fetchOptions.License.Spec.LicenseID
+			writeUpstreamImageOptions.SourceRegistry.Password = fetchOptions.License.Spec.LicenseID
 		}
-		rewrittenImages, err := u.TagAndPushUpstreamImages(pushUpstreamImageOptions)
+
+		newImages, err := u.CopyUpstreamImages(writeUpstreamImageOptions)
 		if err != nil {
-			return errors.Wrap(err, "failed to push upstream images")
+			return errors.Wrap(err, "failed to write upstream images")
 		}
-		images = rewrittenImages
+		images = newImages
 	}
 
 	findObjectsOptions := upstream.FindObjectsWithImagesOptions{
diff --git a/pkg/upstream/write_images.go b/pkg/upstream/write_images.go
index cf0e1d851..ee202ab1a 100644
--- a/pkg/upstream/write_images.go
+++ b/pkg/upstream/write_images.go
@@ -2,13 +2,13 @@ package upstream
 
 import (
 	"io"
-	"os"
 	"path"
 
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/image"
 	"github.com/replicatedhq/kots/pkg/logger"
+	kustomizeimage "sigs.k8s.io/kustomize/v3/pkg/image"
 )
 
 type WriteUpstreamImageOptions struct {
@@ -16,28 +16,22 @@ type WriteUpstreamImageOptions struct {
 	CreateAppDir   bool
 	AppSlug        string
 	SourceRegistry registry.RegistryOptions
+	DestRegistry   registry.RegistryOptions
 	Log            *logger.Logger
 	ReportWriter   io.Writer
 }
 
-func (u *Upstream) WriteUpstreamImages(options WriteUpstreamImageOptions) error {
+func (u *Upstream) CopyUpstreamImages(options WriteUpstreamImageOptions) ([]kustomizeimage.Image, error) {
 	rootDir := options.RootDir
 	if options.CreateAppDir {
 		rootDir = path.Join(rootDir, u.Name)
 	}
 	upstreamDir := path.Join(rootDir, "upstream")
-	imagesDir := path.Join(rootDir, "images")
 
-	_, err := os.Stat(imagesDir)
-	if err == nil {
-		if err := os.RemoveAll(imagesDir); err != nil {
-			return errors.Wrap(err, "failed to remove existing images")
-		}
+	newImages, err := image.CopyImages(options.SourceRegistry, options.DestRegistry, options.AppSlug, options.Log, options.ReportWriter, upstreamDir)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to save images")
 	}
 
-	if err := image.SaveImages(options.SourceRegistry, options.AppSlug, options.Log, options.ReportWriter, imagesDir, upstreamDir); err != nil {
-		return errors.Wrap(err, "failed to save images")
-	}
-
-	return nil
+	return newImages, nil
 }


Test patch:
diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go
index 7770c8330..23b965efb 100644
--- a/pkg/image/image_test.go
+++ b/pkg/image/image_test.go
@@ -6,7 +6,9 @@ import (
 
 	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"sigs.k8s.io/kustomize/v3/pkg/image"
+	kustomizeimage "sigs.k8s.io/kustomize/v3/pkg/image"
 )
 
 func Test_ImageNameFromNameParts(t *testing.T) {
@@ -74,3 +76,228 @@ func Test_ImageNameFromNameParts(t *testing.T) {
 		})
 	}
 }
+
+func TestDestRef(t *testing.T) {
+	registryOps := registry.RegistryOptions{
+		Endpoint:  "localhost:5000",
+		Namespace: "somebigbank",
+	}
+
+	type args struct {
+		registry registry.RegistryOptions
+		srcImage string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "ECR style image",
+			args: args{
+				registry: registryOps,
+				srcImage: "411111111111.dkr.ecr.us-west-1.amazonaws.com/myrepo:v0.0.1",
+			},
+			want: fmt.Sprintf("%s/%s/myrepo:v0.0.1", registryOps.Endpoint, registryOps.Namespace),
+		},
+		{
+			name: "Quay image with tag",
+			args: args{
+				registry: registryOps,
+				srcImage: "quay.io/someorg/debian:0.1",
+			},
+			want: fmt.Sprintf("%s/%s/debian:0.1", registryOps.Endpoint, registryOps.Namespace),
+		},
+		{
+			name: "Quay image with digest",
+			args: args{
+				registry: registryOps,
+				srcImage: "quay.io/someorg/debian@sha256:mytestdigest",
+			},
+			want: fmt.Sprintf("%s/%s/debian@sha256:mytestdigest", registryOps.Endpoint, registryOps.Namespace),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := DestRef(tt.args.registry, tt.args.srcImage); got != tt.want {
+				t.Errorf("DestImageName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_buildImageAlts(t *testing.T) {
+	tests := []struct {
+		name         string
+		destRegistry registry.RegistryOptions
+		image        string
+		want         []kustomizeimage.Image
+	}{
+		{
+			name: "naked image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "redis",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "latest",
+					Digest:  "",
+				},
+			},
+		},
+		{
+			name: "naked tagged image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "redis:v1",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "v1",
+					Digest:  "",
+				},
+			},
+		},
+		{
+			name: "naked contentAddressableSha image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somesmallcorp",
+			},
+			image: "redis@sha256:mytestdigest",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "redis",
+					NewName: "localhost:5000/somesmallcorp/redis",
+					NewTag:  "",
+					Digest:  "mytestdigest",
+				},
+			},
+		},
+		{
+			name: "tagged image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "library/redis:v1",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "library/redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "v1",
+					Digest:  "",
+				},
+			},
+		},
+		{
+			name: "quay.io tagged image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "quay.io/library/redis:v1",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "quay.io/library/redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "v1",
+					Digest:  "",
+				},
+			},
+		},
+		{
+			name: "ported registry tagged image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "example.com:5000/library/redis:v1",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "example.com:5000/library/redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "v1",
+					Digest:  "",
+				},
+			},
+		},
+		{
+			name: "ported registry untagged image",
+			destRegistry: registry.RegistryOptions{
+				Endpoint:  "localhost:5000",
+				Namespace: "somebigbank",
+			},
+			image: "example.com:5000/library/redis",
+			want: []kustomizeimage.Image{
+				{
+					Name:    "example.com:5000/library/redis",
+					NewName: "localhost:5000/somebigbank/redis",
+					NewTag:  "latest",
+					Digest:  "",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := require.New(t)
+			got, err := buildImageAlts(tt.destRegistry, tt.image)
+			req.NoError(err)
+			req.Equal(tt.want, got)
+		})
+	}
+}
+
+func Test_stripImageTag(t *testing.T) {
+	tests := []struct {
+		name  string
+		image string
+		want  string
+	}{
+		{
+			name:  "untagged image",
+			image: "myimage",
+			want:  "myimage",
+		},
+		{
+			name:  "untagged image on ported registry",
+			image: "example.com:5000/myimage",
+			want:  "example.com:5000/myimage",
+		},
+		{
+			name:  "tagged image",
+			image: "myimage:abc",
+			want:  "myimage",
+		},
+		{
+			name:  "tagged image on ported registry",
+			image: "example.com:5000/myimage:abc",
+			want:  "example.com:5000/myimage",
+		},
+		{
+			name:  "digest image",
+			image: "myimage@sha256:abc",
+			want:  "myimage",
+		},
+		{
+			name:  "digest image on ported registry",
+			image: "example.com:5000/myimage@sha256:abc",
+			want:  "example.com:5000/myimage",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := stripImageTag(tt.image); got != tt.want {
+				t.Errorf("stripImageTag() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}