diff --git a/pkg/apparchive/helm-v1beta2.go b/pkg/apparchive/helm-v1beta2.go index 2ed514929d..ab6bbde1f9 100644 --- a/pkg/apparchive/helm-v1beta2.go +++ b/pkg/apparchive/helm-v1beta2.go @@ -12,14 +12,12 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/base" "github.com/replicatedhq/kots/pkg/docker/registry" - dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" "github.com/replicatedhq/kots/pkg/image" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/midstream" upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/action" @@ -68,7 +66,7 @@ type WriteV1Beta2HelmChartsOptions struct { Upstream *upstreamtypes.Upstream WriteUpstreamOptions upstreamtypes.WriteOptions RenderOptions *base.RenderOptions - ProcessImageOptions image.ProcessImageOptions + ProcessImageOptions imagetypes.ProcessImageOptions KotsKinds *kotsutil.KotsKinds Clientset kubernetes.Interface } @@ -83,8 +81,6 @@ func WriteV1Beta2HelmCharts(opts WriteV1Beta2HelmChartsOptions) error { return nil } - checkedImages := []kotsv1beta1.InstallationImage{} - for _, v1Beta2Chart := range opts.KotsKinds.V1Beta2HelmCharts.Items { helmChart := v1Beta2Chart @@ -158,20 +154,9 @@ func WriteV1Beta2HelmCharts(opts WriteV1Beta2HelmChartsOptions) error { continue } - result, err := processV1Beta2HelmChartImages(opts, &helmChart, chartDir) - if err != nil { + if err := processOnlineV1Beta2HelmChartImages(opts, &helmChart, chartDir); err != nil { return errors.Wrap(err, "failed to process online images") } - - checkedImages = append(checkedImages, result.CheckedImages...) - } - - if len(checkedImages) > 0 { - opts.KotsKinds.Installation.Spec.KnownImages = append(opts.KotsKinds.Installation.Spec.KnownImages, checkedImages...) - - if err := kotsutil.SaveInstallation(&opts.KotsKinds.Installation, opts.Upstream.GetUpstreamDir(opts.WriteUpstreamOptions)); err != nil { - return errors.Wrap(err, "failed to save installation") - } } return nil @@ -183,7 +168,7 @@ type WriteRenderedV1Beta2HelmChartsOptions struct { Log *logger.CLILogger Downstreams []string KotsKinds *kotsutil.KotsKinds - ProcessImageOptions image.ProcessImageOptions + ProcessImageOptions imagetypes.ProcessImageOptions } // WriteRenderedV1Beta2HelmCharts writes the rendered v1beta2 helm charts to the rendered directory for diffing @@ -279,36 +264,36 @@ func templateV1Beta2HelmChartWithValuesToDir(helmChart *kotsv1beta2.HelmChart, c return nil } -func processV1Beta2HelmChartImages(opts WriteV1Beta2HelmChartsOptions, helmChart *kotsv1beta2.HelmChart, chartDir string) (*base.RewriteImagesResult, error) { +func processOnlineV1Beta2HelmChartImages(opts WriteV1Beta2HelmChartsOptions, helmChart *kotsv1beta2.HelmChart, chartDir string) error { // template the chart with the builder values to a temp dir and then process images tmpDir, err := os.MkdirTemp("", fmt.Sprintf("kots-images-%s", helmChart.GetDirName())) if err != nil { - return nil, errors.Wrap(err, "failed to create temp dir for image processing") + return errors.Wrap(err, "failed to create temp dir for image processing") } defer os.RemoveAll(tmpDir) builderHelmValues, err := helmChart.GetBuilderValues() if err != nil { - return nil, errors.Wrap(err, "failed to get builder values for chart") + return errors.Wrap(err, "failed to get builder values for chart") } builderValuesContent, err := yaml.Marshal(builderHelmValues) if err != nil { - return nil, errors.Wrap(err, "failed to marshal builder values") + return errors.Wrap(err, "failed to marshal builder values") } builderValuesPath := path.Join(tmpDir, "builder-values.yaml") if err := os.WriteFile(builderValuesPath, builderValuesContent, 0644); err != nil { - return nil, errors.Wrap(err, "failed to write builder values file") + return errors.Wrap(err, "failed to write builder values file") } templatedOutputDir := path.Join(tmpDir, helmChart.GetDirName()) if err := os.Mkdir(templatedOutputDir, 0755); err != nil { - return nil, errors.Wrap(err, "failed to create temp dir for image processing") + return errors.Wrap(err, "failed to create temp dir for image processing") } if err := templateV1Beta2HelmChartWithValuesToDir(helmChart, chartDir, builderValuesPath, templatedOutputDir, opts.RenderOptions.Log.Debug); err != nil { - return nil, errors.Wrap(err, "failed to template helm chart for image processing") + return errors.Wrap(err, "failed to template helm chart for image processing") } var dockerHubRegistryCreds registry.Credentials @@ -317,27 +302,24 @@ func processV1Beta2HelmChartImages(opts WriteV1Beta2HelmChartsOptions, helmChart dockerHubRegistryCreds, _ = registry.GetCredentialsForRegistryFromConfigJSON(dockerhubSecret.Data[".dockerconfigjson"], registry.DockerHubRegistryName) } - destRegistry := &dockerregistrytypes.RegistryOptions{ - Endpoint: opts.ProcessImageOptions.RegistrySettings.Hostname, - Namespace: opts.ProcessImageOptions.RegistrySettings.Namespace, - Username: opts.ProcessImageOptions.RegistrySettings.Username, - Password: opts.ProcessImageOptions.RegistrySettings.Password, - } - - baseImages, err := image.FindImagesInDir(templatedOutputDir) + chartImages, err := image.FindImagesInDir(templatedOutputDir) if err != nil { - return nil, errors.Wrap(err, "failed to find base images") + return errors.Wrap(err, "failed to find base images") } - kotsKindsImages, err := kotsutil.GetImagesFromKotsKinds(opts.KotsKinds, destRegistry) - if err != nil { - return nil, errors.Wrap(err, "failed to get images from kots kinds") + if err := image.UpdateInstallationImages(image.UpdateInstallationImagesOptions{ + Images: chartImages, + KotsKinds: opts.KotsKinds, + IsAirgap: opts.ProcessImageOptions.IsAirgap, + UpstreamDir: opts.Upstream.GetUpstreamDir(opts.WriteUpstreamOptions), + DockerHubRegistryCreds: dockerHubRegistryCreds, + }); err != nil { + return errors.Wrap(err, "failed to update installation images") } - result, err := midstream.RewriteBaseImages(opts.ProcessImageOptions, baseImages, kotsKindsImages, opts.KotsKinds, opts.KotsKinds.License, dockerHubRegistryCreds, opts.RenderOptions.Log) - if err != nil { - return nil, errors.Wrap(err, "failed to rewrite base images") + if err := image.CopyOnlineImages(opts.ProcessImageOptions, chartImages, opts.KotsKinds, opts.KotsKinds.License, dockerHubRegistryCreds, opts.RenderOptions.Log); err != nil { + return errors.Wrap(err, "failed to rewrite base images") } - return result, nil + return nil } diff --git a/pkg/archives/archives.go b/pkg/archives/archives.go index 8b445d1f51..e2ca26467c 100644 --- a/pkg/archives/archives.go +++ b/pkg/archives/archives.go @@ -28,6 +28,42 @@ func ExtractTGZArchiveFromFile(tgzFile string, destDir string) error { return nil } +func DirExistsInAirgap(dirToCheck string, archive string) (bool, error) { + fileReader, err := os.Open(archive) + if err != nil { + return false, errors.Wrap(err, "failed to open file") + } + defer fileReader.Close() + + gzipReader, err := gzip.NewReader(fileReader) + if err != nil { + return false, errors.Wrap(err, "failed to get new gzip reader") + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return false, errors.Wrap(err, "failed to get read archive") + } + + if header.Typeflag != tar.TypeDir { + continue + } + if header.Name != dirToCheck { + continue + } + + return true, nil + } + + return false, nil +} + func GetFileFromAirgap(fileToGet string, archive string) ([]byte, error) { fileReader, err := os.Open(archive) if err != nil { diff --git a/pkg/base/airgap.go b/pkg/base/airgap.go deleted file mode 100644 index df97089849..0000000000 --- a/pkg/base/airgap.go +++ /dev/null @@ -1,95 +0,0 @@ -package base - -import ( - "io" - - "github.com/pkg/errors" - registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" - "github.com/replicatedhq/kots/pkg/image" - imagetypes "github.com/replicatedhq/kots/pkg/image/types" - "github.com/replicatedhq/kots/pkg/imageutil" - "github.com/replicatedhq/kots/pkg/kotsutil" - "github.com/replicatedhq/kots/pkg/logger" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kustomizetypes "sigs.k8s.io/kustomize/api/types" -) - -type ProcessAirgapImagesOptions struct { - BaseImages []string - KotsKindsImages []string - RootDir string - AirgapRoot string - AirgapBundle string - CreateAppDir bool - PushImages bool - Log *logger.CLILogger - ReplicatedRegistry registrytypes.RegistryOptions - ReportWriter io.Writer - DestinationRegistry registrytypes.RegistryOptions - KotsKinds *kotsutil.KotsKinds -} - -type ProcessAirgapImagesResult struct { - KustomizeImages []kustomizetypes.Image - KnownImages []kotsv1beta1.InstallationImage -} - -func ProcessAirgapImages(opts ProcessAirgapImagesOptions) (*ProcessAirgapImagesResult, error) { - pushOpts := imagetypes.PushImagesOptions{ - Registry: opts.DestinationRegistry, - Log: opts.Log, - ProgressWriter: opts.ReportWriter, - LogForUI: true, - } - - if opts.PushImages { - if opts.AirgapBundle != "" { - err := image.TagAndPushAppImagesFromBundle(opts.AirgapBundle, pushOpts) - if err != nil { - return nil, errors.Wrap(err, "failed to push images from bundle") - } - } else { - err := image.TagAndPushAppImagesFromPath(opts.AirgapRoot, pushOpts) - if err != nil { - return nil, errors.Wrap(err, "failed to push images from dir") - } - } - } - - rewrittenImages := []kustomizetypes.Image{} - for _, image := range append(opts.BaseImages, opts.KotsKindsImages...) { - rewrittenImage, err := imageutil.RewriteDockerRegistryImage(opts.DestinationRegistry, image) - if err != nil { - return nil, errors.Wrapf(err, "failed to rewrite image %s", image) - } - rewrittenImages = append(rewrittenImages, *rewrittenImage) - } - - withAltNames := make([]kustomizetypes.Image, 0) - for _, i := range rewrittenImages { - altNames, err := imageutil.BuildImageAltNames(i) - if err != nil { - return nil, errors.Wrap(err, "failed to build image alt names") - } - withAltNames = append(withAltNames, altNames...) - } - - result := &ProcessAirgapImagesResult{ - KustomizeImages: withAltNames, - // This list is slightly different from the list we get from app specs because of alternative names, - // but it still works because after rewriting image names with private registry, the lists become the same. - KnownImages: installationImagesFromKustomizeImages(withAltNames), - } - return result, nil -} - -func installationImagesFromKustomizeImages(images []kustomizetypes.Image) []kotsv1beta1.InstallationImage { - result := make([]kotsv1beta1.InstallationImage, 0) - for _, i := range images { - result = append(result, kotsv1beta1.InstallationImage{ - Image: imageutil.SrcImageFromKustomizeImage(i), - IsPrivate: true, - }) - } - return result -} diff --git a/pkg/base/airgap_test.go b/pkg/base/airgap_test.go deleted file mode 100644 index 993a56b7d4..0000000000 --- a/pkg/base/airgap_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package base - -import ( - "reflect" - "testing" - - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kustomizetypes "sigs.k8s.io/kustomize/api/types" -) - -func Test_installationImagesFromKustomizeImages(t *testing.T) { - tests := []struct { - name string - images []kustomizetypes.Image - want []kotsv1beta1.InstallationImage - }{ - { - name: "preserves references", - images: []kustomizetypes.Image{ - { - Name: "registry.replicated.com/appslug/taggedimage", - NewTag: "v1.0.0", - }, - { - Name: "registry.replicated.com/appslug/digestimage", - Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - }, - { - Name: "registry.replicated.com/appslug/taganddigestimage", - NewTag: "v1.0.0", - Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - }, - }, - want: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/taggedimage:v1.0.0", - IsPrivate: true, - }, - { - Image: "registry.replicated.com/appslug/digestimage@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - IsPrivate: true, - }, - { - Image: "registry.replicated.com/appslug/taganddigestimage@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - IsPrivate: true, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := installationImagesFromKustomizeImages(tt.images); !reflect.DeepEqual(got, tt.want) { - t.Errorf("installationImagesFromKustomizeImages() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/base/base.go b/pkg/base/base.go index 734706b0a5..9d575f3f1f 100644 --- a/pkg/base/base.go +++ b/pkg/base/base.go @@ -5,9 +5,11 @@ import ( "fmt" "path" "path/filepath" + "sort" "strconv" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/k8sdoc" "github.com/replicatedhq/kots/pkg/kotsutil" kotsscheme "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" @@ -295,3 +297,45 @@ func PrependBaseFilesPath(files []BaseFile, prefix string) []BaseFile { } return next } + +func FindImages(b *Base) ([]string, []k8sdoc.K8sDoc, error) { + uniqueImages := make(map[string]bool) + objectsWithImages := make([]k8sdoc.K8sDoc, 0) // all objects where images are referenced from + + for _, file := range b.Files { + parsed, err := k8sdoc.ParseYAML(file.Content) + if err != nil { + continue + } + + images := parsed.ListImages() + if len(images) > 0 { + objectsWithImages = append(objectsWithImages, parsed) + } + + for _, image := range images { + uniqueImages[image] = true + } + } + + for _, subBase := range b.Bases { + subImages, subObjects, err := FindImages(&subBase) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to find images in sub base %s", subBase.Path) + } + + objectsWithImages = append(objectsWithImages, subObjects...) + + for _, subImage := range subImages { + uniqueImages[subImage] = true + } + } + + result := make([]string, 0, len(uniqueImages)) + for i := range uniqueImages { + result = append(result, i) + } + sort.Strings(result) // sort the images to get an ordered and reproducible output for easier testing + + return result, objectsWithImages, nil +} diff --git a/pkg/base/images.go b/pkg/base/images.go deleted file mode 100644 index d2c4383c31..0000000000 --- a/pkg/base/images.go +++ /dev/null @@ -1,152 +0,0 @@ -package base - -import ( - "sort" - "strings" - - dockerref "github.com/containers/image/v5/docker/reference" - "github.com/distribution/distribution/v3/reference" - "github.com/pkg/errors" - "github.com/replicatedhq/kots/pkg/docker/registry" - registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" - "github.com/replicatedhq/kots/pkg/image" - imagetypes "github.com/replicatedhq/kots/pkg/image/types" - "github.com/replicatedhq/kots/pkg/imageutil" - "github.com/replicatedhq/kots/pkg/k8sdoc" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kustomizeimage "sigs.k8s.io/kustomize/api/types" -) - -type FindPrivateImagesOptions struct { - BaseImages []string - KotsKindsImages []string - AppSlug string - ReplicatedRegistry registrytypes.RegistryOptions - DockerHubRegistry registrytypes.RegistryOptions - Installation *kotsv1beta1.Installation - AllImagesPrivate bool -} - -type FindPrivateImagesResult struct { - Images []kustomizeimage.Image // images to be rewritten - CheckedImages []kotsv1beta1.InstallationImage // all images found in the installation -} - -func FindImages(b *Base) ([]string, []k8sdoc.K8sDoc, error) { - uniqueImages := make(map[string]bool) - objectsWithImages := make([]k8sdoc.K8sDoc, 0) // all objects where images are referenced from - - for _, file := range b.Files { - parsed, err := k8sdoc.ParseYAML(file.Content) - if err != nil { - continue - } - - images := parsed.ListImages() - if len(images) > 0 { - objectsWithImages = append(objectsWithImages, parsed) - } - - for _, image := range images { - uniqueImages[image] = true - } - } - - for _, subBase := range b.Bases { - subImages, subObjects, err := FindImages(&subBase) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to find images in sub base %s", subBase.Path) - } - - objectsWithImages = append(objectsWithImages, subObjects...) - - for _, subImage := range subImages { - uniqueImages[subImage] = true - } - } - - result := make([]string, 0, len(uniqueImages)) - for i := range uniqueImages { - result = append(result, i) - } - sort.Strings(result) // sort the images to get an ordered and reproducible output for easier testing - - return result, objectsWithImages, nil -} - -func FindPrivateImages(opts FindPrivateImagesOptions) (*FindPrivateImagesResult, error) { - checkedImages := makeInstallationImageInfoMap(opts.Installation.Spec.KnownImages) - privateImages, err := image.GetPrivateImages(opts.BaseImages, opts.KotsKindsImages, checkedImages, opts.AllImagesPrivate, opts.DockerHubRegistry) - if err != nil { - return nil, errors.Wrap(err, "failed to list upstream images") - } - - kustomizeImages := make([]kustomizeimage.Image, 0) - for _, privateImage := range privateImages { - dockerRef, err := dockerref.ParseDockerRef(privateImage) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse docker ref %q", privateImage) - } - - registryHost := dockerref.Domain(dockerRef) - if registryHost == opts.ReplicatedRegistry.Endpoint { - // replicated images are also private, but we don't rewrite those - continue - } - - image := kustomizeimage.Image{} - if registryHost == opts.ReplicatedRegistry.UpstreamEndpoint { - // image is using the upstream replicated registry, but a custom registry domain is configured, so rewrite to use the custom domain - image = kustomizeimage.Image{ - Name: dockerRef.Name(), - NewName: strings.Replace(dockerRef.Name(), registryHost, opts.ReplicatedRegistry.Endpoint, 1), - } - } else { - // all other private images are rewritten to use the replicated proxy - image = kustomizeimage.Image{ - Name: dockerRef.Name(), - NewName: registry.MakeProxiedImageURL(opts.ReplicatedRegistry.ProxyEndpoint, opts.AppSlug, privateImage), - } - } - - if can, ok := dockerRef.(reference.Canonical); ok { - image.Digest = can.Digest().String() - } else if tagged, ok := dockerRef.(reference.Tagged); ok { - image.NewTag = tagged.Tag() - } else { - image.NewTag = "latest" - } - - altNames, err := imageutil.BuildImageAltNames(image) - if err != nil { - return nil, errors.Wrap(err, "failed build alt names") - } - kustomizeImages = append(kustomizeImages, altNames...) - } - - return &FindPrivateImagesResult{ - Images: kustomizeImages, - CheckedImages: installationImagesFromInfoMap(checkedImages), - }, nil -} - -func makeInstallationImageInfoMap(images []kotsv1beta1.InstallationImage) map[string]imagetypes.InstallationImageInfo { - result := make(map[string]imagetypes.InstallationImageInfo) - for _, i := range images { - result[i.Image] = imagetypes.InstallationImageInfo{ - IsPrivate: i.IsPrivate, - } - } - return result -} - -func installationImagesFromInfoMap(images map[string]imagetypes.InstallationImageInfo) []kotsv1beta1.InstallationImage { - result := make([]kotsv1beta1.InstallationImage, 0) - for image, info := range images { - result = append(result, kotsv1beta1.InstallationImage{ - Image: image, - IsPrivate: info.IsPrivate, - }) - } - return result -} diff --git a/pkg/base/rewrite.go b/pkg/base/rewrite.go new file mode 100644 index 0000000000..b8587cb32c --- /dev/null +++ b/pkg/base/rewrite.go @@ -0,0 +1,121 @@ +package base + +import ( + "strings" + + dockerref "github.com/containers/image/v5/docker/reference" + "github.com/distribution/distribution/v3/reference" + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/docker/registry" + dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" + "github.com/replicatedhq/kots/pkg/imageutil" + "github.com/replicatedhq/kots/pkg/kotsutil" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + kustomizetypes "sigs.k8s.io/kustomize/api/types" +) + +type RewriteImagesOptions struct { + Images []string + DestinationRegistry dockerregistrytypes.RegistryOptions +} + +// RewriteImages rewrites all images to point to the configured destination registry. +func RewriteImages(images []string, destRegistry dockerregistrytypes.RegistryOptions) ([]kustomizetypes.Image, error) { + rewrittenImages := []kustomizetypes.Image{} + rewritten := map[string]bool{} + + for _, image := range images { + if _, ok := rewritten[image]; ok { + continue + } + rewrittenImage, err := imageutil.RewriteDockerRegistryImage(destRegistry, image) + if err != nil { + return nil, errors.Wrapf(err, "failed to rewrite image %s", image) + } + rewrittenImages = append(rewrittenImages, *rewrittenImage) + rewritten[image] = true + } + + withAltNames := make([]kustomizetypes.Image, 0) + for _, i := range rewrittenImages { + altNames, err := imageutil.BuildImageAltNames(i) + if err != nil { + return nil, errors.Wrap(err, "failed to build image alt names") + } + withAltNames = append(withAltNames, altNames...) + } + + return withAltNames, nil +} + +// RewritePrivateImages rewrites private images to be proxied through proxy.replicated.com, +// and rewrites replicated registry images to use the custom registry domain if configured +func RewritePrivateImages(images []string, kotsKinds *kotsutil.KotsKinds, license *kotsv1beta1.License) ([]kustomizetypes.Image, error) { + replicatedRegistryInfo := registry.GetRegistryProxyInfo(license, &kotsKinds.Installation, &kotsKinds.KotsApplication) + + replicatedRegistry := dockerregistrytypes.RegistryOptions{ + Endpoint: replicatedRegistryInfo.Registry, + ProxyEndpoint: replicatedRegistryInfo.Proxy, + UpstreamEndpoint: replicatedRegistryInfo.Upstream, + } + + installationImages := make(map[string]imagetypes.InstallationImageInfo) + for _, i := range kotsKinds.Installation.Spec.KnownImages { + installationImages[i.Image] = imagetypes.InstallationImageInfo{ + IsPrivate: i.IsPrivate, + } + } + + privateImages := []string{} + for _, img := range images { + if installationImages[img].IsPrivate { + privateImages = append(privateImages, img) + } + } + + kustomizeImages := make([]kustomizetypes.Image, 0) + for _, privateImage := range privateImages { + dockerRef, err := dockerref.ParseDockerRef(privateImage) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse docker ref %q", privateImage) + } + + registryHost := dockerref.Domain(dockerRef) + if registryHost == replicatedRegistry.Endpoint { + // replicated images are also private, but we don't rewrite those + continue + } + + image := kustomizetypes.Image{} + if registryHost == replicatedRegistry.UpstreamEndpoint { + // image is using the upstream replicated registry, but a custom registry domain is configured, so rewrite to use the custom domain + image = kustomizetypes.Image{ + Name: dockerRef.Name(), + NewName: strings.Replace(dockerRef.Name(), registryHost, replicatedRegistry.Endpoint, 1), + } + } else { + // all other private images are rewritten to use the replicated proxy + image = kustomizetypes.Image{ + Name: dockerRef.Name(), + NewName: registry.MakeProxiedImageURL(replicatedRegistry.ProxyEndpoint, license.Spec.AppSlug, privateImage), + } + } + + if can, ok := dockerRef.(reference.Canonical); ok { + image.Digest = can.Digest().String() + } else if tagged, ok := dockerRef.(reference.Tagged); ok { + image.NewTag = tagged.Tag() + } else { + image.NewTag = "latest" + } + + altNames, err := imageutil.BuildImageAltNames(image) + if err != nil { + return nil, errors.Wrap(err, "failed build alt names") + } + kustomizeImages = append(kustomizeImages, altNames...) + } + + return kustomizeImages, nil +} diff --git a/pkg/base/rewrite_test.go b/pkg/base/rewrite_test.go new file mode 100644 index 0000000000..2e74dd5e61 --- /dev/null +++ b/pkg/base/rewrite_test.go @@ -0,0 +1,319 @@ +package base + +import ( + "testing" + + registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" + "github.com/replicatedhq/kots/pkg/kotsutil" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + kustomizetypes "sigs.k8s.io/kustomize/api/types" +) + +func Test_RewriteImages(t *testing.T) { + type args struct { + images []string + destinationRegistry registrytypes.RegistryOptions + } + + tests := []struct { + name string + args args + wantResult []kustomizetypes.Image + }{ + { + name: "basic", + args: args{ + images: []string{ + "quay.io/replicatedcom/qa-kots-3:alpine-3.6", + "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + "registry.replicated.com/appslug/image:version", + "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + "nginx:1", + "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + "testing.registry.com:5000/testing-ns/random-image:1", + "busybox", + }, + destinationRegistry: registrytypes.RegistryOptions{ + Endpoint: "testing.registry.com:5000", + Namespace: "testing-ns", + Username: "testing-user-name", + Password: "testing-password", + }, + }, + wantResult: []kustomizetypes.Image{ + { + Name: "busybox", + NewName: "testing.registry.com:5000/testing-ns/busybox", + NewTag: "latest", + }, + { + Name: "docker.io/library/busybox", + NewName: "testing.registry.com:5000/testing-ns/busybox", + NewTag: "latest", + }, + { + Name: "library/busybox", + NewName: "testing.registry.com:5000/testing-ns/busybox", + NewTag: "latest", + }, + { + Name: "docker.io/busybox", + NewName: "testing.registry.com:5000/testing-ns/busybox", + NewTag: "latest", + }, + { + Name: "registry.replicated.com/appslug/image", + NewName: "testing.registry.com:5000/testing-ns/image", + NewTag: "version", + }, + { + Name: "quay.io/replicatedcom/qa-kots-1", + NewName: "testing.registry.com:5000/testing-ns/qa-kots-1", + NewTag: "alpine-3.5", + }, + { + Name: "quay.io/replicatedcom/qa-kots-2", + NewName: "testing.registry.com:5000/testing-ns/qa-kots-2", + NewTag: "alpine-3.4", + }, + { + Name: "quay.io/replicatedcom/qa-kots-3", + NewName: "testing.registry.com:5000/testing-ns/qa-kots-3", + NewTag: "alpine-3.6", + }, + { + Name: "quay.io/replicatedcom/someimage", + NewName: "testing.registry.com:5000/testing-ns/someimage", + Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + }, + { + Name: "nginx", + NewName: "testing.registry.com:5000/testing-ns/nginx", + NewTag: "1", + }, + { + Name: "docker.io/library/nginx", + NewName: "testing.registry.com:5000/testing-ns/nginx", + NewTag: "1", + }, + { + Name: "library/nginx", + NewName: "testing.registry.com:5000/testing-ns/nginx", + NewTag: "1", + }, + { + Name: "docker.io/nginx", + NewName: "testing.registry.com:5000/testing-ns/nginx", + NewTag: "1", + }, + { + Name: "redis", + NewName: "testing.registry.com:5000/testing-ns/redis", + Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + }, + { + Name: "docker.io/library/redis", + NewName: "testing.registry.com:5000/testing-ns/redis", + Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + }, + { + Name: "library/redis", + NewName: "testing.registry.com:5000/testing-ns/redis", + Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + }, + { + Name: "docker.io/redis", + NewName: "testing.registry.com:5000/testing-ns/redis", + Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + }, + { + Name: "testing.registry.com:5000/testing-ns/random-image", + NewName: "testing.registry.com:5000/testing-ns/random-image", + NewTag: "1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + gotResult, err := RewriteImages(test.args.images, test.args.destinationRegistry) + req.NoError(err) + + assert.ElementsMatch(t, test.wantResult, gotResult) + }) + } +} + +func Test_RewritePrivateImages(t *testing.T) { + type args struct { + images []string + kotsKinds *kotsutil.KotsKinds + } + + tests := []struct { + name string + args args + wantResult []kustomizetypes.Image + }{ + { + name: "basic", + args: args{ + images: []string{ + "quay.io/replicatedcom/qa-kots-3:alpine-3.6", + "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + }, + kotsKinds: &kotsutil.KotsKinds{ + License: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app-slug", + }, + }, + Installation: kotsv1beta1.Installation{ + Spec: kotsv1beta1.InstallationSpec{ + KnownImages: []kotsv1beta1.InstallationImage{ + { + Image: "quay.io/replicatedcom/qa-kots-3:alpine-3.6", + IsPrivate: true, + }, + { + Image: "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + IsPrivate: true, + }, + { + Image: "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + IsPrivate: false, + }, + }, + }, + }, + }, + }, + wantResult: []kustomizetypes.Image{ + { + Name: "quay.io/replicatedcom/qa-kots-3", + NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/qa-kots-3", + NewTag: "alpine-3.6", + }, + { + Name: "quay.io/replicatedcom/someimage", + NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", + Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + }, + }, + }, + { + name: "replicated registry with custom domains configured should rewrite replicated images and not custom domain images", + args: args{ + images: []string{ + "registry.replicated.com/appslug/image:version", + "my-registry.example.com/appslug/some-other-image:version", + "quay.io/replicatedcom/someimage:1", + }, + kotsKinds: &kotsutil.KotsKinds{ + License: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app-slug", + }, + }, + Installation: kotsv1beta1.Installation{ + Spec: kotsv1beta1.InstallationSpec{ + ReplicatedRegistryDomain: "my-registry.example.com", + ReplicatedProxyDomain: "my-proxy.example.com", + KnownImages: []kotsv1beta1.InstallationImage{ + { + Image: "registry.replicated.com/appslug/image:version", + IsPrivate: true, + }, + { + Image: "my-registry.example.com/appslug/some-other-image:version", + IsPrivate: true, + }, + { + Image: "quay.io/replicatedcom/someimage:1", + IsPrivate: true, + }, + }, + }, + }, + }, + }, + wantResult: []kustomizetypes.Image{ + { + Name: "registry.replicated.com/appslug/image", + NewName: "my-registry.example.com/appslug/image", + NewTag: "version", + }, + { + Name: "quay.io/replicatedcom/someimage", + NewName: "my-proxy.example.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", + NewTag: "1", + }, + }, + }, + { + name: "replicated registry without custom domains should not rewrite replicated registry images", + args: args{ + images: []string{ + "registry.replicated.com/appslug/image:version", + "my-registry.example.com/appslug/some-other-image:version", + "quay.io/replicatedcom/someimage:1", + }, + kotsKinds: &kotsutil.KotsKinds{ + License: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app-slug", + }, + }, + Installation: kotsv1beta1.Installation{ + Spec: kotsv1beta1.InstallationSpec{ + KnownImages: []kotsv1beta1.InstallationImage{ + { + Image: "registry.replicated.com/appslug/image:version", + IsPrivate: true, + }, + { + Image: "my-registry.example.com/appslug/some-other-image:version", + IsPrivate: true, + }, + { + Image: "quay.io/replicatedcom/someimage:1", + IsPrivate: true, + }, + }, + }, + }, + }, + }, + wantResult: []kustomizetypes.Image{ + { + Name: "my-registry.example.com/appslug/some-other-image", + NewName: "proxy.replicated.com/proxy/test-app-slug/my-registry.example.com/appslug/some-other-image", + NewTag: "version", + }, + { + Name: "quay.io/replicatedcom/someimage", + NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", + NewTag: "1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + gotResult, err := RewritePrivateImages(test.args.images, test.args.kotsKinds, test.args.kotsKinds.License) + req.NoError(err) + + assert.ElementsMatch(t, test.wantResult, gotResult) + }) + } +} diff --git a/pkg/base/testdata/base-specs/pod.yaml b/pkg/base/testdata/base-specs/pod.yaml deleted file mode 100644 index f0ad51e329..0000000000 --- a/pkg/base/testdata/base-specs/pod.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: base-test-pod - namespace: base-test -spec: - initContainers: - - image: quay.io/replicatedcom/qa-kots-3:alpine-3.6 - name: private-app-image - - image: quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4 - name: private-app-image-with-digest - - image: redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633 - name: public-app-image-with-digest - containers: - - image: busybox - name: busybox-container diff --git a/pkg/base/testdata/replicated-registry/pod.yaml b/pkg/base/testdata/replicated-registry/pod.yaml deleted file mode 100644 index 6100892e6d..0000000000 --- a/pkg/base/testdata/replicated-registry/pod.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: replcated-registry-test-pod - namespace: replicated-registry-test -spec: - containers: - - image: registry.replicated.com/appslug/image:version - name: replicated-registry-image - - image: my-registry.example.com/appslug/some-other-image:version - name: custom-registry-hostname-image - - image: quay.io/replicatedcom/someimage:1 - name: private-image - diff --git a/pkg/base/write_images.go b/pkg/base/write_images.go deleted file mode 100644 index 0e11d4fa48..0000000000 --- a/pkg/base/write_images.go +++ /dev/null @@ -1,55 +0,0 @@ -package base - -import ( - "io" - - "github.com/pkg/errors" - registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" - "github.com/replicatedhq/kots/pkg/image" - imagetypes "github.com/replicatedhq/kots/pkg/image/types" - "github.com/replicatedhq/kots/pkg/kotsutil" - "github.com/replicatedhq/kots/pkg/logger" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kustomizeimage "sigs.k8s.io/kustomize/api/types" -) - -type RewriteImageOptions struct { - BaseImages []string - KotsKindsImages []string - AppSlug string - SourceRegistry registrytypes.RegistryOptions - DestRegistry registrytypes.RegistryOptions - DockerHubRegistry registrytypes.RegistryOptions - CopyImages bool - IsAirgap bool - Log *logger.CLILogger - ReportWriter io.Writer - KotsKinds *kotsutil.KotsKinds -} - -type RewriteImagesResult struct { - Images []kustomizeimage.Image // images to be rewritten - CheckedImages []kotsv1beta1.InstallationImage // all images found in the installation -} - -func RewriteImages(options RewriteImageOptions) (*RewriteImagesResult, error) { - allImagesPrivate := options.IsAirgap - checkedImages := make(map[string]imagetypes.InstallationImageInfo) - - if options.KotsKinds != nil { - checkedImages = makeInstallationImageInfoMap(options.KotsKinds.Installation.Spec.KnownImages) - if options.KotsKinds.KotsApplication.Spec.ProxyPublicImages { - allImagesPrivate = true - } - } - - newImages, err := image.RewriteImages(options.SourceRegistry, options.DestRegistry, options.AppSlug, options.Log, options.ReportWriter, options.BaseImages, options.KotsKindsImages, options.CopyImages, allImagesPrivate, checkedImages, options.DockerHubRegistry) - if err != nil { - return nil, errors.Wrap(err, "failed to save images") - } - - return &RewriteImagesResult{ - Images: newImages, - CheckedImages: installationImagesFromInfoMap(checkedImages), - }, nil -} diff --git a/pkg/base/write_images_test.go b/pkg/base/write_images_test.go deleted file mode 100644 index ea5ec0982f..0000000000 --- a/pkg/base/write_images_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package base - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/pkg/errors" - registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" - "github.com/replicatedhq/kots/pkg/image" - "github.com/replicatedhq/kots/pkg/k8sdoc" - "github.com/replicatedhq/kots/pkg/kotsutil" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - kustomizeimage "sigs.k8s.io/kustomize/api/types" -) - -func Test_RewriteImages(t *testing.T) { - tests := []struct { - name string - baseDir string - appSlug string - processOptions RewriteImageOptions - wantProcessResult RewriteImagesResult - findOptions FindPrivateImagesOptions - wantFindResult FindPrivateImagesResult - }{ - { - name: "all unique", - baseDir: "./testdata/base-specs", - appSlug: "test-app-slug", - processOptions: RewriteImageOptions{ - SourceRegistry: registrytypes.RegistryOptions{ - Endpoint: "registry.replicated.com", - ProxyEndpoint: "proxy.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - KotsKinds: &kotsutil.KotsKinds{ - KotsApplication: kotsv1beta1.Application{ - Spec: kotsv1beta1.ApplicationSpec{ - AdditionalImages: []string{ - "registry.replicated.com/appslug/image:version", - }, - }, - }, - Preflight: &troubleshootv1beta2.Preflight{ - Spec: troubleshootv1beta2.PreflightSpec{ - Collectors: []*troubleshootv1beta2.Collect{ - { - Run: &troubleshootv1beta2.Run{ - Image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", - }, - }, - { - Run: &troubleshootv1beta2.Run{ - Image: "testing.registry.com:5000/testing-ns/random-image:2", - }, - }, - { - RunPod: &troubleshootv1beta2.RunPod{ - PodSpec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Image: "nginx:1", - }, - }, - }, - }, - }, - }, - }, - }, - SupportBundle: &troubleshootv1beta2.SupportBundle{ - Spec: troubleshootv1beta2.SupportBundleSpec{ - Collectors: []*troubleshootv1beta2.Collect{ - { - Run: &troubleshootv1beta2.Run{ - Image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", - }, - }, - { - Run: &troubleshootv1beta2.Run{ - Image: "testing.registry.com:5000/testing-ns/random-image:1", - }, - }, - }, - }, - }, - }, - CopyImages: false, - AppSlug: "test-app-slug", - DestRegistry: registrytypes.RegistryOptions{ - Endpoint: "testing.registry.com:5000", - Namespace: "testing-ns", - Username: "testing-user-name", - Password: "testing-password", - }, - }, - wantProcessResult: RewriteImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "busybox", - NewName: "testing.registry.com:5000/testing-ns/busybox", - NewTag: "latest", - }, - { - Name: "docker.io/library/busybox", - NewName: "testing.registry.com:5000/testing-ns/busybox", - NewTag: "latest", - }, - { - Name: "library/busybox", - NewName: "testing.registry.com:5000/testing-ns/busybox", - NewTag: "latest", - }, - { - Name: "docker.io/busybox", - NewName: "testing.registry.com:5000/testing-ns/busybox", - NewTag: "latest", - }, - { - Name: "registry.replicated.com/appslug/image", - NewName: "testing.registry.com:5000/testing-ns/image", - NewTag: "version", - }, - { - Name: "quay.io/replicatedcom/qa-kots-1", - NewName: "testing.registry.com:5000/testing-ns/qa-kots-1", - NewTag: "alpine-3.5", - }, - { - Name: "quay.io/replicatedcom/qa-kots-2", - NewName: "testing.registry.com:5000/testing-ns/qa-kots-2", - NewTag: "alpine-3.4", - }, - { - Name: "quay.io/replicatedcom/qa-kots-3", - NewName: "testing.registry.com:5000/testing-ns/qa-kots-3", - NewTag: "alpine-3.6", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "testing.registry.com:5000/testing-ns/someimage", - Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - }, - { - Name: "nginx", - NewName: "testing.registry.com:5000/testing-ns/nginx", - NewTag: "1", - }, - { - Name: "docker.io/library/nginx", - NewName: "testing.registry.com:5000/testing-ns/nginx", - NewTag: "1", - }, - { - Name: "library/nginx", - NewName: "testing.registry.com:5000/testing-ns/nginx", - NewTag: "1", - }, - { - Name: "docker.io/nginx", - NewName: "testing.registry.com:5000/testing-ns/nginx", - NewTag: "1", - }, - { - Name: "redis", - NewName: "testing.registry.com:5000/testing-ns/redis", - Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - }, - { - Name: "docker.io/library/redis", - NewName: "testing.registry.com:5000/testing-ns/redis", - Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - }, - { - Name: "library/redis", - NewName: "testing.registry.com:5000/testing-ns/redis", - Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - }, - { - Name: "docker.io/redis", - NewName: "testing.registry.com:5000/testing-ns/redis", - Digest: "sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "busybox", - IsPrivate: false, - }, - { - Image: "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - IsPrivate: false, - }, - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-3:alpine-3.6", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - IsPrivate: true, - }, - { - Image: "nginx:1", - IsPrivate: false, - }, - }, - }, - - findOptions: FindPrivateImagesOptions{ - AppSlug: "test-app-slug", - ReplicatedRegistry: registrytypes.RegistryOptions{ - Endpoint: "registry.replicated.com", - ProxyEndpoint: "proxy.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - Installation: &kotsv1beta1.Installation{}, - AllImagesPrivate: false, - }, - wantFindResult: FindPrivateImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "quay.io/replicatedcom/qa-kots-3", - NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/qa-kots-3", - NewTag: "alpine-3.6", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", - Digest: "sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/qa-kots-3:alpine-3.6", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", - IsPrivate: true, - }, - { - Image: "testing.registry.com:5000/testing-ns/random-image:2", - IsPrivate: true, - }, - { - Image: "testing.registry.com:5000/testing-ns/random-image:1", - IsPrivate: true, - }, - { - Image: "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", - IsPrivate: false, - }, - { - Image: "nginx:1", - IsPrivate: false, - }, - { - Image: "busybox", - IsPrivate: false, - }, - }, - }, - }, - { - name: "replicated registry with custom domains configured should rewrite replicated images and not custom domain images", - baseDir: "./testdata/replicated-registry", - appSlug: "test-app-slug", - processOptions: RewriteImageOptions{ - SourceRegistry: registrytypes.RegistryOptions{ - Endpoint: "my-registry.example.com", - ProxyEndpoint: "my-proxy.example.com", - UpstreamEndpoint: "registry.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - KotsKinds: &kotsutil.KotsKinds{ - KotsApplication: kotsv1beta1.Application{ - Spec: kotsv1beta1.ApplicationSpec{ - AdditionalImages: []string{}, - }, - }, - }, - CopyImages: false, - AppSlug: "test-app-slug", - DestRegistry: registrytypes.RegistryOptions{ - Endpoint: "ttl.sh", - Namespace: "testing-ns", - Username: "testing-user-name", - Password: "testing-password", - }, - }, - wantProcessResult: RewriteImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "registry.replicated.com/appslug/image", - NewName: "ttl.sh/testing-ns/image", - NewTag: "version", - }, - { - Name: "my-registry.example.com/appslug/some-other-image", - NewName: "ttl.sh/testing-ns/some-other-image", - NewTag: "version", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "ttl.sh/testing-ns/someimage", - NewTag: "1", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "my-registry.example.com/appslug/some-other-image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1", - IsPrivate: true, - }, - }, - }, - - findOptions: FindPrivateImagesOptions{ - AppSlug: "test-app-slug", - ReplicatedRegistry: registrytypes.RegistryOptions{ - Endpoint: "my-registry.example.com", - ProxyEndpoint: "my-proxy.example.com", - UpstreamEndpoint: "registry.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - Installation: &kotsv1beta1.Installation{}, - AllImagesPrivate: false, - }, - wantFindResult: FindPrivateImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "registry.replicated.com/appslug/image", - NewName: "my-registry.example.com/appslug/image", - NewTag: "version", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "my-proxy.example.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", - NewTag: "1", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "my-registry.example.com/appslug/some-other-image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1", - IsPrivate: true, - }, - }, - }, - }, - { - name: "replicated registry without custom domains should not rewrite replicated registry images", - baseDir: "./testdata/replicated-registry", - appSlug: "test-app-slug", - processOptions: RewriteImageOptions{ - SourceRegistry: registrytypes.RegistryOptions{ - Endpoint: "registry.replicated.com", - ProxyEndpoint: "proxy.replicated.com", - UpstreamEndpoint: "registry.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - KotsKinds: &kotsutil.KotsKinds{ - KotsApplication: kotsv1beta1.Application{ - Spec: kotsv1beta1.ApplicationSpec{ - AdditionalImages: []string{}, - }, - }, - }, - CopyImages: false, - AppSlug: "test-app-slug", - DestRegistry: registrytypes.RegistryOptions{ - Endpoint: "ttl.sh", - Namespace: "testing-ns", - Username: "testing-user-name", - Password: "testing-password", - }, - }, - wantProcessResult: RewriteImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "registry.replicated.com/appslug/image", - NewName: "ttl.sh/testing-ns/image", - NewTag: "version", - }, - { - Name: "my-registry.example.com/appslug/some-other-image", - NewName: "ttl.sh/testing-ns/some-other-image", - NewTag: "version", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "ttl.sh/testing-ns/someimage", - NewTag: "1", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "my-registry.example.com/appslug/some-other-image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1", - IsPrivate: true, - }, - }, - }, - - findOptions: FindPrivateImagesOptions{ - AppSlug: "test-app-slug", - ReplicatedRegistry: registrytypes.RegistryOptions{ - Endpoint: "registry.replicated.com", - ProxyEndpoint: "proxy.replicated.com", - Username: "test-license-id", - Password: "test-license-id", - }, - Installation: &kotsv1beta1.Installation{}, - AllImagesPrivate: false, - }, - wantFindResult: FindPrivateImagesResult{ - Images: []kustomizeimage.Image{ - { - Name: "my-registry.example.com/appslug/some-other-image", - NewName: "proxy.replicated.com/proxy/test-app-slug/my-registry.example.com/appslug/some-other-image", - NewTag: "version", - }, - { - Name: "quay.io/replicatedcom/someimage", - NewName: "proxy.replicated.com/proxy/test-app-slug/quay.io/replicatedcom/someimage", - NewTag: "1", - }, - }, - CheckedImages: []kotsv1beta1.InstallationImage{ - { - Image: "registry.replicated.com/appslug/image:version", - IsPrivate: true, - }, - { - Image: "my-registry.example.com/appslug/some-other-image:version", - IsPrivate: true, - }, - { - Image: "quay.io/replicatedcom/someimage:1", - IsPrivate: true, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := require.New(t) - - kotsKindsImages, err := kotsutil.GetImagesFromKotsKinds(test.processOptions.KotsKinds, &test.processOptions.DestRegistry) - req.NoError(err) - test.processOptions.KotsKindsImages = kotsKindsImages - - baseImages, err := image.FindImagesInDir(test.baseDir) - req.NoError(err) - test.processOptions.BaseImages = baseImages - - gotResult, err := RewriteImages(test.processOptions) - req.NoError(err) - - assert.ElementsMatch(t, test.wantProcessResult.Images, gotResult.Images) - assert.ElementsMatch(t, test.wantProcessResult.CheckedImages, gotResult.CheckedImages) - - kotsKindsImages, err = kotsutil.GetImagesFromKotsKinds(test.processOptions.KotsKinds, nil) // no dest registry - req.NoError(err) - test.findOptions.KotsKindsImages = kotsKindsImages - test.findOptions.BaseImages = baseImages - - gotFindResult, err := FindPrivateImages(test.findOptions) - req.NoError(err) - - assert.ElementsMatch(t, test.wantFindResult.Images, gotFindResult.Images) - assert.ElementsMatch(t, test.wantFindResult.CheckedImages, gotFindResult.CheckedImages) - }) - } - -} - -func loadDocs(basePath string) ([]k8sdoc.K8sDoc, error) { - files, err := ioutil.ReadDir(basePath) - if err != nil { - return nil, errors.Wrap(err, "read base dir") - } - - docs := []k8sdoc.K8sDoc{} - for _, file := range files { - if file.IsDir() { - continue - } - content, err := os.ReadFile(filepath.Join(basePath, file.Name())) - if err != nil { - return nil, errors.Wrap(err, "read file") - } - - doc, err := k8sdoc.ParseYAML(content) - if err != nil { - continue - } - docs = append(docs, doc) - } - - return docs, nil -} diff --git a/pkg/image/push.go b/pkg/image/airgap.go similarity index 93% rename from pkg/image/push.go rename to pkg/image/airgap.go index 4d4b26d716..2d376213a9 100644 --- a/pkg/image/push.go +++ b/pkg/image/airgap.go @@ -20,16 +20,19 @@ import ( containerstypes "github.com/containers/image/v5/types" "github.com/mholt/archiver/v3" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/archives" dockerarchive "github.com/replicatedhq/kots/pkg/docker/archive" dockerregistry "github.com/replicatedhq/kots/pkg/docker/registry" + dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" dockertypes "github.com/replicatedhq/kots/pkg/docker/types" imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/imageutil" "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" "k8s.io/client-go/kubernetes/scheme" ) -// Pushes Admin Console images from airgap bundle to private registry +// PushImages detects if airgap bundle is a KOTS or app bundle, then pushes images from airgap bundle to private registry accordingly func PushImages(airgapArchive string, options imagetypes.PushImagesOptions) error { airgapRootDir, err := ioutil.TempDir("", "kotsadm-airgap") if err != nil { @@ -236,6 +239,35 @@ func WriteProgressLine(progressWriter io.Writer, line string) { fmt.Fprint(progressWriter, fmt.Sprintf("%s\n", line)) } +// CopyAirgapImages pushes images found in the app airgap bundle/airgap root to the configured registry. +func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) error { + pushOpts := imagetypes.PushImagesOptions{ + Registry: dockerregistrytypes.RegistryOptions{ + Endpoint: opts.RegistrySettings.Hostname, + Namespace: opts.RegistrySettings.Namespace, + Username: opts.RegistrySettings.Username, + Password: opts.RegistrySettings.Password, + }, + Log: log, + ProgressWriter: opts.ReportWriter, + LogForUI: true, + } + + if opts.AirgapBundle != "" { + err := TagAndPushAppImagesFromBundle(opts.AirgapBundle, pushOpts) + if err != nil { + return errors.Wrap(err, "failed to push images from bundle") + } + } else { + err := TagAndPushAppImagesFromPath(opts.AirgapRoot, pushOpts) + if err != nil { + return errors.Wrap(err, "failed to push images from dir") + } + } + + return nil +} + func TagAndPushAppImagesFromPath(airgapRootDir string, options imagetypes.PushImagesOptions) error { airgap, err := kotsutil.FindAirgapMetaInDir(airgapRootDir) if err != nil { @@ -283,8 +315,13 @@ func TagAndPushAppImagesFromBundle(airgapBundle string, options imagetypes.PushI } func PushAppImagesFromTempRegistry(airgapRootDir string, imageList []string, options imagetypes.PushImagesOptions) error { + imagesDir := filepath.Join(airgapRootDir, "images") + if _, err := os.Stat(imagesDir); os.IsNotExist(err) { + return nil + } + tempRegistry := &dockerregistry.TempRegistry{} - if err := tempRegistry.Start(filepath.Join(airgapRootDir, "images")); err != nil { + if err := tempRegistry.Start(imagesDir); err != nil { return errors.Wrap(err, "failed to start temp registry") } defer tempRegistry.Stop() @@ -352,8 +389,10 @@ func PushAppImagesFromTempRegistry(airgapRootDir string, imageList []string, opt Password: options.Registry.Password, }, CopyAll: rewrittenImage.Digest != "", // we only support multi-arch images using digests - SkipSrcTLSVerify: true, - SkipDestTLSVerify: true, + SrcDisableV1Ping: true, + SrcSkipTLSVerify: true, + DestDisableV1Ping: true, + DestSkipTLSVerify: true, ReportWriter: reportWriter, }, } @@ -366,9 +405,13 @@ func PushAppImagesFromTempRegistry(airgapRootDir string, imageList []string, opt } func PushAppImagesFromDockerArchivePath(airgapRootDir string, options imagetypes.PushImagesOptions) error { + imagesDir := filepath.Join(airgapRootDir, "images") + if _, err := os.Stat(imagesDir); os.IsNotExist(err) { + return nil + } + imageInfos := make(map[string]*imagetypes.ImageInfo) - imagesDir := filepath.Join(airgapRootDir, "images") walkErr := filepath.Walk(imagesDir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -443,7 +486,8 @@ func PushAppImagesFromDockerArchivePath(airgapRootDir string, options imagetypes Password: options.Registry.Password, }, CopyAll: false, // docker-archive format does not support multi-arch images - SkipDestTLSVerify: true, + DestSkipTLSVerify: true, + DestDisableV1Ping: true, ReportWriter: reportWriter, }, } @@ -456,6 +500,12 @@ func PushAppImagesFromDockerArchivePath(airgapRootDir string, options imagetypes } func PushAppImagesFromDockerArchiveBundle(airgapBundle string, options imagetypes.PushImagesOptions) error { + if exists, err := archives.DirExistsInAirgap("images", airgapBundle); err != nil { + return errors.Wrap(err, "failed to check if images dir exists in airgap bundle") + } else if !exists { + return nil + } + if options.LogForUI { WriteProgressLine(options.ProgressWriter, "Reading image information from bundle...") } @@ -561,7 +611,8 @@ func PushAppImagesFromDockerArchiveBundle(airgapBundle string, options imagetype Password: options.Registry.Password, }, CopyAll: false, // docker-archive format does not support multi-arch images - SkipDestTLSVerify: true, + DestSkipTLSVerify: true, + DestDisableV1Ping: true, ReportWriter: reportWriter, }, } diff --git a/pkg/image/builder.go b/pkg/image/online.go similarity index 55% rename from pkg/image/builder.go rename to pkg/image/online.go index a3f4716a86..e5346bade5 100644 --- a/pkg/image/builder.go +++ b/pkg/image/online.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "io" - "io/ioutil" "os" - "path" "path/filepath" "runtime" "sort" @@ -24,68 +22,66 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/docker/registry" dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" - dockertypes "github.com/replicatedhq/kots/pkg/docker/types" "github.com/replicatedhq/kots/pkg/image/types" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/imageutil" "github.com/replicatedhq/kots/pkg/k8sdoc" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" - regsitrytypes "github.com/replicatedhq/kots/pkg/registry/types" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "golang.org/x/sync/errgroup" - kustomizeimage "sigs.k8s.io/kustomize/api/types" ) var imagePolicy = []byte(`{ "default": [{"type": "insecureAcceptAnything"}] }`) -func RewriteImages(srcRegistry, destRegistry dockerregistrytypes.RegistryOptions, appSlug string, log *logger.CLILogger, reportWriter io.Writer, baseImages []string, kotsKindsImages []string, copyImages, allImagesPrivate bool, checkedImages map[string]types.InstallationImageInfo, dockerHubRegistry dockerregistrytypes.RegistryOptions) ([]kustomizeimage.Image, error) { - rewrittenImages := []kustomizeimage.Image{} - savedImages := map[string]bool{} +type UpdateInstallationImagesOptions struct { + Images []string + KotsKinds *kotsutil.KotsKinds + IsAirgap bool + UpstreamDir string + DockerHubRegistryCreds registry.Credentials +} - for _, baseImage := range baseImages { - if _, saved := savedImages[baseImage]; saved { - continue - } - rewrittenImage, err := rewriteOneImage(srcRegistry, destRegistry, baseImage, appSlug, reportWriter, log, copyImages, allImagesPrivate, checkedImages, dockerHubRegistry) - if err != nil { - return nil, errors.Wrapf(err, "failed to process base image %s", baseImage) - } - rewrittenImages = append(rewrittenImages, rewrittenImage...) - savedImages[baseImage] = true +func UpdateInstallationImages(opts UpdateInstallationImagesOptions) error { + if opts.KotsKinds == nil { + return nil } - for _, kotsKindImage := range kotsKindsImages { - if _, saved := savedImages[kotsKindImage]; saved { - continue - } - rewrittenImage, err := rewriteOneImage(srcRegistry, destRegistry, kotsKindImage, appSlug, reportWriter, log, copyImages, allImagesPrivate, checkedImages, dockerHubRegistry) - if err != nil { - return nil, errors.Wrapf(err, "failed to process kots kind image %s", kotsKindImage) + dockerHubRegistry := dockerregistrytypes.RegistryOptions{ + Username: opts.DockerHubRegistryCreds.Username, + Password: opts.DockerHubRegistryCreds.Password, + } + + installationImagesMap := make(map[string]imagetypes.InstallationImageInfo) + for _, i := range opts.KotsKinds.Installation.Spec.KnownImages { + installationImagesMap[i.Image] = imagetypes.InstallationImageInfo{ + IsPrivate: i.IsPrivate, } - rewrittenImages = append(rewrittenImages, rewrittenImage...) - savedImages[kotsKindImage] = true } - return rewrittenImages, nil -} + allImagesPrivate := opts.IsAirgap + if opts.KotsKinds.KotsApplication.Spec.ProxyPublicImages { + allImagesPrivate = true + } -func GetPrivateImages(baseImages []string, kotsKindsImages []string, checkedImages map[string]types.InstallationImageInfo, allPrivate bool, dockerHubRegistry dockerregistrytypes.RegistryOptions) ([]string, error) { var mtx sync.Mutex const concurrencyLimit = 10 g, _ := errgroup.WithContext(context.Background()) g.SetLimit(concurrencyLimit) isPrivateImage := func(image string) (bool, error) { - if allPrivate { + if allImagesPrivate { return true, nil } mtx.Lock() - checkedImage, ok := checkedImages[image] + installationImage, ok := installationImagesMap[image] mtx.Unlock() if ok { - return checkedImage.IsPrivate, nil + return installationImage.IsPrivate, nil } p, err := IsPrivateImage(image, dockerHubRegistry) @@ -95,23 +91,7 @@ func GetPrivateImages(baseImages []string, kotsKindsImages []string, checkedImag return p, nil } - for _, image := range kotsKindsImages { - func(image string) { - g.Go(func() error { - isPrivate, err := isPrivateImage(image) - if err != nil { - return errors.Wrapf(err, "failed to check if kotskinds image %s is private", image) - } - mtx.Lock() - checkedImages[image] = types.InstallationImageInfo{IsPrivate: isPrivate} - mtx.Unlock() - return nil - }) - }(image) - } - - privateImages := []string{} - for _, image := range baseImages { + for _, image := range opts.Images { func(image string) { g.Go(func() error { isPrivate, err := isPrivateImage(image) @@ -119,10 +99,7 @@ func GetPrivateImages(baseImages []string, kotsKindsImages []string, checkedImag return errors.Wrapf(err, "failed to check if image %s is private", image) } mtx.Lock() - checkedImages[image] = types.InstallationImageInfo{IsPrivate: isPrivate} - if isPrivate { - privateImages = append(privateImages, image) - } + installationImagesMap[image] = types.InstallationImageInfo{IsPrivate: isPrivate} mtx.Unlock() return nil }) @@ -130,108 +107,89 @@ func GetPrivateImages(baseImages []string, kotsKindsImages []string, checkedImag } if err := g.Wait(); err != nil { - return nil, errors.Wrap(err, "failed to wait for image checks") + return errors.Wrap(err, "failed to wait for image checks") + } + + installationImages := []kotsv1beta1.InstallationImage{} + for image, info := range installationImagesMap { + installationImages = append(installationImages, kotsv1beta1.InstallationImage{ + Image: image, + IsPrivate: info.IsPrivate, + }) } // sort the images to get an ordered and reproducible output for easier testing - sort.Strings(privateImages) + sort.Slice(installationImages, func(i, j int) bool { + return installationImages[i].Image < installationImages[j].Image + }) - return privateImages, nil -} + opts.KotsKinds.Installation.Spec.KnownImages = installationImages -func FindImagesInDir(dir string) ([]string, error) { - uniqueImages := make(map[string]bool) + if err := kotsutil.SaveInstallation(&opts.KotsKinds.Installation, opts.UpstreamDir); err != nil { + return errors.Wrap(err, "failed to save installation") + } - err := filepath.Walk(dir, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } + return nil +} - if info.IsDir() { - return nil - } +func CopyOnlineImages(opts imagetypes.ProcessImageOptions, images []string, kotsKinds *kotsutil.KotsKinds, license *kotsv1beta1.License, dockerHubRegistryCreds registry.Credentials, log *logger.CLILogger) error { + installationImages := make(map[string]imagetypes.InstallationImageInfo) + for _, i := range kotsKinds.Installation.Spec.KnownImages { + installationImages[i.Image] = imagetypes.InstallationImageInfo{ + IsPrivate: i.IsPrivate, + } + } - contents, err := os.ReadFile(path) - if err != nil { - return errors.Wrapf(err, "failed to read file %s", path) - } + replicatedRegistryInfo := registry.GetRegistryProxyInfo(license, &kotsKinds.Installation, &kotsKinds.KotsApplication) - return listImagesInFile(contents, func(images []string, doc k8sdoc.K8sDoc) error { - for _, image := range images { - uniqueImages[image] = true - } - return nil - }) - }) - if err != nil { - return nil, errors.Wrap(err, "failed to walk dir") + sourceRegistry := dockerregistrytypes.RegistryOptions{ + Endpoint: replicatedRegistryInfo.Registry, + ProxyEndpoint: replicatedRegistryInfo.Proxy, + UpstreamEndpoint: replicatedRegistryInfo.Upstream, } - - result := make([]string, 0, len(uniqueImages)) - for i := range uniqueImages { - result = append(result, i) + if license != nil { + sourceRegistry.Username = license.Spec.LicenseID + sourceRegistry.Password = license.Spec.LicenseID } - sort.Strings(result) // sort the images to get an ordered and reproducible output for easier testing - return result, nil -} + dockerHubRegistry := dockerregistrytypes.RegistryOptions{ + Username: dockerHubRegistryCreds.Username, + Password: dockerHubRegistryCreds.Password, + } -type processImagesFunc func([]string, k8sdoc.K8sDoc) error + destRegistry := dockerregistrytypes.RegistryOptions{ + Endpoint: opts.RegistrySettings.Hostname, + Namespace: opts.RegistrySettings.Namespace, + Username: opts.RegistrySettings.Username, + Password: opts.RegistrySettings.Password, + } -func listImagesInFile(contents []byte, handler processImagesFunc) error { - yamlDocs := util.ConvertToSingleDocs(contents) - for _, yamlDoc := range yamlDocs { - parsed, err := k8sdoc.ParseYAML(yamlDoc) - if err != nil { + copiedImages := map[string]bool{} + for _, img := range images { + if _, copied := copiedImages[img]; copied { continue } - - images := parsed.ListImages() - - if err := handler(images, parsed); err != nil { - return err + if err := copyOnlineImage(sourceRegistry, destRegistry, img, opts.AppSlug, opts.ReportWriter, log, installationImages, dockerHubRegistry); err != nil { + return errors.Wrapf(err, "failed to copy online image %s", img) } + copiedImages[img] = true } return nil } -func rewriteOneImage(srcRegistry, destRegistry dockerregistrytypes.RegistryOptions, image string, appSlug string, reportWriter io.Writer, log *logger.CLILogger, copyImages, allImagesPrivate bool, checkedImages map[string]types.InstallationImageInfo, dockerHubRegistry dockerregistrytypes.RegistryOptions) ([]kustomizeimage.Image, error) { - sourceCtx := &containerstypes.SystemContext{DockerDisableV1Ping: true} - - // allow pulling images from http/invalid https docker repos - // intended for development only, _THIS MAKES THINGS INSECURE_ - if os.Getenv("KOTSADM_INSECURE_SRCREGISTRY") == "true" { - sourceCtx.DockerInsecureSkipTLSVerify = containerstypes.OptionalBoolTrue - } - - isPrivate := allImagesPrivate // rewrite all images with airgap - if i, ok := checkedImages[image]; ok { - isPrivate = i.IsPrivate - } else { - if !allImagesPrivate { - p, err := IsPrivateImage(image, dockerHubRegistry) - if err != nil { - return nil, errors.Wrap(err, "failed to check if image is private") - } - isPrivate = p - } - checkedImages[image] = types.InstallationImageInfo{ - IsPrivate: isPrivate, - } - } - +func copyOnlineImage(srcRegistry, destRegistry dockerregistrytypes.RegistryOptions, image string, appSlug string, reportWriter io.Writer, log *logger.CLILogger, installationImages map[string]types.InstallationImageInfo, dockerHubRegistry dockerregistrytypes.RegistryOptions) error { // TODO: This reaches out to internet in airgap installs. It shouldn't. sourceImage := image - if isPrivate { - sourceCtx.DockerAuthConfig = &containerstypes.DockerAuthConfig{ + srcAuth := imagetypes.RegistryAuth{} + if installationImages[image].IsPrivate { + srcAuth = imagetypes.RegistryAuth{ Username: srcRegistry.Username, Password: srcRegistry.Password, } rewritten, err := RewritePrivateImage(srcRegistry, image, appSlug) if err != nil { - return nil, errors.Wrap(err, "failed to rewrite private image") + return errors.Wrap(err, "failed to rewrite private image") } sourceImage = rewritten } @@ -239,133 +197,74 @@ func rewriteOneImage(srcRegistry, destRegistry dockerregistrytypes.RegistryOptio // normalize image to make sure only either a digest or a tag exist but not both parsedSrc, err := reference.ParseDockerRef(sourceImage) if err != nil { - return nil, errors.Wrapf(err, "failed to normalize source image %s", sourceImage) + return errors.Wrapf(err, "failed to normalize source image %s", sourceImage) } sourceImage = parsedSrc.String() srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", sourceImage)) if err != nil { - return nil, errors.Wrapf(err, "failed to parse source image name %s", sourceImage) + return errors.Wrapf(err, "failed to parse source image name %s", sourceImage) } destImage, err := imageutil.DestImage(destRegistry, image) if err != nil { - return nil, errors.Wrap(err, "failed to get destination image") + return errors.Wrap(err, "failed to get destination image") } destStr := fmt.Sprintf("docker://%s", destImage) destRef, err := alltransports.ParseImageName(destStr) if err != nil { - return nil, errors.Wrapf(err, "failed to parse dest image name %s", destStr) - } - - destCtx := &containerstypes.SystemContext{ - DockerInsecureSkipTLSVerify: containerstypes.OptionalBoolTrue, - DockerDisableV1Ping: true, - } - - username, password := destRegistry.Username, destRegistry.Password - registryHost := reference.Domain(destRef.DockerReference()) - - if registry.IsECREndpoint(registryHost) && username != "AWS" { - login, err := registry.GetECRLogin(registryHost, username, password) - if err != nil { - return nil, errors.Wrap(err, "failed to get ECR login") - } - username = login.Username - password = login.Password - } - - if username != "" && password != "" { - destCtx.DockerAuthConfig = &containerstypes.DockerAuthConfig{ - Username: username, - Password: password, - } + return errors.Wrapf(err, "failed to parse dest image name %s", destStr) } - if !copyImages { - return imageutil.KustomizeImage(destRegistry, image) - } - - imageListSelection := copy.CopySystemImage + copyAll := false if _, ok := parsedSrc.(reference.Canonical); ok { - // this could be a multi-arch image, copy all architectures so that the digests match. - imageListSelection = copy.CopyAllImages + // we only support multi-arch images using digests + copyAll = true } - _, err = CopyImageWithGC(context.Background(), destRef, srcRef, ©.Options{ - RemoveSignatures: true, - SignBy: "", - ReportWriter: reportWriter, - SourceCtx: sourceCtx, - DestinationCtx: destCtx, - ForceManifestMIMEType: "", - ImageListSelection: imageListSelection, - }) - if err != nil { - 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("%s:%s", dockertypes.FormatDockerArchive, 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 = CopyImageWithGC(context.Background(), localRef, srcRef, ©.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 = CopyImageWithGC(context.Background(), destRef, localRef, ©.Options{ - RemoveSignatures: true, - SignBy: "", - ReportWriter: reportWriter, - SourceCtx: nil, - DestinationCtx: destCtx, - ForceManifestMIMEType: "", - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to push image") - } + copyImageOpts := imagetypes.CopyImageOptions{ + SrcRef: srcRef, + DestRef: destRef, + SrcAuth: srcAuth, + DestAuth: imagetypes.RegistryAuth{ + Username: destRegistry.Username, + Password: destRegistry.Password, + }, + CopyAll: copyAll, + SrcDisableV1Ping: true, + SrcSkipTLSVerify: os.Getenv("KOTSADM_INSECURE_SRCREGISTRY") == "true", + DestDisableV1Ping: true, + DestSkipTLSVerify: true, + ReportWriter: reportWriter, + } + if err := CopyImage(copyImageOpts); err != nil { + return errors.Wrapf(err, "failed to copy %s to %s", sourceImage, destImage) } - return imageutil.KustomizeImage(destRegistry, image) + return nil } func CopyImage(opts types.CopyImageOptions) error { srcCtx := &containerstypes.SystemContext{} destCtx := &containerstypes.SystemContext{} - if opts.SkipSrcTLSVerify { - srcCtx = &containerstypes.SystemContext{ - DockerInsecureSkipTLSVerify: containerstypes.OptionalBoolTrue, - DockerDisableV1Ping: true, - } + if opts.SrcDisableV1Ping { + srcCtx.DockerDisableV1Ping = true + } + if opts.SrcSkipTLSVerify { + srcCtx.DockerInsecureSkipTLSVerify = containerstypes.OptionalBoolTrue + } + if opts.DestDisableV1Ping { + destCtx.DockerDisableV1Ping = true + } + if opts.DestSkipTLSVerify { + destCtx.DockerInsecureSkipTLSVerify = containerstypes.OptionalBoolTrue } - if opts.SkipDestTLSVerify { - destCtx = &containerstypes.SystemContext{ - DockerInsecureSkipTLSVerify: containerstypes.OptionalBoolTrue, - DockerDisableV1Ping: true, + if opts.SrcAuth.Username != "" && opts.SrcAuth.Password != "" { + srcCtx.DockerAuthConfig = &containerstypes.DockerAuthConfig{ + Username: opts.SrcAuth.Username, + Password: opts.SrcAuth.Password, } } @@ -526,17 +425,60 @@ func CopyImageWithGC(ctx context.Context, destRef, srcRef containerstypes.ImageR return manifest, err } -type ProcessImageOptions struct { - AppSlug string - Namespace string - RewriteImages bool - RegistrySettings regsitrytypes.RegistrySettings - CopyImages bool - RootDir string - IsAirgap bool - AirgapRoot string - AirgapBundle string - PushImages bool - CreateAppDir bool - ReportWriter io.Writer +func FindImagesInDir(dir string) ([]string, error) { + uniqueImages := make(map[string]bool) + + err := filepath.Walk(dir, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + contents, err := os.ReadFile(path) + if err != nil { + return errors.Wrapf(err, "failed to read file %s", path) + } + + return listImagesInFile(contents, func(images []string, doc k8sdoc.K8sDoc) error { + for _, image := range images { + uniqueImages[image] = true + } + return nil + }) + }) + if err != nil { + return nil, errors.Wrap(err, "failed to walk dir") + } + + result := make([]string, 0, len(uniqueImages)) + for i := range uniqueImages { + result = append(result, i) + } + sort.Strings(result) // sort the images to get an ordered and reproducible output for easier testing + + return result, nil +} + +type processImagesFunc func([]string, k8sdoc.K8sDoc) error + +func listImagesInFile(contents []byte, handler processImagesFunc) error { + yamlDocs := util.ConvertToSingleDocs(contents) + for _, yamlDoc := range yamlDocs { + parsed, err := k8sdoc.ParseYAML(yamlDoc) + if err != nil { + continue + } + + images := parsed.ListImages() + + if err := handler(images, parsed); err != nil { + return err + } + } + + return nil } diff --git a/pkg/image/online_test.go b/pkg/image/online_test.go new file mode 100644 index 0000000000..1d459f7580 --- /dev/null +++ b/pkg/image/online_test.go @@ -0,0 +1,75 @@ +package image + +import ( + "testing" + + dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_IsPrivateImages(t *testing.T) { + type args struct { + baseImages []string + kotsKindsImages []string + kotsKinds *kotsutil.KotsKinds + } + + tests := []struct { + image string + want bool + }{ + { + image: "registry.replicated.com/appslug/image:version", + want: true, + }, + { + image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + want: true, + }, + { + image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + want: true, + }, + { + image: "quay.io/replicatedcom/qa-kots-3:alpine-3.6", + want: true, + }, + { + image: "quay.io/replicatedcom/someimage:1@sha256:25dedae0aceb6b4fe5837a0acbacc6580453717f126a095aa05a3c6fcea14dd4", + want: true, + }, + { + image: "testing.registry.com:5000/testing-ns/random-image:2", + want: true, + }, + { + image: "testing.registry.com:5000/testing-ns/random-image:1", + want: true, + }, + { + image: "redis:7@sha256:e96c03a6dda7d0f28e2de632048a3d34bb1636d0858b65ef9a554441c70f6633", + want: false, + }, + { + image: "nginx:1", + want: false, + }, + { + image: "busybox", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.image, func(t *testing.T) { + req := require.New(t) + + got, err := IsPrivateImage(test.image, dockerregistrytypes.RegistryOptions{}) + req.NoError(err) + + assert.Equal(t, test.want, got) + }) + } +} diff --git a/pkg/image/types/types.go b/pkg/image/types/types.go index 94064da553..bcb47ed231 100644 --- a/pkg/image/types/types.go +++ b/pkg/image/types/types.go @@ -5,10 +5,25 @@ import ( "time" "github.com/containers/image/v5/types" - registrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" + dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" "github.com/replicatedhq/kots/pkg/logger" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" ) +type ProcessImageOptions struct { + AppSlug string + Namespace string + RewriteImages bool + CopyImages bool + RegistrySettings registrytypes.RegistrySettings + RootDir string + IsAirgap bool + AirgapRoot string + AirgapBundle string + CreateAppDir bool + ReportWriter io.Writer +} + type RegistryAuth struct { Username string Password string @@ -21,15 +36,18 @@ type InstallationImageInfo struct { type CopyImageOptions struct { SrcRef types.ImageReference DestRef types.ImageReference + SrcAuth RegistryAuth DestAuth RegistryAuth CopyAll bool - SkipSrcTLSVerify bool - SkipDestTLSVerify bool + SrcDisableV1Ping bool + SrcSkipTLSVerify bool + DestDisableV1Ping bool + DestSkipTLSVerify bool ReportWriter io.Writer } type PushImagesOptions struct { - Registry registrytypes.RegistryOptions + Registry dockerregistrytypes.RegistryOptions KotsadmTag string Log *logger.CLILogger ProgressWriter io.Writer diff --git a/pkg/imageutil/image.go b/pkg/imageutil/image.go index 2316b47555..9743c44296 100644 --- a/pkg/imageutil/image.go +++ b/pkg/imageutil/image.go @@ -118,22 +118,6 @@ func DestImageFromKustomizeImage(image kustomizetypes.Image) string { return destImage } -// SrcImageFromKustomizeImage returns the location of the source image from a kustomize image type -// Note: if image name contains both a tag and a digest, only the digest is used, so the result might not exactly match the original image name. -func SrcImageFromKustomizeImage(image kustomizetypes.Image) string { - srcImage := image.Name - - if image.Digest != "" { - srcImage += "@" - srcImage += image.Digest - } else if image.NewTag != "" { - srcImage += ":" - srcImage += image.NewTag - } - - return srcImage -} - func BuildImageAltNames(rewrittenImage kustomizetypes.Image) ([]kustomizetypes.Image, error) { // kustomize does string based comparison, so all of these are treated as different images: // docker.io/library/redis:latest diff --git a/pkg/imageutil/image_test.go b/pkg/imageutil/image_test.go index 510ae0c378..9e6d2a391f 100644 --- a/pkg/imageutil/image_test.go +++ b/pkg/imageutil/image_test.go @@ -542,66 +542,6 @@ func TestDestImageFromKustomizeImage(t *testing.T) { } } -func TestSrcImageFromKustomizeImage(t *testing.T) { - type args struct { - image kustomizetypes.Image - } - tests := []struct { - name string - args args - want string - }{ - { - name: "latest tag", - args: args{ - image: kustomizetypes.Image{ - Name: "replicated.registry.com/replicatedcom/alpine", - NewTag: "latest", - }, - }, - want: "replicated.registry.com/replicatedcom/alpine:latest", - }, - { - name: "tag only", - args: args{ - image: kustomizetypes.Image{ - Name: "replicated.registry.com/replicatedcom/alpine", - NewTag: "3.14", - }, - }, - want: "replicated.registry.com/replicatedcom/alpine:3.14", - }, - { - name: "digest only", - args: args{ - image: kustomizetypes.Image{ - Name: "replicated.registry.com/replicatedcom/alpine", - Digest: "sha256:06b5d462c92fc39303e6363c65e074559f8d6b1363250027ed5053557e3398c5", - }, - }, - want: "replicated.registry.com/replicatedcom/alpine@sha256:06b5d462c92fc39303e6363c65e074559f8d6b1363250027ed5053557e3398c5", - }, - { - name: "tag and digest", - args: args{ - image: kustomizetypes.Image{ - Name: "replicated.registry.com/replicatedcom/alpine", - NewTag: "3.14", - Digest: "sha256:06b5d462c92fc39303e6363c65e074559f8d6b1363250027ed5053557e3398c5", - }, - }, - want: "replicated.registry.com/replicatedcom/alpine@sha256:06b5d462c92fc39303e6363c65e074559f8d6b1363250027ed5053557e3398c5", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := SrcImageFromKustomizeImage(tt.args.image); got != tt.want { - t.Errorf("SrcImageFromKustomizeImage() = %v, want %v", got, tt.want) - } - }) - } -} - func Test_BuildImageAltNames(t *testing.T) { tests := []struct { name string diff --git a/pkg/kotsutil/kots_test.go b/pkg/kotsutil/kots_test.go index 53b8de27fb..30353887bf 100644 --- a/pkg/kotsutil/kots_test.go +++ b/pkg/kotsutil/kots_test.go @@ -10,10 +10,14 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/replicatedhq/kots/pkg/crypto" + dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/util" - kotsv1beta "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" ) var _ = Describe("Kots", func() { @@ -287,7 +291,7 @@ var _ = Describe("Kots", func() { Describe("EncryptConfigValues()", func() { It("does not error when the config field is missing", func() { kotsKind := &kotsutil.KotsKinds{ - ConfigValues: &kotsv1beta.ConfigValues{}, + ConfigValues: &kotsv1beta1.ConfigValues{}, } err := kotsKind.EncryptConfigValues() Expect(err).ToNot(HaveOccurred()) @@ -295,24 +299,24 @@ var _ = Describe("Kots", func() { It("does not error when the configValues field is missing", func() { kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{}, + Config: &kotsv1beta1.Config{}, } err := kotsKind.EncryptConfigValues() Expect(err).ToNot(HaveOccurred()) }) It("returns an error if the configItemType is not found", func() { - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues["name"] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues["name"] = kotsv1beta1.ConfigValue{ ValuePlaintext: "valuePlaintext", } kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{ - Spec: kotsv1beta.ConfigSpec{ - Groups: []kotsv1beta.ConfigGroup{ + Config: &kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ { - Items: []kotsv1beta.ConfigItem{ + Items: []kotsv1beta1.ConfigItem{ { Name: "item1", Type: "", @@ -322,8 +326,8 @@ var _ = Describe("Kots", func() { }, }, }, - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -336,17 +340,17 @@ var _ = Describe("Kots", func() { It("returns an error if the configItemType is not a password", func() { configItemType := "notAPassword" itemName := "some-item" - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ ValuePlaintext: "valuePlainText", } kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{ - Spec: kotsv1beta.ConfigSpec{ - Groups: []kotsv1beta.ConfigGroup{ + Config: &kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ { - Items: []kotsv1beta.ConfigItem{ + Items: []kotsv1beta1.ConfigItem{ { Name: itemName, Type: configItemType, @@ -356,8 +360,8 @@ var _ = Describe("Kots", func() { }, }, }, - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -371,18 +375,18 @@ var _ = Describe("Kots", func() { configItemType := "password" itemName := "some-item" nonEncryptedValue := "not-encrypted" - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ Value: nonEncryptedValue, ValuePlaintext: "some-nonEncryptedValue-in-plain-text", } kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{ - Spec: kotsv1beta.ConfigSpec{ - Groups: []kotsv1beta.ConfigGroup{ + Config: &kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ { - Items: []kotsv1beta.ConfigItem{ + Items: []kotsv1beta1.ConfigItem{ { Name: itemName, Type: configItemType, @@ -392,8 +396,8 @@ var _ = Describe("Kots", func() { }, }, }, - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -407,7 +411,7 @@ var _ = Describe("Kots", func() { Describe("DecryptConfigValues()", func() { It("does not error when config values are empty", func() { kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{}, + Config: &kotsv1beta1.Config{}, } err := kotsKind.DecryptConfigValues() Expect(err).ToNot(HaveOccurred()) @@ -415,15 +419,15 @@ var _ = Describe("Kots", func() { It("does not change the value if it is missing", func() { itemName := "some-item" - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ Value: "", ValuePlaintext: "some-nonEncryptedValue-in-plain-text", } kotsKind := &kotsutil.KotsKinds{ - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -438,15 +442,15 @@ var _ = Describe("Kots", func() { encryptedValue := crypto.Encrypt([]byte("someEncryptedValueInPlainText")) encodedValue := base64.StdEncoding.EncodeToString(encryptedValue) valuePlainText := "someEncryptedValueInPlainText" - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ Value: encodedValue, ValuePlaintext: "", } kotsKind := &kotsutil.KotsKinds{ - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -459,15 +463,15 @@ var _ = Describe("Kots", func() { It("does not change the value if it cannot be decoded", func() { itemName := "some-item" - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ Value: "not-an-encoded-value", ValuePlaintext: "", } kotsKind := &kotsutil.KotsKinds{ - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -480,15 +484,15 @@ var _ = Describe("Kots", func() { It("does not change the value if it cannot be decrypted", func() { itemName := "some-item" encodedButNotEncryptedValue := base64.StdEncoding.EncodeToString([]byte("someEncryptedValueInPlainText")) - configValues := make(map[string]kotsv1beta.ConfigValue) - configValues[itemName] = kotsv1beta.ConfigValue{ + configValues := make(map[string]kotsv1beta1.ConfigValue) + configValues[itemName] = kotsv1beta1.ConfigValue{ Value: encodedButNotEncryptedValue, ValuePlaintext: "", } kotsKind := &kotsutil.KotsKinds{ - ConfigValues: &kotsv1beta.ConfigValues{ - Spec: kotsv1beta.ConfigValuesSpec{ + ConfigValues: &kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ Values: configValues, }, }, @@ -514,9 +518,9 @@ var _ = Describe("Kots", func() { It("returns false when the length of groups is zero", func() { kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{ - Spec: kotsv1beta.ConfigSpec{ - Groups: []kotsv1beta.ConfigGroup{}, + Config: &kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{}, }, }, } @@ -526,9 +530,9 @@ var _ = Describe("Kots", func() { It("returns true when the length of the groups is greater than zero", func() { kotsKind := &kotsutil.KotsKinds{ - Config: &kotsv1beta.Config{ - Spec: kotsv1beta.ConfigSpec{ - Groups: []kotsv1beta.ConfigGroup{ + Config: &kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ { Name: "group-item", }, @@ -741,3 +745,149 @@ func TestIsKotsKind(t *testing.T) { }) } } + +func TestGetImagesFromKotsKinds(t *testing.T) { + type args struct { + kotsKinds *kotsutil.KotsKinds + destRegistry *dockerregistrytypes.RegistryOptions + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "basic", + args: args{ + kotsKinds: &kotsutil.KotsKinds{ + KotsApplication: kotsv1beta1.Application{ + Spec: kotsv1beta1.ApplicationSpec{ + AdditionalImages: []string{ + "registry.replicated.com/appslug/image:version", + }, + }, + }, + Preflight: &troubleshootv1beta2.Preflight{ + Spec: troubleshootv1beta2.PreflightSpec{ + Collectors: []*troubleshootv1beta2.Collect{ + { + Run: &troubleshootv1beta2.Run{ + Image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + }, + }, + { + RunPod: &troubleshootv1beta2.RunPod{ + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:1", + }, + }, + }, + }, + }, + }, + }, + }, + SupportBundle: &troubleshootv1beta2.SupportBundle{ + Spec: troubleshootv1beta2.SupportBundleSpec{ + Collectors: []*troubleshootv1beta2.Collect{ + { + Run: &troubleshootv1beta2.Run{ + Image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + }, + }, + }, + }, + }, + }, + }, + want: []string{ + "registry.replicated.com/appslug/image:version", + "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + "nginx:1", + "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + }, + }, + { + name: "excludes images that already point to the destination registry", + args: args{ + kotsKinds: &kotsutil.KotsKinds{ + KotsApplication: kotsv1beta1.Application{ + Spec: kotsv1beta1.ApplicationSpec{ + AdditionalImages: []string{ + "registry.replicated.com/appslug/image:version", + }, + }, + }, + Preflight: &troubleshootv1beta2.Preflight{ + Spec: troubleshootv1beta2.PreflightSpec{ + Collectors: []*troubleshootv1beta2.Collect{ + { + Run: &troubleshootv1beta2.Run{ + Image: "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + }, + }, + { + Run: &troubleshootv1beta2.Run{ + Image: "testing.registry.com:5000/testing-ns/random-image:2", + }, + }, + { + RunPod: &troubleshootv1beta2.RunPod{ + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx:1", + }, + }, + }, + }, + }, + }, + }, + }, + SupportBundle: &troubleshootv1beta2.SupportBundle{ + Spec: troubleshootv1beta2.SupportBundleSpec{ + Collectors: []*troubleshootv1beta2.Collect{ + { + Run: &troubleshootv1beta2.Run{ + Image: "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + }, + }, + { + Run: &troubleshootv1beta2.Run{ + Image: "testing.registry.com:5000/testing-ns/random-image:1", + }, + }, + }, + }, + }, + }, + destRegistry: &dockerregistrytypes.RegistryOptions{ + Endpoint: "testing.registry.com:5000", + Namespace: "testing-ns", + Username: "testing-user-name", + Password: "testing-password", + }, + }, + want: []string{ + "registry.replicated.com/appslug/image:version", + "quay.io/replicatedcom/qa-kots-1:alpine-3.5", + "nginx:1", + "quay.io/replicatedcom/qa-kots-2:alpine-3.4", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + got, err := kotsutil.GetImagesFromKotsKinds(tt.args.kotsKinds, tt.args.destRegistry) + req.NoError(err) + + assert.ElementsMatch(t, tt.want, got) + }) + } +} diff --git a/pkg/midstream/write.go b/pkg/midstream/write.go index 89d82be208..6e5598a8c0 100644 --- a/pkg/midstream/write.go +++ b/pkg/midstream/write.go @@ -14,6 +14,7 @@ import ( "github.com/replicatedhq/kots/pkg/docker/registry" dockerregistrytypes "github.com/replicatedhq/kots/pkg/docker/registry/types" "github.com/replicatedhq/kots/pkg/image" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/k8sdoc" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" @@ -45,9 +46,9 @@ type WriteOptions struct { HTTPSProxyEnvValue string NoProxyEnvValue string NewHelmCharts []*kotsv1beta1.HelmChart - ProcessImageOptions image.ProcessImageOptions + ProcessImageOptions imagetypes.ProcessImageOptions License *kotsv1beta1.License - RenderedKotsKinds *kotsutil.KotsKinds + KotsKinds *kotsutil.KotsKinds IdentityConfig *kotsv1beta1.IdentityConfig UpstreamDir string Log *logger.CLILogger @@ -87,39 +88,52 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { return nil, errors.Wrap(err, "failed to find base images") } - kotsKindsImages, err := kotsutil.GetImagesFromKotsKinds(opts.RenderedKotsKinds, destRegistry) + kotsKindsImages, err := kotsutil.GetImagesFromKotsKinds(opts.KotsKinds, destRegistry) if err != nil { return nil, errors.Wrap(err, "failed to get images from kots kinds") } + allImages := append(baseImages, kotsKindsImages...) + + if err := image.UpdateInstallationImages(image.UpdateInstallationImagesOptions{ + Images: allImages, + KotsKinds: opts.KotsKinds, + IsAirgap: opts.ProcessImageOptions.IsAirgap, + UpstreamDir: opts.UpstreamDir, + DockerHubRegistryCreds: dockerHubRegistryCreds, + }); err != nil { + return nil, errors.Wrap(err, "failed to update installation images") + } + if opts.ProcessImageOptions.RewriteImages { - // A target registry is configured. Rewrite all images and copy them (if necessary) to the configured registry. - if opts.ProcessImageOptions.RegistrySettings.IsReadOnly { - opts.Log.ActionWithSpinner("Rewriting images") - io.WriteString(opts.ProcessImageOptions.ReportWriter, "Rewriting images\n") - } else { + // A target registry is configured. Rewrite all images to point to the configured destination registry, and copy them (if necessary). + if opts.ProcessImageOptions.CopyImages { opts.Log.ActionWithSpinner("Copying images") io.WriteString(opts.ProcessImageOptions.ReportWriter, "Copying images\n") - } - if opts.ProcessImageOptions.AirgapRoot == "" { - // This is an online installation. Pull and rewrite images from online and copy them (if necessary) to the configured registry. - rewriteResult, err := RewriteBaseImages(opts.ProcessImageOptions, baseImages, kotsKindsImages, opts.RenderedKotsKinds, opts.License, dockerHubRegistryCreds, opts.Log) - if err != nil { - return nil, errors.Wrap(err, "failed to rewrite base images") - } - images = rewriteResult.Images - opts.RenderedKotsKinds.Installation.Spec.KnownImages = rewriteResult.CheckedImages - } else { - // This is an airgapped installation. Copy and rewrite images from the airgap bundle to the configured registry. - result, err := ProcessAirgapImages(opts.ProcessImageOptions, baseImages, kotsKindsImages, opts.RenderedKotsKinds, opts.License, opts.Log) - if err != nil { - return nil, errors.Wrap(err, "failed to process airgap images") + if opts.ProcessImageOptions.AirgapRoot == "" { + err := image.CopyOnlineImages(opts.ProcessImageOptions, allImages, opts.KotsKinds, opts.License, dockerHubRegistryCreds, opts.Log) + if err != nil { + return nil, errors.Wrap(err, "failed to copy online images") + } + } else { + err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log) + if err != nil { + return nil, errors.Wrap(err, "failed to copy airgap images") + } } - images = result.KustomizeImages - opts.RenderedKotsKinds.Installation.Spec.KnownImages = result.KnownImages } + // Rewrite images to point to the configured destination registry. + opts.Log.ActionWithSpinner("Rewriting images") + io.WriteString(opts.ProcessImageOptions.ReportWriter, "Rewriting images\n") + + rewrittenImages, err := base.RewriteImages(allImages, *destRegistry) + if err != nil { + return nil, errors.Wrap(err, "failed to rewrite images") + } + images = rewrittenImages + // Use target registry credentials to create image pull secrets for all objects that have images. pullSecretRegistries = []string{opts.ProcessImageOptions.RegistrySettings.Hostname} pullSecretUsername = opts.ProcessImageOptions.RegistrySettings.Username @@ -131,16 +145,15 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { } } } else if opts.License != nil { - // A target registry is NOT configured. Find and rewrite private images to be proxied through proxy.replicated.com - findResult, err := findPrivateImages(opts, baseImages, kotsKindsImages, dockerHubRegistryCreds) + // A target registry is NOT configured. Rewrite private images to be proxied through proxy.replicated.com + rewrittenImages, err := base.RewritePrivateImages(baseImages, opts.KotsKinds, opts.License) if err != nil { - return nil, errors.Wrap(err, "failed to find private images") + return nil, errors.Wrap(err, "failed to rewrite private images") } - images = findResult.Images - opts.RenderedKotsKinds.Installation.Spec.KnownImages = findResult.CheckedImages + images = rewrittenImages // Use license to create image pull secrets for all objects that have private images. - pullSecretRegistries = registry.GetRegistryProxyInfo(opts.License, &opts.RenderedKotsKinds.Installation, &opts.RenderedKotsKinds.KotsApplication).ToSlice() + pullSecretRegistries = registry.GetRegistryProxyInfo(opts.License, &opts.KotsKinds.Installation, &opts.KotsKinds.KotsApplication).ToSlice() pullSecretUsername = opts.License.Spec.LicenseID pullSecretPassword = opts.License.Spec.LicenseID } @@ -166,11 +179,7 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { } pullSecrets.DockerHubSecret = dockerhubSecret - if err := kotsutil.SaveInstallation(&opts.RenderedKotsKinds.Installation, opts.UpstreamDir); err != nil { - return nil, errors.Wrap(err, "failed to save installation") - } - - m, err := CreateMidstream(opts.Base, images, objects, &pullSecrets, opts.RenderedKotsKinds.Identity, opts.IdentityConfig) + m, err := CreateMidstream(opts.Base, images, objects, &pullSecrets, opts.KotsKinds.Identity, opts.IdentityConfig) if err != nil { return nil, errors.Wrap(err, "failed to create midstream") } @@ -182,117 +191,6 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { return m, nil } -// RewriteBaseImages Will rewrite images found in base and copy them (if necessary) to the configured registry. -func RewriteBaseImages(options image.ProcessImageOptions, baseImages []string, kotsKindsImages []string, kotsKinds *kotsutil.KotsKinds, license *kotsv1beta1.License, dockerHubRegistryCreds registry.Credentials, log *logger.CLILogger) (*base.RewriteImagesResult, error) { - replicatedRegistryInfo := registry.GetRegistryProxyInfo(license, &kotsKinds.Installation, &kotsKinds.KotsApplication) - - rewriteImageOptions := base.RewriteImageOptions{ - BaseImages: baseImages, - KotsKindsImages: kotsKindsImages, - Log: log, - SourceRegistry: dockerregistrytypes.RegistryOptions{ - Endpoint: replicatedRegistryInfo.Registry, - ProxyEndpoint: replicatedRegistryInfo.Proxy, - UpstreamEndpoint: replicatedRegistryInfo.Upstream, - }, - DockerHubRegistry: dockerregistrytypes.RegistryOptions{ - Username: dockerHubRegistryCreds.Username, - Password: dockerHubRegistryCreds.Password, - }, - DestRegistry: dockerregistrytypes.RegistryOptions{ - Endpoint: options.RegistrySettings.Hostname, - Namespace: options.RegistrySettings.Namespace, - Username: options.RegistrySettings.Username, - Password: options.RegistrySettings.Password, - }, - ReportWriter: options.ReportWriter, - KotsKinds: kotsKinds, - IsAirgap: options.IsAirgap, - CopyImages: options.CopyImages, - } - if license != nil { - rewriteImageOptions.AppSlug = license.Spec.AppSlug - rewriteImageOptions.SourceRegistry.Username = license.Spec.LicenseID - rewriteImageOptions.SourceRegistry.Password = license.Spec.LicenseID - } - - rewriteResult, err := base.RewriteImages(rewriteImageOptions) - if err != nil { - return nil, errors.Wrap(err, "failed to rewrite images") - } - - return rewriteResult, nil -} - -// processAirgapImages Will rewrite images found in the airgap bundle/airgap root and copy them (if necessary) to the configured registry. -func ProcessAirgapImages(options image.ProcessImageOptions, baseImages []string, kotsKindsImages []string, kotsKinds *kotsutil.KotsKinds, license *kotsv1beta1.License, log *logger.CLILogger) (*base.ProcessAirgapImagesResult, error) { - replicatedRegistryInfo := registry.GetRegistryProxyInfo(license, &kotsKinds.Installation, &kotsKinds.KotsApplication) - - processAirgapImageOptions := base.ProcessAirgapImagesOptions{ - BaseImages: baseImages, - KotsKindsImages: kotsKindsImages, - RootDir: options.RootDir, - AirgapRoot: options.AirgapRoot, - AirgapBundle: options.AirgapBundle, - CreateAppDir: options.CreateAppDir, - PushImages: !options.RegistrySettings.IsReadOnly && options.PushImages, - Log: log, - ReplicatedRegistry: dockerregistrytypes.RegistryOptions{ - Endpoint: replicatedRegistryInfo.Registry, - ProxyEndpoint: replicatedRegistryInfo.Proxy, - UpstreamEndpoint: replicatedRegistryInfo.Upstream, - }, - ReportWriter: options.ReportWriter, - DestinationRegistry: dockerregistrytypes.RegistryOptions{ - Endpoint: options.RegistrySettings.Hostname, - Namespace: options.RegistrySettings.Namespace, - Username: options.RegistrySettings.Username, - Password: options.RegistrySettings.Password, - }, - KotsKinds: kotsKinds, - } - if license != nil { - processAirgapImageOptions.ReplicatedRegistry.Username = license.Spec.LicenseID - processAirgapImageOptions.ReplicatedRegistry.Password = license.Spec.LicenseID - } - - result, err := base.ProcessAirgapImages(processAirgapImageOptions) - if err != nil { - return nil, errors.Wrap(err, "failed to process airgap images") - } - - return result, nil -} - -// findPrivateImages Finds and rewrites private images to be proxied through proxy.replicated.com -func findPrivateImages(opts WriteOptions, baseImages []string, kotsKindsImages []string, dockerHubRegistryCreds registry.Credentials) (*base.FindPrivateImagesResult, error) { - replicatedRegistryInfo := registry.GetRegistryProxyInfo(opts.License, &opts.RenderedKotsKinds.Installation, &opts.RenderedKotsKinds.KotsApplication) - allPrivate := opts.RenderedKotsKinds.KotsApplication.Spec.ProxyPublicImages - - findPrivateImagesOptions := base.FindPrivateImagesOptions{ - BaseImages: baseImages, - KotsKindsImages: kotsKindsImages, - AppSlug: opts.License.Spec.AppSlug, - ReplicatedRegistry: dockerregistrytypes.RegistryOptions{ - Endpoint: replicatedRegistryInfo.Registry, - ProxyEndpoint: replicatedRegistryInfo.Proxy, - UpstreamEndpoint: replicatedRegistryInfo.Upstream, - }, - DockerHubRegistry: dockerregistrytypes.RegistryOptions{ - Username: dockerHubRegistryCreds.Username, - Password: dockerHubRegistryCreds.Password, - }, - Installation: &opts.RenderedKotsKinds.Installation, - AllImagesPrivate: allPrivate, - } - findResult, err := base.FindPrivateImages(findPrivateImagesOptions) - if err != nil { - return nil, errors.Wrap(err, "failed to find private images") - } - - return findResult, nil -} - func (m *Midstream) Write(options WriteOptions) error { if err := os.MkdirAll(options.MidstreamDir, 0744); err != nil { return errors.Wrap(err, "failed to mkdir") diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 416109e238..7c4d607834 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -17,6 +17,7 @@ import ( "github.com/replicatedhq/kots/pkg/crypto" "github.com/replicatedhq/kots/pkg/downstream" "github.com/replicatedhq/kots/pkg/image" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsadmconfig" "github.com/replicatedhq/kots/pkg/kotsutil" @@ -340,7 +341,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { return "", errors.Wrap(err, "failed to check if version needs configuration") } - processImageOptions := image.ProcessImageOptions{ + processImageOptions := imagetypes.ProcessImageOptions{ AppSlug: pullOptions.AppSlug, Namespace: pullOptions.Namespace, RewriteImages: pullOptions.RewriteImages, @@ -350,7 +351,6 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { IsAirgap: pullOptions.AirgapRoot != "", AirgapRoot: pullOptions.AirgapRoot, AirgapBundle: pullOptions.AirgapBundle, - PushImages: pullOptions.RewriteImageOptions.Hostname != "", CreateAppDir: pullOptions.CreateAppDir, ReportWriter: pullOptions.ReportWriter, } @@ -361,8 +361,8 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { } if processImageOptions.RewriteImages && processImageOptions.AirgapRoot != "" { // if this is an airgap install, we still need to process the images - if _, err = midstream.ProcessAirgapImages(processImageOptions, nil, nil, renderedKotsKinds, fetchOptions.License, log); err != nil { - return "", errors.Wrap(err, "failed to process airgap images") + if err := image.CopyAirgapImages(processImageOptions, log); err != nil { + return "", errors.Wrap(err, "failed to copy airgap images") } } return "", ErrConfigNeeded @@ -503,7 +503,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { NoProxyEnvValue: pullOptions.NoProxyEnvValue, NewHelmCharts: v1Beta1HelmCharts, License: fetchOptions.License, - RenderedKotsKinds: renderedKotsKinds, + KotsKinds: renderedKotsKinds, IdentityConfig: identityConfig, UpstreamDir: u.GetUpstreamDir(writeUpstreamOptions), Log: log, @@ -527,7 +527,10 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { processImageOptionsCopy := processImageOptions processImageOptionsCopy.Namespace = helmBaseCopy.Namespace - processImageOptionsCopy.PushImages = false // never push images more than once + if processImageOptions.IsAirgap { + // don't copy images if airgap, as all images would've been pushed from the airgap bundle. + processImageOptionsCopy.CopyImages = false + } writeMidstreamOptions.MidstreamDir = filepath.Join(u.GetOverlaysDir(writeUpstreamOptions), "midstream", helmBaseCopy.Path) writeMidstreamOptions.BaseDir = filepath.Join(u.GetBaseDir(writeUpstreamOptions), helmBaseCopy.Path) diff --git a/pkg/rendered/rendered.go b/pkg/rendered/rendered.go index 41c2ca4533..11a335dbe8 100644 --- a/pkg/rendered/rendered.go +++ b/pkg/rendered/rendered.go @@ -3,7 +3,7 @@ package rendered import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/apparchive" - "github.com/replicatedhq/kots/pkg/image" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "k8s.io/client-go/kubernetes" @@ -18,7 +18,7 @@ type WriteOptions struct { HelmDir string Log *logger.CLILogger KotsKinds *kotsutil.KotsKinds - ProcessImageOptions image.ProcessImageOptions + ProcessImageOptions imagetypes.ProcessImageOptions Clientset kubernetes.Interface } diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index 8419129ee6..01cc1e6729 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -13,7 +13,7 @@ import ( "github.com/replicatedhq/kots/pkg/base" "github.com/replicatedhq/kots/pkg/crypto" "github.com/replicatedhq/kots/pkg/downstream" - "github.com/replicatedhq/kots/pkg/image" + imagetypes "github.com/replicatedhq/kots/pkg/image/types" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" @@ -236,13 +236,13 @@ func Rewrite(rewriteOptions RewriteOptions) error { NoProxyEnvValue: rewriteOptions.NoProxyEnvValue, NewHelmCharts: v1Beta1HelmCharts, License: rewriteOptions.License, - RenderedKotsKinds: renderedKotsKinds, + KotsKinds: renderedKotsKinds, IdentityConfig: identityConfig, UpstreamDir: u.GetUpstreamDir(writeUpstreamOptions), Log: log, } - processImageOptions := image.ProcessImageOptions{ + processImageOptions := imagetypes.ProcessImageOptions{ AppSlug: rewriteOptions.AppSlug, Namespace: rewriteOptions.K8sNamespace, RewriteImages: rewriteOptions.RegistrySettings.Hostname != "", @@ -252,7 +252,6 @@ func Rewrite(rewriteOptions RewriteOptions) error { IsAirgap: rewriteOptions.IsAirgap, AirgapRoot: "", AirgapBundle: "", - PushImages: rewriteOptions.RegistrySettings.Hostname != "", CreateAppDir: false, ReportWriter: rewriteOptions.ReportWriter, } @@ -274,7 +273,10 @@ func Rewrite(rewriteOptions RewriteOptions) error { processImageOptionsCopy := processImageOptions processImageOptionsCopy.Namespace = helmBaseCopy.Namespace - processImageOptionsCopy.CopyImages = false // don't copy images more than once + if processImageOptions.IsAirgap { + // don't copy images if airgap, as all images would've been pushed from the airgap bundle. + processImageOptionsCopy.CopyImages = false + } writeMidstreamOptions.MidstreamDir = filepath.Join(u.GetOverlaysDir(writeUpstreamOptions), "midstream", helmBaseCopy.Path) writeMidstreamOptions.BaseDir = filepath.Join(u.GetBaseDir(writeUpstreamOptions), helmBaseCopy.Path)