diff --git a/cmd/kots/cli/admin-console-push-images.go b/cmd/kots/cli/admin-console-push-images.go index 90e8f31d9d..a4e2809d3f 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 a157bd933d..d28e03a7fe 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/replicatedhq/kots -go 1.21.0 - -toolchain go1.21.3 +go 1.21.7 require ( cloud.google.com/go/storage v1.35.1 @@ -49,8 +47,8 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/replicatedhq/embedded-cluster-operator v0.20.0 - github.com/replicatedhq/kotskinds v0.0.0-20240209205029-4b4312dee0e4 + github.com/replicatedhq/embedded-cluster-kinds v1.1.1 + github.com/replicatedhq/kotskinds v0.0.0-20240326213823-6a0ed11e7397 github.com/replicatedhq/kurlkinds v1.3.6 github.com/replicatedhq/troubleshoot v0.83.0 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq diff --git a/go.sum b/go.sum index 533464b0a6..3d1cf4bfd6 100644 --- a/go.sum +++ b/go.sum @@ -1311,10 +1311,12 @@ github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1: github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= -github.com/replicatedhq/embedded-cluster-operator v0.20.0 h1:s9tlPeeOn6faj76bmW5PuMndj/uvUhrS/07iaGqIC0Q= -github.com/replicatedhq/embedded-cluster-operator v0.20.0/go.mod h1:qkCOGMKxiCzSkqM+3IWACbAB/32G0V67HYreJ5iaa30= -github.com/replicatedhq/kotskinds v0.0.0-20240209205029-4b4312dee0e4 h1:gVPYIjTxxtPs58pc+9euQNmI1jA/xExfam7bAYNUH5c= -github.com/replicatedhq/kotskinds v0.0.0-20240209205029-4b4312dee0e4/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/embedded-cluster-kinds v1.1.1 h1:300bVpCBHtZ9acJgEHL3/S4CapvxCRfcSwg/adMNWW8= +github.com/replicatedhq/embedded-cluster-kinds v1.1.1/go.mod h1:LheSDOgMngMRAbwAj0sVZUVv2ciKIVR2bYTMeOBGwlg= +github.com/replicatedhq/kotskinds v0.0.0-20240326192117-c171d8810b55 h1:jMMARRH885QgPR8LBFGjRxYGBQgzADsyjZyQLZ/TKlc= +github.com/replicatedhq/kotskinds v0.0.0-20240326192117-c171d8810b55/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +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/kurlkinds v1.3.6 h1:/dhS32cSSZR4yS4vA8EquBvz+VgJCyTqBO9Xw+6eI4M= github.com/replicatedhq/kurlkinds v1.3.6/go.mod h1:c5+hoAkuARgftB2Ft3RCyWRZZPhL0clHEaw7XoGDAg4= github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 h1:eRlNDHxGfVkPCRXbA4BfQJvt5DHjFiTtWy3R/t4djyY= @@ -2302,8 +2304,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= helm.sh/helm/v3 v3.14.3 h1:HmvRJlwyyt9HjgmAuxHbHv3PhMz9ir/XNWHyXfmnOP4= helm.sh/helm/v3 v3.14.3/go.mod h1:v6myVbyseSBJTzhmeE39UcPLNv6cQK6qss3dvgAySaE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/embeddedcluster/monitor.go b/pkg/embeddedcluster/monitor.go index a0ee5ef401..74516f1236 100644 --- a/pkg/embeddedcluster/monitor.go +++ b/pkg/embeddedcluster/monitor.go @@ -7,8 +7,9 @@ import ( "sync" "time" - "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" appstatetypes "github.com/replicatedhq/kots/pkg/appstate/types" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" @@ -22,8 +23,8 @@ var stateMut = sync.Mutex{} // - The app has an embedded cluster configuration. // - The app embedded cluster configuration differs from the current embedded cluster config. // - The current cluster config (as part of the Installation object) already exists in the cluster. -func MaybeStartClusterUpgrade(ctx context.Context, store store.Store, conf *v1beta1.Config, appID string) error { - if conf == nil { +func MaybeStartClusterUpgrade(ctx context.Context, store store.Store, kotsKinds *kotsutil.KotsKinds, appID string) error { + if kotsKinds == nil || kotsKinds.EmbeddedClusterConfig == nil { return nil } @@ -31,7 +32,7 @@ func MaybeStartClusterUpgrade(ctx context.Context, store store.Store, conf *v1be return nil } - spec := conf.Spec + spec := kotsKinds.EmbeddedClusterConfig.Spec if upgrade, err := RequiresUpgrade(ctx, spec); err != nil { // if there is no installation object we can't start an upgrade. this is a valid // scenario specially during cluster bootstrap. as we do not need to upgrade the @@ -66,7 +67,9 @@ func MaybeStartClusterUpgrade(ctx context.Context, store store.Store, conf *v1be continue } - if err := startClusterUpgrade(ctx, spec); err != nil { + artifacts := getArtifactsFromInstallation(kotsKinds.Installation, kotsKinds.License.Spec.AppSlug) + + if err := startClusterUpgrade(ctx, spec, artifacts); err != nil { return fmt.Errorf("failed to start cluster upgrade: %w", err) } logger.Info("started cluster upgrade") @@ -107,7 +110,7 @@ func watchClusterState(ctx context.Context, store store.Store) { logger.Errorf("embeddedcluster monitor: fail updating state: %v", err) } - if state == v1beta1.InstallationStateInstalled { + if state == embeddedclusterv1beta1.InstallationStateInstalled { numReady++ } else { numReady = 0 @@ -124,7 +127,7 @@ func updateClusterState(ctx context.Context, store store.Store, lastState string if err != nil { return "", fmt.Errorf("failed to get current installation: %w", err) } - state := v1beta1.InstallationStateUnknown + state := embeddedclusterv1beta1.InstallationStateUnknown if installation.Status.State != "" { state = installation.Status.State } diff --git a/pkg/embeddedcluster/roles.go b/pkg/embeddedcluster/roles.go index 08a395f38f..6dc0d80e86 100644 --- a/pkg/embeddedcluster/roles.go +++ b/pkg/embeddedcluster/roles.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" ) const DEFAULT_CONTROLLER_ROLE_NAME = "controller" diff --git a/pkg/embeddedcluster/roles_test.go b/pkg/embeddedcluster/roles_test.go index 5c00c458b0..198087e2e5 100644 --- a/pkg/embeddedcluster/roles_test.go +++ b/pkg/embeddedcluster/roles_test.go @@ -3,7 +3,7 @@ package embeddedcluster import ( "testing" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" "github.com/stretchr/testify/require" ) diff --git a/pkg/embeddedcluster/util.go b/pkg/embeddedcluster/util.go index 0b4a8a2601..4b9b7daad1 100644 --- a/pkg/embeddedcluster/util.go +++ b/pkg/embeddedcluster/util.go @@ -5,11 +5,14 @@ import ( "context" "encoding/json" "fmt" + "regexp" "sort" "time" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + 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" "k8s.io/apimachinery/pkg/runtime" @@ -23,6 +26,13 @@ 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{}) @@ -102,8 +112,32 @@ func ClusterConfig(ctx context.Context) (*embeddedclusterv1beta1.ConfigSpec, err return latest.Spec.Config, nil } +func getArtifactsFromInstallation(installation kotsv1beta1.Installation, appSlug string) *embeddedclusterv1beta1.ArtifactsLocation { + if len(installation.Spec.EmbeddedClusterArtifacts) == 0 { + 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 artifacts +} + // startClusterUpgrade will create a new installation with the provided config. -func startClusterUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.ConfigSpec) error { +func startClusterUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.ConfigSpec, artifacts *embeddedclusterv1beta1.ArtifactsLocation) error { clientConfig, err := k8sutil.GetClusterConfig() if err != nil { return fmt.Errorf("failed to get cluster config: %w", err) @@ -126,6 +160,7 @@ func startClusterUpgrade(ctx context.Context, newcfg embeddedclusterv1beta1.Conf ClusterID: current.Spec.ClusterID, MetricsBaseURL: current.Spec.MetricsBaseURL, AirGap: current.Spec.AirGap, + Artifacts: artifacts, Config: &newcfg, EndUserK0sConfigOverrides: current.Spec.EndUserK0sConfigOverrides, }, diff --git a/pkg/embeddedcluster/util_test.go b/pkg/embeddedcluster/util_test.go new file mode 100644 index 0000000000..deb293d396 --- /dev/null +++ b/pkg/embeddedcluster/util_test.go @@ -0,0 +1,60 @@ +package embeddedcluster + +import ( + "reflect" + "testing" + + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" +) + +func Test_getArtifactsFromInstallation(t *testing.T) { + type args struct { + installation kotsv1beta1.Installation + appSlug string + } + tests := []struct { + name string + args args + want *embeddedclusterv1beta1.ArtifactsLocation + }{ + { + name: "no artifacts", + args: args{ + installation: kotsv1beta1.Installation{}, + appSlug: "my-app", + }, + want: nil, + }, + { + name: "has all artifacts", + 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", + }, + }, + }, + appSlug: "my-app", + }, + want: &embeddedclusterv1beta1.ArtifactsLocation{ + Images: "onprem.registry.com/my-app/embedded-cluster/images-amd64.tar:v1", + HelmCharts: "onprem.registry.com/my-app/embedded-cluster/charts.tar.gz:v1", + EmbeddedClusterBinary: "onprem.registry.com/my-app/embedded-cluster/embedded-cluster-amd64:v1", + EmbeddedClusterMetadata: "onprem.registry.com/my-app/embedded-cluster/version-metadata.json:v1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getArtifactsFromInstallation(tt.args.installation, tt.args.appSlug) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getArtifactsFromInstallation() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/handlers/app.go b/pkg/handlers/app.go index a8d1a430d5..c2c6826a1d 100644 --- a/pkg/handlers/app.go +++ b/pkg/handlers/app.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" "github.com/replicatedhq/kots/pkg/airgap" downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" "github.com/replicatedhq/kots/pkg/api/handlers/types" diff --git a/pkg/image/airgap.go b/pkg/image/airgap.go index 1bc31aedbc..e9c3c181a7 100644 --- a/pkg/image/airgap.go +++ b/pkg/image/airgap.go @@ -108,7 +108,11 @@ 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) error { +func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger) (*imagetypes.CopyAirgapImagesResult, error) { + if opts.AirgapBundle == "" { + return &imagetypes.CopyAirgapImagesResult{}, nil + } + pushOpts := imagetypes.PushImagesOptions{ Registry: dockerregistrytypes.RegistryOptions{ Endpoint: opts.RegistrySettings.Hostname, @@ -121,27 +125,27 @@ func CopyAirgapImages(opts imagetypes.ProcessImageOptions, log *logger.CLILogger LogForUI: true, } - if opts.AirgapBundle != "" { - err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts) - if err != nil { - return errors.Wrap(err, "failed to push images from bundle") - } + copyResult, err := TagAndPushImagesFromBundle(opts.AirgapBundle, pushOpts) + if err != nil { + return nil, errors.Wrap(err, "failed to push images from bundle") } - return nil + return &imagetypes.CopyAirgapImagesResult{ + EmbeddedClusterArtifacts: copyResult.EmbeddedClusterArtifacts, + }, nil } -func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) error { +func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImagesOptions) (*imagetypes.CopyAirgapImagesResult, error) { airgap, err := kotsutil.FindAirgapMetaInBundle(airgapBundle) if err != nil { - return errors.Wrap(err, "failed to find airgap meta") + return nil, 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 errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle") + return nil, errors.Wrap(err, "failed to create temp dir for unarchived airgap bundle") } defer os.RemoveAll(extractedBundle) @@ -151,17 +155,17 @@ func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImag }, } if err := tarGz.Unarchive(airgapBundle, extractedBundle); err != nil { - return errors.Wrap(err, "falied to unarchive airgap bundle") + return nil, errors.Wrap(err, "falied to unarchive airgap bundle") } if err := PushImagesFromTempRegistry(extractedBundle, airgap.Spec.SavedImages, options); err != nil { - return errors.Wrap(err, "failed to push images from docker registry bundle") + return nil, errors.Wrap(err, "failed to push images from docker registry bundle") } case dockertypes.FormatDockerArchive, "": if err := PushImagesFromDockerArchiveBundle(airgapBundle, options); err != nil { - return errors.Wrap(err, "failed to push images from docker archive bundle") + return nil, errors.Wrap(err, "failed to push images from docker archive bundle") } default: - return errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format) + return nil, errors.Errorf("Airgap bundle format '%s' is not supported", airgap.Spec.Format) } pushEmbeddedArtifactsOpts := imagetypes.PushEmbeddedClusterArtifactsOptions{ @@ -169,11 +173,16 @@ func TagAndPushImagesFromBundle(airgapBundle string, options imagetypes.PushImag Tag: imageutil.SanitizeTag(fmt.Sprintf("%s-%s-%s", airgap.Spec.ChannelID, airgap.Spec.UpdateCursor, airgap.Spec.VersionLabel)), HTTPClient: orasretry.DefaultClient, } - if err := PushEmbeddedClusterArtifacts(airgapBundle, pushEmbeddedArtifactsOpts); err != nil { - return errors.Wrap(err, "failed to push embedded cluster artifacts") + pushedArtifacts, err := PushEmbeddedClusterArtifacts(airgapBundle, pushEmbeddedArtifactsOpts) + if err != nil { + return nil, errors.Wrap(err, "failed to push embedded cluster artifacts") } - return nil + result := &imagetypes.CopyAirgapImagesResult{ + EmbeddedClusterArtifacts: pushedArtifacts, + } + + return result, nil } func PushImagesFromTempRegistry(airgapRootDir string, imageList []string, options imagetypes.PushImagesOptions) error { @@ -681,33 +690,34 @@ func reportWriterWithProgress(imageInfos map[string]*imagetypes.ImageInfo, repor return pipeWriter } -func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbeddedClusterArtifactsOptions) error { +func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbeddedClusterArtifactsOptions) ([]string, error) { tmpDir, err := os.MkdirTemp("", "embedded-cluster-artifacts") if err != nil { - return errors.Wrap(err, "failed to create temp directory") + return nil, errors.Wrap(err, "failed to create temp directory") } defer os.RemoveAll(tmpDir) fileReader, err := os.Open(airgapBundle) if err != nil { - return errors.Wrap(err, "failed to open file") + return nil, errors.Wrap(err, "failed to open file") } defer fileReader.Close() gzipReader, err := gzip.NewReader(fileReader) if err != nil { - return errors.Wrap(err, "failed to get new gzip reader") + return nil, errors.Wrap(err, "failed to get new gzip reader") } defer gzipReader.Close() tarReader := tar.NewReader(gzipReader) + pushedArtifacts := make([]string, 0) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { - return errors.Wrap(err, "failed to get read archive") + return nil, errors.Wrap(err, "failed to get read archive") } if header.Typeflag != tar.TypeReg { @@ -753,18 +763,20 @@ func PushEmbeddedClusterArtifacts(airgapBundle string, opts imagetypes.PushEmbed HTTPClient: opts.HTTPClient, } - fmt.Printf("Pushing artifact %s:%s\n", filepath.Join(opts.Registry.Endpoint, opts.Registry.Namespace, repository), opts.Tag) + artifact := fmt.Sprintf("%s:%s", filepath.Join(opts.Registry.Endpoint, opts.Registry.Namespace, repository), opts.Tag) + fmt.Printf("Pushing artifact %s\n", artifact) if err := pushOCIArtifact(pushOCIArtifactOpts); err != nil { return errors.Wrapf(err, "failed to push oci artifact %s", name) } + pushedArtifacts = append(pushedArtifacts, artifact) return nil }(); err != nil { - return err + return nil, err } } - return nil + return pushedArtifacts, nil } func pushOCIArtifact(opts imagetypes.PushOCIArtifactOptions) error { diff --git a/pkg/image/airgap_test.go b/pkg/image/airgap_test.go index 5e01bddce1..c592ac4db4 100644 --- a/pkg/image/airgap_test.go +++ b/pkg/image/airgap_test.go @@ -22,10 +22,10 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { testTag := "test-tag" tests := []struct { - name string - airgapFiles map[string][]byte - wantArtifacts map[string]string - wantErr bool + name string + airgapFiles map[string][]byte + wantRegistryArtifacts map[string]string + wantErr bool }{ { name: "no embedded cluster files", @@ -34,25 +34,27 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { "app.tar.gz": []byte("this-is-the-app-archive"), "images/something": []byte("this-is-an-image"), }, - wantArtifacts: map[string]string{}, - wantErr: false, + wantRegistryArtifacts: map[string]string{}, + wantErr: false, }, { name: "has embedded cluster files", airgapFiles: map[string][]byte{ - "airgap.yaml": []byte("this-is-the-airgap-metadata"), - "app.tar.gz": []byte("this-is-the-app-archive"), - "images/something": []byte("this-is-an-image"), - "embedded-cluster/test-app": []byte("this-is-the-binary"), - "embedded-cluster/charts.tar.gz": []byte("this-is-the-charts-bundle"), - "embedded-cluster/images-amd64.tar": []byte("this-is-the-images-bundle"), - "embedded-cluster/some-file-TBD": []byte("this-is-an-arbitrary-file"), + "airgap.yaml": []byte("this-is-the-airgap-metadata"), + "app.tar.gz": []byte("this-is-the-app-archive"), + "images/something": []byte("this-is-an-image"), + "embedded-cluster/test-app": []byte("this-is-the-binary"), + "embedded-cluster/charts.tar.gz": []byte("this-is-the-charts-bundle"), + "embedded-cluster/images-amd64.tar": []byte("this-is-the-images-bundle"), + "embedded-cluster/version-metadata.json": []byte("this-is-the-metadata"), + "embedded-cluster/some-file-TBD": []byte("this-is-an-arbitrary-file"), }, - wantArtifacts: 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/some-file-tbd", testAppSlug): testTag, + 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, }, wantErr: false, }, @@ -65,8 +67,8 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { t.Fatalf("Failed to create airgap bundle: %v", err) } - pushedArtifacts := make(map[string]string) - mockRegistryServer := newMockRegistryServer(pushedArtifacts) + pushedRegistryArtifacts := make(map[string]string) + mockRegistryServer := newMockRegistryServer(pushedRegistryArtifacts) defer mockRegistryServer.Close() u, err := url.Parse(mockRegistryServer.URL) @@ -82,7 +84,8 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { Tag: testTag, HTTPClient: mockRegistryServer.Client(), } - if err := PushEmbeddedClusterArtifacts(airgapBundle, opts); (err != nil) != tt.wantErr { + gotArtifacts, err := PushEmbeddedClusterArtifacts(airgapBundle, opts) + if (err != nil) != tt.wantErr { t.Errorf("PushEmbeddedClusterArtifacts() error = %v, wantErr %v", err, tt.wantErr) } @@ -92,8 +95,14 @@ func TestPushEmbeddedClusterArtifacts(t *testing.T) { 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.wantArtifacts, pushedArtifacts) + req.Equal(tt.wantRegistryArtifacts, pushedRegistryArtifacts) }) } } @@ -126,7 +135,7 @@ func createTestAirgapBundle(airgapFiles map[string][]byte, dstPath string) error return nil } -func newMockRegistryServer(pushedArtifacts map[string]string) *httptest.Server { +func newMockRegistryServer(pushedRegistryArtifacts map[string]string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { blobsRegex := regexp.MustCompile(`/v2/(.+)/blobs/(.*)`) manifestsRegex := regexp.MustCompile(`/v2/(.+)/manifests/(.*)`) @@ -138,7 +147,7 @@ func newMockRegistryServer(pushedArtifacts map[string]string) *httptest.Server { case r.Method == http.MethodPut && manifestsRegex.MatchString(r.URL.Path): repo := manifestsRegex.FindStringSubmatch(r.URL.Path)[1] tag := manifestsRegex.FindStringSubmatch(r.URL.Path)[2] - pushedArtifacts[repo] = tag + pushedRegistryArtifacts[repo] = tag w.WriteHeader(http.StatusCreated) default: w.WriteHeader(http.StatusNotFound) diff --git a/pkg/image/online.go b/pkg/image/online.go index e5346bade5..a4af9c6d4c 100644 --- a/pkg/image/online.go +++ b/pkg/image/online.go @@ -45,6 +45,12 @@ type UpdateInstallationImagesOptions struct { DockerHubRegistryCreds registry.Credentials } +type UpdateInstallationEmbeddedClusterArtifactsOptions struct { + Artifacts []string + KotsKinds *kotsutil.KotsKinds + UpstreamDir string +} + func UpdateInstallationImages(opts UpdateInstallationImagesOptions) error { if opts.KotsKinds == nil { return nil @@ -132,6 +138,20 @@ 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 67192b7cec..64e5b59638 100644 --- a/pkg/image/types/types.go +++ b/pkg/image/types/types.go @@ -46,6 +46,10 @@ type CopyImageOptions struct { ReportWriter io.Writer } +type CopyAirgapImagesResult struct { + EmbeddedClusterArtifacts []string +} + type PushImagesOptions struct { Registry dockerregistrytypes.RegistryOptions KotsadmTag string diff --git a/pkg/kotsadm/main.go b/pkg/kotsadm/main.go index cf87267135..337c8374e9 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/kotsutil/kots.go b/pkg/kotsutil/kots.go index a559eebdbd..0affdd355f 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -17,7 +17,7 @@ import ( "github.com/blang/semver" dockerref "github.com/containers/image/v5/docker/reference" "github.com/pkg/errors" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" "github.com/replicatedhq/kots/pkg/archives" "github.com/replicatedhq/kots/pkg/buildversion" "github.com/replicatedhq/kots/pkg/crypto" diff --git a/pkg/midstream/write.go b/pkg/midstream/write.go index 173539e53f..795d702d1e 100644 --- a/pkg/midstream/write.go +++ b/pkg/midstream/write.go @@ -112,10 +112,18 @@ func WriteMidstream(opts WriteOptions) (*Midstream, error) { io.WriteString(opts.ProcessImageOptions.ReportWriter, "Copying images\n") if opts.ProcessImageOptions.IsAirgap { - err := image.CopyAirgapImages(opts.ProcessImageOptions, opts.Log) + copyResult, 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/operator/operator.go b/pkg/operator/operator.go index 003b9ef12c..f058ac611f 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -412,7 +412,7 @@ func (o *Operator) DeployApp(appID string, sequence int64) (deployed bool, deplo if deployed { go func() { - err = embeddedcluster.MaybeStartClusterUpgrade(context.TODO(), o.store, kotsKinds.EmbeddedClusterConfig, app.ID) + err = embeddedcluster.MaybeStartClusterUpgrade(context.TODO(), o.store, kotsKinds, app.ID) if err != nil { logger.Error(errors.Wrap(err, "failed to start cluster upgrade")) } diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 7df9ab7498..7ccb73fd20 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -364,7 +364,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/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 9b44d13583..081f38dac5 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -14,7 +14,7 @@ import ( "github.com/blang/semver" "github.com/mholt/archiver/v3" "github.com/pkg/errors" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" versiontypes "github.com/replicatedhq/kots/pkg/api/version/types" apptypes "github.com/replicatedhq/kots/pkg/app/types" diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 92fdcf3057..9deaf4936c 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -10,7 +10,7 @@ import ( time "time" gomock "github.com/golang/mock/gomock" - v1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + v1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" types "github.com/replicatedhq/kots/pkg/airgap/types" types0 "github.com/replicatedhq/kots/pkg/api/downstream/types" types1 "github.com/replicatedhq/kots/pkg/api/version/types" diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 2b39321105..05f1ee395e 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -4,7 +4,7 @@ import ( "context" "time" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1" + embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-kinds/apis/v1beta1" airgaptypes "github.com/replicatedhq/kots/pkg/airgap/types" downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" versiontypes "github.com/replicatedhq/kots/pkg/api/version/types" diff --git a/pkg/upstream/upgrade.go b/pkg/upstream/upgrade.go index 45c660930b..2d8709221b 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") }