From 3c242463b43672f3da43d5329ba70148ba9deda1 Mon Sep 17 00:00:00 2001 From: Craig O'Donnell Date: Tue, 16 Apr 2024 11:32:18 -0400 Subject: [PATCH] pass embedded cluster artifacts in upstream upgrade (#4549) --- cmd/kots/cli/admin-console-push-images.go | 2 +- go.mod | 2 +- go.sum | 6 + pkg/embeddedcluster/util.go | 32 +----- pkg/embeddedcluster/util_test.go | 10 +- pkg/image/airgap.go | 99 +++++++++-------- pkg/image/airgap_test.go | 40 +++---- pkg/image/online.go | 14 --- pkg/image/types/types.go | 8 +- pkg/imageutil/image.go | 39 +++++++ pkg/imageutil/image_test.go | 104 ++++++++++++++++++ pkg/kotsadm/main.go | 2 +- pkg/midstream/write.go | 10 +- pkg/pull/pull.go | 3 +- pkg/rewrite/rewrite.go | 1 + .../upstream/userdata/installation.yaml | 14 +++ .../kotsKinds/userdata/installation.yaml | 5 + .../upstream/userdata/installation.yaml | 5 + pkg/upstream/fetch.go | 26 +++++ pkg/upstream/replicated.go | 8 +- pkg/upstream/types/types.go | 2 + pkg/upstream/upgrade.go | 2 +- pkg/upstream/write.go | 1 + 23 files changed, 309 insertions(+), 126 deletions(-) create mode 100644 pkg/tests/pull/cases/airgap/upstream/userdata/installation.yaml diff --git a/cmd/kots/cli/admin-console-push-images.go b/cmd/kots/cli/admin-console-push-images.go index a4e2809d3f..90e8f31d9d 100644 --- a/cmd/kots/cli/admin-console-push-images.go +++ b/cmd/kots/cli/admin-console-push-images.go @@ -54,7 +54,7 @@ func AdminPushImagesCmd() *cobra.Command { } if _, err := os.Stat(imageSource); err == nil { - _, err = image.TagAndPushImagesFromBundle(imageSource, *options) + err = image.TagAndPushImagesFromBundle(imageSource, *options) if err != nil { return errors.Wrap(err, "failed to push images") } diff --git a/go.mod b/go.mod index 3389983aeb..ff07836485 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/replicatedhq/embedded-cluster-kinds v1.1.2 - github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397 + github.com/replicatedhq/kotskinds v0.0.0-20240416132840-4e646b87f7a1 github.com/replicatedhq/kurlkinds v1.5.0 github.com/replicatedhq/troubleshoot v0.87.0 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq diff --git a/go.sum b/go.sum index d2656f09ce..cfa281994d 100644 --- a/go.sum +++ b/go.sum @@ -1310,6 +1310,12 @@ github.com/replicatedhq/embedded-cluster-kinds v1.1.2 h1:2ITzcUzh5uh0fsnfZsVHvkw github.com/replicatedhq/embedded-cluster-kinds v1.1.2/go.mod h1:LheSDOgMngMRAbwAj0sVZUVv2ciKIVR2bYTMeOBGwlg= github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397 h1:JNuBcFH9D3Osyi+1QUwdvaAklEd6HXznqZDfpWlr73M= github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20240402213802-a6d75e97be70 h1:55fMr60YysSf+ac5zeW+xTIIJ8edUpAjorQyFn0iP2c= +github.com/replicatedhq/kotskinds v0.0.0-20240402213802-a6d75e97be70/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20240415152931-2837245679f5 h1:SI1Ar5c6QWTm1IpPOO/CVv8dktdqd8KaQUEOeHEbhgM= +github.com/replicatedhq/kotskinds v0.0.0-20240415152931-2837245679f5/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20240416132840-4e646b87f7a1 h1:+RvMZ646tQTRzWFZTy6mnmgWJZOLFu6B9PXv8tcIcFY= +github.com/replicatedhq/kotskinds v0.0.0-20240416132840-4e646b87f7a1/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= github.com/replicatedhq/kurlkinds v1.5.0 h1:zZ0PKNeh4kXvSzVGkn62DKTo314GxhXg1TSB3azURMc= github.com/replicatedhq/kurlkinds v1.5.0/go.mod h1:rUpBMdC81IhmJNCWMU/uRsMETv9P0xFoMvdSP/TAr5A= github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 h1:eRlNDHxGfVkPCRXbA4BfQJvt5DHjFiTtWy3R/t4djyY= diff --git a/pkg/embeddedcluster/util.go b/pkg/embeddedcluster/util.go index 240581da92..667398a2a8 100644 --- a/pkg/embeddedcluster/util.go +++ b/pkg/embeddedcluster/util.go @@ -5,13 +5,11 @@ import ( "context" "encoding/json" "fmt" - "regexp" "sort" "time" embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" "github.com/replicatedhq/kots/pkg/k8sutil" - "github.com/replicatedhq/kots/pkg/logger" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,13 +24,6 @@ const configMapNamespace = "embedded-cluster" // ErrNoInstallations is returned when no installation object is found in the cluster. var ErrNoInstallations = fmt.Errorf("no installations found") -var ( - chartsArtifactRegex = regexp.MustCompile(`\/embedded-cluster\/(charts\.tar\.gz):`) - imagesArtifactRegex = regexp.MustCompile(`\/embedded-cluster\/(images-.+\.tar):`) - binaryArtifactRegex = regexp.MustCompile(`\/embedded-cluster\/(embedded-cluster-.+):`) - metadataArtifactRegex = regexp.MustCompile(`\/embedded-cluster\/(version-metadata\.json):`) -) - // ReadConfigMap will read the Kurl config from a configmap func ReadConfigMap(client kubernetes.Interface) (*corev1.ConfigMap, error) { return client.CoreV1().ConfigMaps(configMapNamespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) @@ -113,27 +104,16 @@ func ClusterConfig(ctx context.Context) (*embeddedclusterv1beta1.ConfigSpec, err } func getArtifactsFromInstallation(installation kotsv1beta1.Installation, appSlug string) *embeddedclusterv1beta1.ArtifactsLocation { - if len(installation.Spec.EmbeddedClusterArtifacts) == 0 { + if installation.Spec.EmbeddedClusterArtifacts == nil { return nil } - artifacts := &embeddedclusterv1beta1.ArtifactsLocation{} - for _, artifact := range installation.Spec.EmbeddedClusterArtifacts { - switch { - case chartsArtifactRegex.MatchString(artifact): - artifacts.HelmCharts = artifact - case imagesArtifactRegex.MatchString(artifact): - artifacts.Images = artifact - case binaryArtifactRegex.MatchString(artifact): - artifacts.EmbeddedClusterBinary = artifact - case metadataArtifactRegex.MatchString(artifact): - artifacts.EmbeddedClusterMetadata = artifact - default: - logger.Warnf("unknown artifact in installation: %s", artifact) - } + return &embeddedclusterv1beta1.ArtifactsLocation{ + EmbeddedClusterBinary: installation.Spec.EmbeddedClusterArtifacts.BinaryAmd64, + Images: installation.Spec.EmbeddedClusterArtifacts.ImagesAmd64, + HelmCharts: installation.Spec.EmbeddedClusterArtifacts.Charts, + EmbeddedClusterMetadata: installation.Spec.EmbeddedClusterArtifacts.Metadata, } - - return artifacts } // startClusterUpgrade will create a new installation with the provided config. diff --git a/pkg/embeddedcluster/util_test.go b/pkg/embeddedcluster/util_test.go index deb293d396..7305476721 100644 --- a/pkg/embeddedcluster/util_test.go +++ b/pkg/embeddedcluster/util_test.go @@ -31,11 +31,11 @@ func Test_getArtifactsFromInstallation(t *testing.T) { args: args{ installation: kotsv1beta1.Installation{ Spec: kotsv1beta1.InstallationSpec{ - EmbeddedClusterArtifacts: []string{ - "onprem.registry.com/my-app/embedded-cluster/charts.tar.gz:v1", - "onprem.registry.com/my-app/embedded-cluster/images-amd64.tar:v1", - "onprem.registry.com/my-app/embedded-cluster/embedded-cluster-amd64:v1", - "onprem.registry.com/my-app/embedded-cluster/version-metadata.json:v1", + EmbeddedClusterArtifacts: &kotsv1beta1.EmbeddedClusterArtifacts{ + Charts: "onprem.registry.com/my-app/embedded-cluster/charts.tar.gz:v1", + ImagesAmd64: "onprem.registry.com/my-app/embedded-cluster/images-amd64.tar:v1", + BinaryAmd64: "onprem.registry.com/my-app/embedded-cluster/embedded-cluster-amd64:v1", + Metadata: "onprem.registry.com/my-app/embedded-cluster/version-metadata.json:v1", }, }, }, diff --git a/pkg/image/airgap.go b/pkg/image/airgap.go index 0dea825898..d6fb2f8f52 100644 --- a/pkg/image/airgap.go +++ b/pkg/image/airgap.go @@ -27,6 +27,7 @@ import ( "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" oras "oras.land/oras-go/v2" orasfile "oras.land/oras-go/v2/content/file" orasremote "oras.land/oras-go/v2/registry/remote" @@ -108,9 +109,9 @@ func WriteProgressLine(progressWriter io.Writer, line string) { } // CopyAirgapImages pushes images found in the airgap bundle/airgap root to the configured registry. -func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) (*imagetypes.CopyAirgapImagesResult, error) { +func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) error { if opts.AirgapBundle == "" { - return &imagetypes.CopyAirgapImagesResult{}, nil + return nil } pushOpts := imagetypes.PushImagesOptions{ @@ -125,27 +126,25 @@ func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger LogForUI: true, } - copyResult, err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts) + err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts) if err != nil { - return nil, errors.Wrap(err, "failed to push images from bundle") + return errors.Wrap(err, "failed to push images from bundle") } - return &imagetypes.CopyAirgapImagesResult{ - EmbeddedClusterArtifacts: copyResult.EmbeddedClusterArtifacts, - }, nil + return nil } -func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) (*imagetypes.CopyAirgapImagesResult, error) { +func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) error { airgap, err := kotsutil.FindAirgapMetaInBundle(airgapBundle) if err != nil { - return nil, errors.Wrap(err, "failed to find airgap meta") + return errors.Wrap(err, "failed to find airgap meta") } switch airgap.Spec.Format { case dockertypes.FormatDockerRegistry: extractedBundle, err := os.MkdirTemp("", "extracted-airgap-kots") if err != nil { - return nil, errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle") + return errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle") } defer os.RemoveAll(extractedBundle) @@ -155,34 +154,32 @@ func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImag }, } if err := tarGz.Unarchive(airgapBundle, extractedBundle); err != nil { - return nil, errors.Wrap(err, "falied to unarchive airgap bundle") + return errors.Wrap(err, "falied to unarchive airgap bundle") } if err := PushImagesFromTempRegistry(extractedBundle, airgap.Spec.SavedImages, options); err != nil { - return nil, errors.Wrap(err, "failed to push images from docker registry bundle") + return errors.Wrap(err, "failed to push images from docker registry bundle") } case dockertypes.FormatDockerArchive, "": if err := PushImagesFromDockerArchiveBundle(airgapBundle, options); err != nil { - return nil, errors.Wrap(err, "failed to push images from docker archive bundle") + return errors.Wrap(err, "failed to push images from docker archive bundle") } default: - return nil, errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format) + return errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format) } pushEmbeddedArtifactsOpts := imagetypes.PushEmbeddedClusterArtifactsOptions{ - Registry: options.Registry, - Tag: imageutil.SanitizeTag(fmt.Sprintf("%s-%s-%s", airgap.Spec.ChannelID, airgap.Spec.UpdateCursor, airgap.Spec.VersionLabel)), - HTTPClient: orasretry.DefaultClient, + Registry: options.Registry, + ChannelID: airgap.Spec.ChannelID, + UpdateCursor: airgap.Spec.UpdateCursor, + VersionLabel: airgap.Spec.VersionLabel, + HTTPClient: orasretry.DefaultClient, } - pushedArtifacts, err := PushEmbeddedClusterArtifacts(airgapBundle, pushEmbeddedArtifactsOpts) + err = PushEmbeddedClusterArtifacts(airgapBundle, airgap.Spec.EmbeddedClusterArtifacts, pushEmbeddedArtifactsOpts) if err != nil { - return nil, errors.Wrap(err, "failed to push embedded cluster artifacts") - } - - result := &imagetypes.CopyAirgapImagesResult{ - EmbeddedClusterArtifacts: pushedArtifacts, + return errors.Wrap(err, "failed to push embedded cluster artifacts") } - return result, nil + return nil } func PushImagesFromTempRegistry(airgapRootDir string, imageList []string, options imagetypes.PushImagesOptions) error { @@ -684,59 +681,58 @@ func reportWriterWithProgress(imageInfos map[string]*imagetypes.ImageInfo, repor return pipeWriter } -func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbeddedClusterArtifactsOptions) ([]string, error) { +func PushEmbeddedClusterArtifacts(airgapBundle string, artifactsToPush *kotsv1beta1.EmbeddedClusterArtifacts, opts imagetypes.PushEmbeddedClusterArtifactsOptions) error { tmpDir, err := os.MkdirTemp("", "embedded-cluster-artifacts") if err != nil { - return nil, errors.Wrap(err, "failed to create temp directory") + return errors.Wrap(err, "failed to create temp directory") } defer os.RemoveAll(tmpDir) fileReader, err := os.Open(airgapBundle) if err != nil { - return nil, errors.Wrap(err, "failed to open file") + return errors.Wrap(err, "failed to open file") } defer fileReader.Close() gzipReader, err := gzip.NewReader(fileReader) if err != nil { - return nil, errors.Wrap(err, "failed to get new gzip reader") + return errors.Wrap(err, "failed to get new gzip reader") } defer gzipReader.Close() var artifacts []string tarReader := tar.NewReader(gzipReader) - pushedArtifacts := make([]string, 0) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { - return nil, errors.Wrap(err, "failed to get read archive") + return errors.Wrap(err, "failed to get read archive") } if header.Typeflag != tar.TypeReg { continue } - if filepath.Dir(header.Name) != "embedded-cluster" { + if !shouldPushArtifact(header.Name, artifactsToPush) { continue } dstFilePath := filepath.Join(tmpDir, header.Name) if err := os.MkdirAll(filepath.Dir(dstFilePath), 0755); err != nil { - return nil, errors.Wrap(err, "failed to create path") + return errors.Wrap(err, "failed to create path") } dstFile, err := os.Create(dstFilePath) if err != nil { - return nil, errors.Wrap(err, "failed to create file") + return errors.Wrap(err, "failed to create file") } if _, err := io.Copy(dstFile, tarReader); err != nil { dstFile.Close() - return nil, errors.Wrap(err, "failed to copy file data") + return errors.Wrap(err, "failed to copy file data") } dstFile.Close() @@ -744,10 +740,16 @@ func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbed } for i, dstFilePath := range artifacts { - name := filepath.Base(dstFilePath) - repository := filepath.Join("embedded-cluster", imageutil.SanitizeRepo(name)) + ociArtifactPath := imageutil.NewEmbeddedClusterOCIArtifactPath(dstFilePath, imageutil.EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: opts.Registry.Endpoint, + RegistryNamespace: opts.Registry.Namespace, + ChannelID: opts.ChannelID, + UpdateCursor: opts.UpdateCursor, + VersionLabel: opts.VersionLabel, + }) + artifactFile := imagetypes.OCIArtifactFile{ - Name: name, + Name: ociArtifactPath.Name, Path: dstFilePath, MediaType: EmbeddedClusterMediaType, } @@ -756,20 +758,31 @@ func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbed Files: []imagetypes.OCIArtifactFile{artifactFile}, ArtifactType: EmbeddedClusterArtifactType, Registry: opts.Registry, - Repository: repository, - Tag: opts.Tag, + Repository: ociArtifactPath.Repository, + Tag: ociArtifactPath.Tag, HTTPClient: opts.HTTPClient, } fmt.Printf("Pushing embedded cluster artifacts (%d/%d)\n", i+1, len(artifacts)) - artifact := fmt.Sprintf("%s:%s", filepath.Join(opts.Registry.Endpoint, opts.Registry.Namespace, repository), opts.Tag) if err := pushOCIArtifact(pushOCIArtifactOpts); err != nil { - return nil, errors.Wrapf(err, "failed to push oci artifact %s", name) + return errors.Wrapf(err, "failed to push oci artifact %s", ociArtifactPath.Name) } - pushedArtifacts = append(pushedArtifacts, artifact) } - return pushedArtifacts, nil + return nil +} + +func shouldPushArtifact(artifactPath string, artifactsToPush *kotsv1beta1.EmbeddedClusterArtifacts) bool { + if artifactsToPush == nil { + return false + } + + switch artifactPath { + case artifactsToPush.BinaryAmd64, artifactsToPush.Charts, artifactsToPush.ImagesAmd64, artifactsToPush.Metadata: + return true + default: + return false + } } func pushOCIArtifact(opts imagetypes.PushOCIArtifactOptions) error { diff --git a/pkg/image/airgap_test.go b/pkg/image/airgap_test.go index c10a913038..a015eca92a 100644 --- a/pkg/image/airgap_test.go +++ b/pkg/image/airgap_test.go @@ -16,16 +16,20 @@ import ( dockertypes "github.com/replicatedhq/kots/pkg/docker/types" "github.com/replicatedhq/kots/pkg/image/types" imagetypes "github.com/replicatedhq/kots/pkg/image/types" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/require" ) func TestPushEmbeddedClusterArtifacts(t *testing.T) { testAppSlug := "test-app" - testTag := "test-tag" + testChannelID := "test-tag" + testUpdateCursor := "test-cursor" + testVersionLabel := "test-version" tests := []struct { name string airgapFiles map[string][]byte + artifactsToPush *kotsv1beta1.EmbeddedClusterArtifacts wantRegistryArtifacts map[string]string wantErr bool }{ @@ -36,6 +40,7 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { "app.tar.gz": []byte("this-is-the-app-archive"), "images/something": []byte("this-is-an-image"), }, + artifactsToPush: nil, wantRegistryArtifacts: map[string]string{}, wantErr: false, }, @@ -51,12 +56,17 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { "embedded-cluster/version-metadata.json": []byte("this-is-the-metadata"), "embedded-cluster/some-file-TBD": []byte("this-is-an-arbitrary-file"), }, + artifactsToPush: &kotsv1beta1.EmbeddedClusterArtifacts{ + BinaryAmd64: "embedded-cluster/test-app", + ImagesAmd64: "embedded-cluster/images-amd64.tar", + Charts: "embedded-cluster/charts.tar.gz", + Metadata: "embedded-cluster/version-metadata.json", + }, wantRegistryArtifacts: map[string]string{ - fmt.Sprintf("%s/embedded-cluster/test-app", testAppSlug): testTag, - fmt.Sprintf("%s/embedded-cluster/charts.tar.gz", testAppSlug): testTag, - fmt.Sprintf("%s/embedded-cluster/images-amd64.tar", testAppSlug): testTag, - fmt.Sprintf("%s/embedded-cluster/version-metadata.json", testAppSlug): testTag, - fmt.Sprintf("%s/embedded-cluster/some-file-tbd", testAppSlug): testTag, + fmt.Sprintf("%s/embedded-cluster/test-app", testAppSlug): fmt.Sprintf("%s-%s-%s", testChannelID, testUpdateCursor, testVersionLabel), + fmt.Sprintf("%s/embedded-cluster/charts.tar.gz", testAppSlug): fmt.Sprintf("%s-%s-%s", testChannelID, testUpdateCursor, testVersionLabel), + fmt.Sprintf("%s/embedded-cluster/images-amd64.tar", testAppSlug): fmt.Sprintf("%s-%s-%s", testChannelID, testUpdateCursor, testVersionLabel), + fmt.Sprintf("%s/embedded-cluster/version-metadata.json", testAppSlug): fmt.Sprintf("%s-%s-%s", testChannelID, testUpdateCursor, testVersionLabel), }, wantErr: false, }, @@ -83,26 +93,18 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { Endpoint: u.Host, Namespace: testAppSlug, }, - Tag: testTag, - HTTPClient: mockRegistryServer.Client(), - } - gotArtifacts, err := PushEmbeddedClusterArtifacts(airgapBundle, opts) - if (err != nil) != tt.wantErr { - t.Errorf("PushEmbeddedClusterArtifacts() error = %v, wantErr %v", err, tt.wantErr) + ChannelID: testChannelID, + UpdateCursor: testUpdateCursor, + VersionLabel: testVersionLabel, + HTTPClient: mockRegistryServer.Client(), } - + err = PushEmbeddedClusterArtifacts(airgapBundle, tt.artifactsToPush, opts) if tt.wantErr { req.Error(err) } else { req.NoError(err) } - wantArtifacts := make([]string, 0) - for repo, tag := range tt.wantRegistryArtifacts { - wantArtifacts = append(wantArtifacts, fmt.Sprintf("%s/%s:%s", u.Host, repo, tag)) - } - req.ElementsMatch(wantArtifacts, gotArtifacts) - // validate that each of the expected artifacts were pushed to the registry req.Equal(tt.wantRegistryArtifacts, pushedRegistryArtifacts) }) diff --git a/pkg/image/online.go b/pkg/image/online.go index 146857cb3d..93bcb4afa4 100644 --- a/pkg/image/online.go +++ b/pkg/image/online.go @@ -138,20 +138,6 @@ func UpdateInstallationImages(opts UpdateInstallationImagesOptions) error { return nil } -func UpdateInstallationEmbeddedClusterArtifacts(opts UpdateInstallationEmbeddedClusterArtifactsOptions) error { - if opts.KotsKinds == nil { - return nil - } - - opts.KotsKinds.Installation.Spec.EmbeddedClusterArtifacts = opts.Artifacts - - if err := kotsutil.SaveInstallation(&opts.KotsKinds.Installation, opts.UpstreamDir); err != nil { - return errors.Wrap(err, "failed to save installation") - } - - 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 { diff --git a/pkg/image/types/types.go b/pkg/image/types/types.go index 64e5b59638..720fe43d7c 100644 --- a/pkg/image/types/types.go +++ b/pkg/image/types/types.go @@ -84,9 +84,11 @@ type LayerInfo struct { } type PushEmbeddedClusterArtifactsOptions struct { - Registry dockerregistrytypes.RegistryOptions - Tag string - HTTPClient *http.Client + Registry dockerregistrytypes.RegistryOptions + ChannelID string + UpdateCursor string + VersionLabel string + HTTPClient *http.Client } type PushOCIArtifactOptions struct { diff --git a/pkg/imageutil/image.go b/pkg/imageutil/image.go index df1d7099e5..7f10a1ad4d 100644 --- a/pkg/imageutil/image.go +++ b/pkg/imageutil/image.go @@ -3,6 +3,7 @@ package imageutil import ( "fmt" "path" + "path/filepath" "strings" "github.com/containers/image/v5/docker/reference" @@ -334,3 +335,41 @@ func SanitizeRepo(repo string) string { repo = strings.Join(dockerref.NameRegexp.FindAllString(repo, -1), "") return repo } + +type OCIArtifactPath struct { + Name string + RegistryHost string + RegistryNamespace string + Repository string + Tag string +} + +func (p *OCIArtifactPath) String() string { + if p.RegistryNamespace == "" { + return fmt.Sprintf("%s:%s", filepath.Join(p.RegistryHost, p.Repository), p.Tag) + } + return fmt.Sprintf("%s:%s", filepath.Join(p.RegistryHost, p.RegistryNamespace, p.Repository), p.Tag) +} + +type EmbeddedClusterArtifactOCIPathOptions struct { + RegistryHost string + RegistryNamespace string + ChannelID string + UpdateCursor string + VersionLabel string +} + +// NewEmbeddedClusterOCIArtifactPath returns the OCI path for an embedded cluster artifact given +// the artifact filename and details about the configured registry and channel release. +func NewEmbeddedClusterOCIArtifactPath(filename string, opts EmbeddedClusterArtifactOCIPathOptions) *OCIArtifactPath { + name := filepath.Base(filename) + repository := filepath.Join("embedded-cluster", SanitizeRepo(name)) + tag := SanitizeTag(fmt.Sprintf("%s-%s-%s", opts.ChannelID, opts.UpdateCursor, opts.VersionLabel)) + return &OCIArtifactPath{ + Name: name, + RegistryHost: opts.RegistryHost, + RegistryNamespace: opts.RegistryNamespace, + Repository: repository, + Tag: tag, + } +} diff --git a/pkg/imageutil/image_test.go b/pkg/imageutil/image_test.go index bb8436d011..3e4a584371 100644 --- a/pkg/imageutil/image_test.go +++ b/pkg/imageutil/image_test.go @@ -1198,3 +1198,107 @@ func TestSanitizeRepo(t *testing.T) { }) } } + +func TestEmbeddedClusterArtifactOCIPath(t *testing.T) { + type args struct { + filename string + opts EmbeddedClusterArtifactOCIPathOptions + } + tests := []struct { + name string + args args + want string + }{ + { + name: "happy path for binary", + args: args{ + filename: "embedded-cluster/embedded-cluster-amd64", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "1.0.0", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/embedded-cluster-amd64:test-channel-id-1-1.0.0", + }, + { + name: "happy path for charts bundle", + args: args{ + filename: "embedded-cluster/charts.tar.gz", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "1.0.0", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/charts.tar.gz:test-channel-id-1-1.0.0", + }, + { + name: "happy path for image bundle", + args: args{ + filename: "embedded-cluster/images-amd64.tar", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "1.0.0", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/images-amd64.tar:test-channel-id-1-1.0.0", + }, + { + name: "happy path for version metadata", + args: args{ + filename: "embedded-cluster/version-metadata.json", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "1.0.0", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/version-metadata.json:test-channel-id-1-1.0.0", + }, + { + name: "file with name that needs to be sanitized", + args: args{ + filename: "A file with spaces.tar.gz", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "1.0.0", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/afilewithspaces.tar.gz:test-channel-id-1-1.0.0", + }, + { + name: "version label name that needs to be sanitized", + args: args{ + filename: "test.txt", + opts: EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: "registry.example.com", + RegistryNamespace: "my-app", + ChannelID: "test-channel-id", + UpdateCursor: "1", + VersionLabel: "A version with spaces", + }, + }, + want: "registry.example.com/my-app/embedded-cluster/test.txt:test-channel-id-1-Aversionwithspaces", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewEmbeddedClusterOCIArtifactPath(tt.args.filename, tt.args.opts); got.String() != tt.want { + t.Errorf("EmbeddedClusterArtifactOCIPath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/kotsadm/main.go b/pkg/kotsadm/main.go index 337c8374e9..cf87267135 100644 --- a/pkg/kotsadm/main.go +++ b/pkg/kotsadm/main.go @@ -164,7 +164,7 @@ func Deploy(deployOptions types.DeployOptions, log *logger.CLILogger) error { } if !deployOptions.DisableImagePush { - _, err := image.TagAndPushImagesFromBundle(deployOptions.AirgapBundle, pushOptions) + err := image.TagAndPushImagesFromBundle(deployOptions.AirgapBundle, pushOptions) if err != nil { return errors.Wrap(err, "failed to tag and push app images from path") } diff --git a/pkg/midstream/write.go b/pkg/midstream/write.go index 795d702d1e..173539e53f 100644 --- a/pkg/midstream/write.go +++ b/pkg/midstream/write.go @@ -112,18 +112,10 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { io.WriteString(opts.ProcessImageOptions.ReportWriter, "Copying images\n") if opts.ProcessImageOptions.IsAirgap { - copyResult, err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log) + err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log) if err != nil { return nil, errors.Wrap(err, "failed to copy airgap images") } - - if err := image.UpdateInstallationEmbeddedClusterArtifacts(image.UpdateInstallationEmbeddedClusterArtifactsOptions{ - Artifacts: copyResult.EmbeddedClusterArtifacts, - KotsKinds: opts.KotsKinds, - UpstreamDir: opts.UpstreamDir, - }); err != nil { - return nil, errors.Wrap(err, "failed to update installation airgap artifacts") - } } else { err := image.CopyOnlineImages(opts.ProcessImageOptions, allImages, opts.KotsKinds, opts.License, dockerHubRegistryCreds, opts.Log) if err != nil { diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 7ccb73fd20..830f1b33ca 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -216,6 +216,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { fetchOptions.CurrentVersionIsRequired = installation.Spec.IsRequired fetchOptions.CurrentReplicatedRegistryDomain = installation.Spec.ReplicatedRegistryDomain fetchOptions.CurrentReplicatedProxyDomain = installation.Spec.ReplicatedProxyDomain + fetchOptions.CurrentEmbeddedClusterArtifacts = installation.Spec.EmbeddedClusterArtifacts } if pullOptions.AirgapRoot != "" { @@ -364,7 +365,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { } if processImageOptions.RewriteImages && processImageOptions.IsAirgap { // if this is an airgap install, we still need to process the images - if _, err := image.CopyAirgapImages(processImageOptions, log); err != nil { + if err := image.CopyAirgapImages(processImageOptions, log); err != nil { return "", errors.Wrap(err, "failed to copy airgap images") } } diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index b49aefc535..95dde7e9cd 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -77,6 +77,7 @@ func Rewrite(rewriteOptions RewriteOptions) error { CurrentReplicatedRegistryDomain: rewriteOptions.Installation.Spec.ReplicatedRegistryDomain, CurrentReplicatedProxyDomain: rewriteOptions.Installation.Spec.ReplicatedProxyDomain, CurrentReplicatedChartNames: rewriteOptions.Installation.Spec.ReplicatedChartNames, + CurrentEmbeddedClusterArtifacts: rewriteOptions.Installation.Spec.EmbeddedClusterArtifacts, EncryptionKey: rewriteOptions.Installation.Spec.EncryptionKey, License: rewriteOptions.License, AppSequence: rewriteOptions.AppSequence, diff --git a/pkg/tests/pull/cases/airgap/upstream/userdata/installation.yaml b/pkg/tests/pull/cases/airgap/upstream/userdata/installation.yaml new file mode 100644 index 0000000000..af57ecf3e8 --- /dev/null +++ b/pkg/tests/pull/cases/airgap/upstream/userdata/installation.yaml @@ -0,0 +1,14 @@ +apiVersion: kots.io/v1beta1 +kind: Installation +metadata: + creationTimestamp: null + name: my-app +spec: + channelID: 1vusIYZLAVxMG6q760OJmRKj5i5 + channelName: My Channel + embeddedClusterArtifacts: + binaryAmd64: ttl.sh/replicated/binary-amd64:v1 + imagesAmd64: ttl.sh/replicated/images-amd64:v1 + charts: ttl.sh/replicated/charts.tar.gz:v1 + metadata: ttl.sh/replicated/metadata.json:v1 +status: {} diff --git a/pkg/tests/pull/cases/airgap/wantResults/kotsKinds/userdata/installation.yaml b/pkg/tests/pull/cases/airgap/wantResults/kotsKinds/userdata/installation.yaml index 825d8239f2..d147c12f23 100644 --- a/pkg/tests/pull/cases/airgap/wantResults/kotsKinds/userdata/installation.yaml +++ b/pkg/tests/pull/cases/airgap/wantResults/kotsKinds/userdata/installation.yaml @@ -6,6 +6,11 @@ metadata: spec: channelID: 1vusIYZLAVxMG6q760OJmRKj5i5 channelName: My Channel + embeddedClusterArtifacts: + binaryAmd64: ttl.sh/replicated/binary-amd64:v1 + imagesAmd64: ttl.sh/replicated/images-amd64:v1 + charts: ttl.sh/replicated/charts.tar.gz:v1 + metadata: ttl.sh/replicated/metadata.json:v1 knownImages: - image: alpine isPrivate: true diff --git a/pkg/tests/pull/cases/airgap/wantResults/upstream/userdata/installation.yaml b/pkg/tests/pull/cases/airgap/wantResults/upstream/userdata/installation.yaml index 825d8239f2..d147c12f23 100644 --- a/pkg/tests/pull/cases/airgap/wantResults/upstream/userdata/installation.yaml +++ b/pkg/tests/pull/cases/airgap/wantResults/upstream/userdata/installation.yaml @@ -6,6 +6,11 @@ metadata: spec: channelID: 1vusIYZLAVxMG6q760OJmRKj5i5 channelName: My Channel + embeddedClusterArtifacts: + binaryAmd64: ttl.sh/replicated/binary-amd64:v1 + imagesAmd64: ttl.sh/replicated/images-amd64:v1 + charts: ttl.sh/replicated/charts.tar.gz:v1 + metadata: ttl.sh/replicated/metadata.json:v1 knownImages: - image: alpine isPrivate: true diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go index b7663280f8..9563623b85 100644 --- a/pkg/upstream/fetch.go +++ b/pkg/upstream/fetch.go @@ -5,9 +5,11 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/crypto" + "github.com/replicatedhq/kots/pkg/imageutil" "github.com/replicatedhq/kots/pkg/replicatedapp" "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) func FetchUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*types.Upstream, error) { @@ -50,6 +52,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*ty pickReplicatedRegistryDomain(fetchOptions), pickReplicatedProxyDomain(fetchOptions), pickReplicatedChartNames(fetchOptions), + pickEmbeddedClusterArtifacts(fetchOptions), fetchOptions.AppSlug, fetchOptions.AppSequence, fetchOptions.Airgap != nil, @@ -118,3 +121,26 @@ func pickReplicatedChartNames(fetchOptions *types.FetchOptions) []string { } return fetchOptions.CurrentReplicatedChartNames } + +func pickEmbeddedClusterArtifacts(fetchOptions *types.FetchOptions) *kotsv1beta1.EmbeddedClusterArtifacts { + if fetchOptions.Airgap != nil { + if fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts == nil { + return nil + } + + opts := imageutil.EmbeddedClusterArtifactOCIPathOptions{ + RegistryHost: fetchOptions.LocalRegistry.Hostname, + RegistryNamespace: fetchOptions.LocalRegistry.Namespace, + ChannelID: fetchOptions.Airgap.Spec.ChannelID, + UpdateCursor: fetchOptions.Airgap.Spec.UpdateCursor, + VersionLabel: fetchOptions.Airgap.Spec.VersionLabel, + } + return &kotsv1beta1.EmbeddedClusterArtifacts{ + BinaryAmd64: imageutil.NewEmbeddedClusterOCIArtifactPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.BinaryAmd64, opts).String(), + Charts: imageutil.NewEmbeddedClusterOCIArtifactPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.Charts, opts).String(), + ImagesAmd64: imageutil.NewEmbeddedClusterOCIArtifactPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.ImagesAmd64, opts).String(), + Metadata: imageutil.NewEmbeddedClusterOCIArtifactPath(fetchOptions.Airgap.Spec.EmbeddedClusterArtifacts.Metadata, opts).String(), + } + } + return fetchOptions.CurrentEmbeddedClusterArtifacts +} diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 4be64998eb..f083699374 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -56,6 +56,7 @@ type Release struct { ReplicatedRegistryDomain string ReplicatedProxyDomain string ReplicatedChartNames []string + EmbeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts Manifests map[string][]byte } @@ -127,6 +128,7 @@ func downloadReplicated( replicatedRegistryDomain string, replicatedProxyDomain string, replicatedChartNames []string, + embeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts, appSlug string, appSequence int64, isAirgap bool, @@ -138,7 +140,7 @@ func downloadReplicated( var release *Release if localPath != "" { - parsedLocalRelease, err := readReplicatedAppFromLocalPath(localPath, updateCursor, versionLabel, isRequired, replicatedRegistryDomain, replicatedProxyDomain, replicatedChartNames) + parsedLocalRelease, err := readReplicatedAppFromLocalPath(localPath, updateCursor, versionLabel, isRequired, replicatedRegistryDomain, replicatedProxyDomain, replicatedChartNames, embeddedClusterArtifacts) if err != nil { return nil, errors.Wrap(err, "failed to read replicated app from local path") } @@ -299,12 +301,13 @@ func downloadReplicated( ReplicatedRegistryDomain: release.ReplicatedRegistryDomain, ReplicatedProxyDomain: release.ReplicatedProxyDomain, ReplicatedChartNames: release.ReplicatedChartNames, + EmbeddedClusterArtifacts: release.EmbeddedClusterArtifacts, } return upstream, nil } -func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp.ReplicatedCursor, versionLabel string, isRequired bool, replicatedRegistryDomain string, replicatedProxyDomain string, replicatedChartNames []string) (*Release, error) { +func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp.ReplicatedCursor, versionLabel string, isRequired bool, replicatedRegistryDomain string, replicatedProxyDomain string, replicatedChartNames []string, embeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts) (*Release, error) { release := Release{ Manifests: make(map[string][]byte), UpdateCursor: localCursor, @@ -313,6 +316,7 @@ func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp. ReplicatedRegistryDomain: replicatedRegistryDomain, ReplicatedProxyDomain: replicatedProxyDomain, ReplicatedChartNames: replicatedChartNames, + EmbeddedClusterArtifacts: embeddedClusterArtifacts, } err := filepath.Walk(localPath, diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go index 6f24527b6f..e7548cb19f 100644 --- a/pkg/upstream/types/types.go +++ b/pkg/upstream/types/types.go @@ -37,6 +37,7 @@ type Upstream struct { ReplicatedRegistryDomain string ReplicatedProxyDomain string ReplicatedChartNames []string + EmbeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts EncryptionKey string } @@ -104,6 +105,7 @@ type FetchOptions struct { CurrentReplicatedRegistryDomain string CurrentReplicatedProxyDomain string CurrentReplicatedChartNames []string + CurrentEmbeddedClusterArtifacts *kotsv1beta1.EmbeddedClusterArtifacts ChannelChanged bool AppSlug string AppSequence int64 diff --git a/pkg/upstream/upgrade.go b/pkg/upstream/upgrade.go index 2d8709221b..45c660930b 100644 --- a/pkg/upstream/upgrade.go +++ b/pkg/upstream/upgrade.go @@ -69,7 +69,7 @@ func Upgrade(appSlug string, options UpgradeOptions) (*UpgradeResponse, error) { } if !options.DisableImagePush { - _, err := image.TagAndPushImagesFromBundle(options.AirgapBundle, pushOptions) + err := image.TagAndPushImagesFromBundle(options.AirgapBundle, pushOptions) if err != nil { return nil, errors.Wrap(err, "failed to tag and push app images from path") } diff --git a/pkg/upstream/write.go b/pkg/upstream/write.go index 8481ad946d..f60ad3b9b5 100644 --- a/pkg/upstream/write.go +++ b/pkg/upstream/write.go @@ -141,6 +141,7 @@ func WriteUpstream(u *types.Upstream, options types.WriteOptions) error { ReplicatedRegistryDomain: u.ReplicatedRegistryDomain, ReplicatedProxyDomain: u.ReplicatedProxyDomain, ReplicatedChartNames: u.ReplicatedChartNames, + EmbeddedClusterArtifacts: u.EmbeddedClusterArtifacts, EncryptionKey: encryptionKey, }, }