Repository: replicatedhq/kots
Pull request #113 (2019-10-29 16:00:17)

Support private images with kots pull command


Patch:
diff --git a/pkg/docker/registry/images.go b/pkg/docker/registry/images.go
new file mode 100644
index 000000000..afadd1599
--- /dev/null
+++ b/pkg/docker/registry/images.go
@@ -0,0 +1,11 @@
+package registry
+
+import (
+	"strings"
+)
+
+func MakeProxiedImageURL(proxyHost string, appSlug string, image string) string {
+	// TODO: host with a port breaks this
+	parts := strings.Split(image, ":")
+	return strings.Join([]string{proxyHost, "proxy", appSlug, parts[0]}, "/")
+}
diff --git a/pkg/docker/registry/types.go b/pkg/docker/registry/types.go
new file mode 100644
index 000000000..1a23305a5
--- /dev/null
+++ b/pkg/docker/registry/types.go
@@ -0,0 +1,9 @@
+package registry
+
+type RegistryOptions struct {
+	Endpoint      string
+	ProxyEndpoint string
+	Namespace     string
+	Username      string
+	Password      string
+}
diff --git a/pkg/image/builder.go b/pkg/image/builder.go
index 6e2c7cc89..fa5a95b28 100644
--- a/pkg/image/builder.go
+++ b/pkg/image/builder.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/containers/image/copy"
 	imagedocker "github.com/containers/image/docker"
+	dockerref "github.com/containers/image/docker/reference"
 	"github.com/containers/image/signature"
 	"github.com/containers/image/transports/alltransports"
 	"github.com/containers/image/types"
@@ -41,7 +42,7 @@ type RegistryAuth struct {
 	Password string
 }
 
-func SaveImages(log *logger.Logger, imagesDir string, upstreamDir string) error {
+func SaveImages(srcRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, imagesDir string, upstreamDir string) error {
 	savedImages := make(map[string]bool)
 
 	err := filepath.Walk(upstreamDir,
@@ -59,7 +60,7 @@ func SaveImages(log *logger.Logger, imagesDir string, upstreamDir string) error
 				return err
 			}
 
-			err = saveImagesFromFile(log, imagesDir, contents, savedImages)
+			err = saveImagesToFiles(srcRegistry, appSlug, log, imagesDir, contents, savedImages)
 			if err != nil {
 				return errors.Wrap(err, "failed to extract images")
 			}
@@ -161,7 +162,7 @@ func GetObjectsWithImages(upstreamDir string) ([]*k8sdoc.Doc, error) {
 	return objects, nil
 }
 
-func saveImagesFromFile(log *logger.Logger, imagesDir string, fileData []byte, savedImages map[string]bool) error {
+func saveImagesToFiles(srcRegistry registry.RegistryOptions, appSlug string, log *logger.Logger, imagesDir string, fileData []byte, savedImages map[string]bool) error {
 	err := listImagesInFile(fileData, func(images []string, doc *k8sdoc.Doc) error {
 		for _, image := range images {
 			if _, saved := savedImages[image]; saved {
@@ -169,7 +170,7 @@ func saveImagesFromFile(log *logger.Logger, imagesDir string, fileData []byte, s
 			}
 
 			log.ChildActionWithSpinner("Pulling image %s", image)
-			err := saveOneImage(imagesDir, image)
+			err := saveOneImage(srcRegistry, imagesDir, image, appSlug)
 			if err != nil {
 				log.FinishChildSpinner()
 				return errors.Wrap(err, "failed to save image")
@@ -208,7 +209,7 @@ func listImagesInFile(contents []byte, handler processImagesFunc) error {
 	return nil
 }
 
-func saveOneImage(imagesDir string, image string) error {
+func saveOneImage(srcRegistry registry.RegistryOptions, imagesDir string, image string, appSlug string) error {
 	imageRef, err := imageRefImage(image)
 	if err != nil {
 		return errors.Wrap(err, "failed to parse image ref")
@@ -232,22 +233,42 @@ func saveOneImage(imagesDir string, image string) error {
 		return errors.Wrap(err, "failed to create policy")
 	}
 
-	srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image))
-	if err != nil {
-		return errors.Wrap(err, "failed to parse source image name")
-	}
-
 	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)
 	}
 
+	sourceCtx := &types.SystemContext{}
+
+	isPrivate, err := isPrivateImage(image)
+	if err != nil {
+		return errors.Wrapf(err, "failed to check if image is private: %s", image)
+	}
+
+	sourceImage := image
+	if isPrivate {
+		sourceCtx.DockerAuthConfig = &types.DockerAuthConfig{
+			Username: srcRegistry.Username,
+			Password: srcRegistry.Password,
+		}
+		rewritten, err := rewritePrivateImage(srcRegistry, image, appSlug)
+		if err != nil {
+			return errors.Wrapf(err, "failed to rewrite private image %s", 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")
+	}
+
 	_, err = copy.Image(context.Background(), policyContext, destRef, srcRef, &copy.Options{
 		RemoveSignatures:      true,
 		SignBy:                "",
 		ReportWriter:          nil,
-		SourceCtx:             nil,
+		SourceCtx:             sourceCtx,
 		DestinationCtx:        nil,
 		ForceManifestMIMEType: "",
 	})
@@ -370,10 +391,32 @@ func isPrivateImage(image string) (bool, error) {
 	if !isUnauthorized(err) {
 		return false, errors.Wrapf(err, "failed to create image from ref:%s", image)
 	}
-
 	return true, nil
 }
 
+func rewritePrivateImage(srcRegistry registry.RegistryOptions, image string, appSlug string) (string, error) {
+	ref, err := imagedocker.ParseReference(fmt.Sprintf("//%s", image))
+	if err != nil {
+		return "", errors.Wrapf(err, "failed to parse image ref:%s", image)
+	}
+
+	registryHost := dockerref.Domain(ref.DockerReference())
+	if registryHost == srcRegistry.Endpoint {
+		// replicated images are also private, but we don't rewrite those
+		return image, nil
+	}
+
+	newImage := registry.MakeProxiedImageURL(srcRegistry.ProxyEndpoint, appSlug, image)
+	if tagged, ok := ref.DockerReference().(dockerref.Tagged); ok {
+		return newImage + ":" + tagged.Tag(), nil
+	} else if can, ok := ref.DockerReference().(reference.Canonical); ok {
+		return newImage + "@" + can.Digest().String(), nil
+	}
+
+	// no tag, so it will be "latest"
+	return newImage, nil
+}
+
 func isUnauthorized(err error) bool {
 	switch err := err.(type) {
 	case errcode.Errors:
diff --git a/pkg/image/image.go b/pkg/image/image.go
index 8087e29cd..bb54fa246 100644
--- a/pkg/image/image.go
+++ b/pkg/image/image.go
@@ -5,10 +5,11 @@ import (
 	"path"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/docker/registry"
 	kustomizeimage "sigs.k8s.io/kustomize/v3/pkg/image"
 )
 
-func ImageInfoFromFile(registryHost string, namespace string, nameParts []string) (kustomizeimage.Image, error) {
+func ImageInfoFromFile(registry registry.RegistryOptions, nameParts []string) (kustomizeimage.Image, error) {
 	// imageNameParts looks like this:
 	// ["quay.io", "someorg", "imagename", "imagetag"]
 	// or
@@ -17,11 +18,11 @@ func ImageInfoFromFile(registryHost string, namespace string, nameParts []string
 
 	image := kustomizeimage.Image{}
 
-	if len(nameParts) < 4 {
+	if len(nameParts) < 3 {
 		return image, errors.Errorf("not enough parts in image name: %v", nameParts)
 	}
 
-	newImageNameParts := []string{registryHost, namespace}
+	newImageNameParts := []string{registry.Endpoint, registry.Namespace}
 	var originalName, tag, separator string
 	if nameParts[len(nameParts)-2] == "sha256" {
 		newImageNameParts = append(newImageNameParts, nameParts[len(nameParts)-3])
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index 8ebe68fc5..0ddde35ce 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -139,16 +139,28 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 	}
 	log.FinishSpinner()
 
+	replicatedRegistryInfo := registryProxyEndpointFromLicense(fetchOptions.License)
+
 	var pullSecret *corev1.Secret
 	var images []image.Image
 	var objects []*k8sdoc.Doc
 	if pullOptions.RewriteImages {
+
 		// Rewrite all images
 		if pullOptions.RewriteImageOptions.ImageFiles == "" {
 			writeUpstreamImageOptions := upstream.WriteUpstreamImageOptions{
 				RootDir:      pullOptions.RootDir,
 				CreateAppDir: pullOptions.CreateAppDir,
 				Log:          log,
+				SourceRegistry: registry.RegistryOptions{
+					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")
@@ -159,15 +171,25 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		// push the images
 		if pullOptions.RewriteImageOptions.Host != "" {
 			pushUpstreamImageOptions := upstream.PushUpstreamImageOptions{
-				RootDir:           pullOptions.RootDir,
-				ImagesDir:         imagesDirFromOptions(u, pullOptions),
-				CreateAppDir:      pullOptions.CreateAppDir,
-				Log:               log,
-				ReportWriter:      pullOptions.ReportWriter,
-				RegistryHost:      pullOptions.RewriteImageOptions.Host,
-				RegistryNamespace: pullOptions.RewriteImageOptions.Namespace,
-				Username:          pullOptions.RewriteImageOptions.Username,
-				Password:          pullOptions.RewriteImageOptions.Password,
+				RootDir:      pullOptions.RootDir,
+				ImagesDir:    imagesDirFromOptions(u, pullOptions),
+				CreateAppDir: pullOptions.CreateAppDir,
+				Log:          log,
+				ReplicatedRegistry: registry.RegistryOptions{
+					Endpoint:      replicatedRegistryInfo.Registry,
+					ProxyEndpoint: replicatedRegistryInfo.Proxy,
+				},
+				ReportWriter: pullOptions.ReportWriter,
+				DestinationRegistry: registry.RegistryOptions{
+					Endpoint:  pullOptions.RewriteImageOptions.Host,
+					Namespace: pullOptions.RewriteImageOptions.Namespace,
+					Username:  pullOptions.RewriteImageOptions.Username,
+					Password:  pullOptions.RewriteImageOptions.Password,
+				},
+			}
+			if fetchOptions.License != nil {
+				pushUpstreamImageOptions.ReplicatedRegistry.Username = fetchOptions.License.Spec.LicenseID
+				pushUpstreamImageOptions.ReplicatedRegistry.Password = fetchOptions.License.Spec.LicenseID
 			}
 			rewrittenImages, err := u.TagAndPushUpstreamImages(pushUpstreamImageOptions)
 			if err != nil {
@@ -206,15 +228,17 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 			objects = affectedObjects
 		}
 	} else if fetchOptions.License != nil {
+
 		// Rewrite private images
-		registryInfo := registryProxyEndpointFromLicense(fetchOptions.License)
 		findPrivateImagesOptions := upstream.FindPrivateImagesOptions{
-			RootDir:                 pullOptions.RootDir,
-			CreateAppDir:            pullOptions.CreateAppDir,
-			AppSlug:                 fetchOptions.License.Spec.AppSlug,
-			ReplicatedRegistry:      registryInfo.Registry,
-			ReplicatedRegistryProxy: registryInfo.Proxy,
-			Log:                     log,
+			RootDir:      pullOptions.RootDir,
+			CreateAppDir: pullOptions.CreateAppDir,
+			AppSlug:      fetchOptions.License.Spec.AppSlug,
+			ReplicatedRegistry: registry.RegistryOptions{
+				Endpoint:      replicatedRegistryInfo.Registry,
+				ProxyEndpoint: replicatedRegistryInfo.Proxy,
+			},
+			Log: log,
 		}
 		rewrittenImages, affectedObjects, err := u.FindPrivateImages(findPrivateImagesOptions)
 		if err != nil {
@@ -225,7 +249,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		// We still need to create the secret in that case.
 		if len(affectedObjects) > 0 {
 			pullSecret, err = pullSecretForRegistries(
-				registryInfo.ToSlice(),
+				replicatedRegistryInfo.ToSlice(),
 				fetchOptions.License.Spec.LicenseID,
 				fetchOptions.License.Spec.LicenseID,
 				pullOptions.Namespace,
@@ -341,6 +365,10 @@ func registryProxyEndpointFromLicense(license *kotsv1beta1.License) *registryInf
 		Proxy:    "proxy.replicated.com",
 	}
 
+	if license == nil {
+		return defaultInfo
+	}
+
 	u, err := url.Parse(license.Spec.Endpoint)
 	if err != nil {
 		return defaultInfo
diff --git a/pkg/upstream/push_images.go b/pkg/upstream/push_images.go
index 10301eb0f..80a8d17ca 100644
--- a/pkg/upstream/push_images.go
+++ b/pkg/upstream/push_images.go
@@ -9,21 +9,20 @@ import (
 	"strings"
 
 	"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 PushUpstreamImageOptions struct {
-	RootDir           string
-	ImagesDir         string
-	CreateAppDir      bool
-	Log               *logger.Logger
-	ReportWriter      io.Writer
-	RegistryHost      string
-	RegistryNamespace string
-	Username          string
-	Password          string
+	RootDir             string
+	ImagesDir           string
+	CreateAppDir        bool
+	Log                 *logger.Logger
+	ReplicatedRegistry  registry.RegistryOptions
+	ReportWriter        io.Writer
+	DestinationRegistry registry.RegistryOptions
 }
 
 func (u *Upstream) TagAndPushUpstreamImages(options PushUpstreamImageOptions) ([]kustomizeimage.Image, error) {
@@ -56,7 +55,7 @@ func (u *Upstream) TagAndPushUpstreamImages(options PushUpstreamImageOptions) ([
 
 				pathWithoutRoot := path[len(formatRoot)+1:]
 
-				rewrittenImage, err := image.ImageInfoFromFile(options.RegistryHost, options.RegistryNamespace, strings.Split(pathWithoutRoot, string(os.PathSeparator)))
+				rewrittenImage, err := image.ImageInfoFromFile(options.DestinationRegistry, strings.Split(pathWithoutRoot, string(os.PathSeparator)))
 				if err != nil {
 					return errors.Wrap(err, "failed to decode image from path")
 				}
@@ -64,7 +63,10 @@ func (u *Upstream) TagAndPushUpstreamImages(options PushUpstreamImageOptions) ([
 				// copy to the registry
 				options.Log.ChildActionWithSpinner("Pushing image %s:%s", rewrittenImage.NewName, rewrittenImage.NewTag)
 
-				registryAuth := image.RegistryAuth{Username: options.Username, Password: options.Password}
+				registryAuth := image.RegistryAuth{
+					Username: options.DestinationRegistry.Username,
+					Password: options.DestinationRegistry.Password,
+				}
 				err = image.CopyFromFileToRegistry(path, rewrittenImage.NewName, rewrittenImage.NewTag, rewrittenImage.Digest, registryAuth, options.ReportWriter)
 				if err != nil {
 					options.Log.FinishChildSpinner()
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index 7860ee079..51a0aa16b 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -17,10 +17,10 @@ import (
 
 	imagedocker "github.com/containers/image/docker"
 	dockerref "github.com/containers/image/docker/reference"
-	imagetypes "github.com/containers/image/types"
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	kotsscheme "github.com/replicatedhq/kots/kotskinds/client/kotsclientset/scheme"
+	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/image"
 	"github.com/replicatedhq/kots/pkg/k8sdoc"
 	"github.com/replicatedhq/kots/pkg/logger"
@@ -546,12 +546,11 @@ func getApplicationMetadataFromHost(host string, upstream *url.URL) ([]byte, err
 }
 
 type FindPrivateImagesOptions struct {
-	RootDir                 string
-	CreateAppDir            bool
-	AppSlug                 string
-	ReplicatedRegistry      string
-	ReplicatedRegistryProxy string
-	Log                     *logger.Logger
+	RootDir            string
+	CreateAppDir       bool
+	AppSlug            string
+	ReplicatedRegistry registry.RegistryOptions
+	Log                *logger.Logger
 }
 
 func (u *Upstream) FindPrivateImages(options FindPrivateImagesOptions) ([]kustomizeimage.Image, []*k8sdoc.Doc, error) {
@@ -575,14 +574,14 @@ func (u *Upstream) FindPrivateImages(options FindPrivateImagesOptions) ([]kustom
 		}
 
 		registryHost := dockerref.Domain(ref.DockerReference())
-		if registryHost == options.ReplicatedRegistry {
+		if registryHost == options.ReplicatedRegistry.Endpoint {
 			// replicated images are also private, but we don't rewrite those
 			continue
 		}
 
 		image := kustomizeimage.Image{
 			Name:    upstreamImage,
-			NewName: rewritePrivateImageURL(options.ReplicatedRegistryProxy, options.AppSlug, upstreamImage),
+			NewName: registry.MakeProxiedImageURL(options.ReplicatedRegistry.ProxyEndpoint, options.AppSlug, upstreamImage),
 		}
 		result = append(result, image)
 	}
@@ -610,12 +609,3 @@ func (u *Upstream) FindObjectsWithImages(options FindObjectsWithImagesOptions) (
 
 	return objects, nil
 }
-
-func rewritePrivateImageURL(proxyHost string, appSlug string, image string) string {
-	parts := strings.Split(image, ":")
-	return strings.Join([]string{proxyHost, "proxy", appSlug, parts[0]}, "/")
-}
-
-func parseDockerRef(image string) (imagetypes.ImageReference, error) {
-	return imagedocker.ParseReference(fmt.Sprintf("//%s", image))
-}
diff --git a/pkg/upstream/write_images.go b/pkg/upstream/write_images.go
index bcc065aa3..326330a38 100644
--- a/pkg/upstream/write_images.go
+++ b/pkg/upstream/write_images.go
@@ -5,14 +5,17 @@ import (
 	"path"
 
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/image"
 	"github.com/replicatedhq/kots/pkg/logger"
 )
 
 type WriteUpstreamImageOptions struct {
-	RootDir      string
-	CreateAppDir bool
-	Log          *logger.Logger
+	RootDir        string
+	CreateAppDir   bool
+	AppSlug        string
+	SourceRegistry registry.RegistryOptions
+	Log            *logger.Logger
 }
 
 func (u *Upstream) WriteUpstreamImages(options WriteUpstreamImageOptions) error {
@@ -30,7 +33,7 @@ func (u *Upstream) WriteUpstreamImages(options WriteUpstreamImageOptions) error
 		}
 	}
 
-	if err := image.SaveImages(options.Log, imagesDir, upstreamDir); err != nil {
+	if err := image.SaveImages(options.SourceRegistry, options.AppSlug, options.Log, imagesDir, upstreamDir); err != nil {
 		return errors.Wrap(err, "failed to save images")
 	}
 


Test patch:
diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go
index da7091ec5..7770c8330 100644
--- a/pkg/image/image_test.go
+++ b/pkg/image/image_test.go
@@ -4,13 +4,16 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/stretchr/testify/assert"
 	"sigs.k8s.io/kustomize/v3/pkg/image"
 )
 
 func Test_ImageNameFromNameParts(t *testing.T) {
-	registry := "localhost:5000"
-	namespace := "somebigbank"
+	registryOps := registry.RegistryOptions{
+		Endpoint:  "localhost:5000",
+		Namespace: "somebigbank",
+	}
 
 	tests := []struct {
 		name     string
@@ -20,16 +23,27 @@ func Test_ImageNameFromNameParts(t *testing.T) {
 	}{
 		{
 			name:     "bad name format",
-			parts:    []string{"quay.io", "debian", "latest"},
+			parts:    []string{"quay.io", "latest"},
 			expected: image.Image{},
 			isError:  true,
 		},
+		{
+			name:  "ECR style image",
+			parts: []string{"411111111111.dkr.ecr.us-west-1.amazonaws.com", "myrepo", "v0.0.1"},
+			expected: image.Image{
+				Name:    "411111111111.dkr.ecr.us-west-1.amazonaws.com/myrepo:v0.0.1",
+				NewName: fmt.Sprintf("%s/%s/myrepo", registryOps.Endpoint, registryOps.Namespace),
+				NewTag:  "v0.0.1",
+				Digest:  "",
+			},
+			isError: false,
+		},
 		{
 			name:  "four parts with tag",
 			parts: []string{"quay.io", "someorg", "debian", "0.1"},
 			expected: image.Image{
 				Name:    "quay.io/someorg/debian:0.1",
-				NewName: fmt.Sprintf("%s/%s/debian", registry, namespace),
+				NewName: fmt.Sprintf("%s/%s/debian", registryOps.Endpoint, registryOps.Namespace),
 				NewTag:  "0.1",
 				Digest:  "",
 			},
@@ -40,7 +54,7 @@ func Test_ImageNameFromNameParts(t *testing.T) {
 			parts: []string{"quay.io", "someorg", "debian", "sha256", "1234567890abcdef"},
 			expected: image.Image{
 				Name:    "quay.io/someorg/debian@sha256:1234567890abcdef",
-				NewName: fmt.Sprintf("%s/%s/debian", registry, namespace),
+				NewName: fmt.Sprintf("%s/%s/debian", registryOps.Endpoint, registryOps.Namespace),
 				NewTag:  "",
 				Digest:  "1234567890abcdef",
 			},
@@ -50,7 +64,7 @@ func Test_ImageNameFromNameParts(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			image, err := ImageInfoFromFile(registry, namespace, test.parts)
+			image, err := ImageInfoFromFile(registryOps, test.parts)
 			if test.isError {
 				assert.Error(t, err)
 			} else {