From 984a1bd6c8204bdd3d032a56043523d99dae812f Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 17 Jul 2024 16:56:54 +0000 Subject: [PATCH 01/35] migration to app.channel_id column --- go.mod | 2 +- go.sum | 2 ++ migrations/build-ttl.sh | 3 ++- migrations/tables/app.yaml | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0d46fa6b16..f949a70e56 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,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.4.5 - github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2 + github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41 github.com/replicatedhq/kurlkinds v1.5.0 github.com/replicatedhq/troubleshoot v0.95.1 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq diff --git a/go.sum b/go.sum index 5c227fe18b..8e711a4072 100644 --- a/go.sum +++ b/go.sum @@ -1317,6 +1317,8 @@ github.com/replicatedhq/embedded-cluster-kinds v1.4.5 h1:OZygNoSL4yFF+r+0l2slnq/ github.com/replicatedhq/embedded-cluster-kinds v1.4.5/go.mod h1:AwopUvvGcaWO4mn9DkbPj5RnLuOy756CNLrcaAlmjMo= github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2 h1:xL4u2RHhMaGDgz7Lol5MhVYLnWahV3sCJZbfebpPao0= github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41 h1:xvvRq5EZ7wBlsrDZIAUpJ318cEMzTpt8zVAO2atFQlM= +github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41/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/migrations/build-ttl.sh b/migrations/build-ttl.sh index 8f9daea0f5..bd80613f27 100755 --- a/migrations/build-ttl.sh +++ b/migrations/build-ttl.sh @@ -3,8 +3,9 @@ set -e CURRENT_USER=${GITHUB_USER:-$(id -u -n)} IMAGE=ttl.sh/${CURRENT_USER}/kotsadm-migrations:24h +SCHEMAHERO_TAG=${SCHEMAHERO_TAG:-0.17.9} -docker build -f deploy/Dockerfile -t ${IMAGE} . +docker build --build-arg SCHEMAHERO_TAG=${SCHEMAHERO_TAG} -f deploy/Dockerfile -t ${IMAGE} . docker push ${IMAGE} GREEN='\033[0;32m' diff --git a/migrations/tables/app.yaml b/migrations/tables/app.yaml index af9aec57eb..ccdc0b51ba 100644 --- a/migrations/tables/app.yaml +++ b/migrations/tables/app.yaml @@ -85,3 +85,5 @@ spec: default: 0 constraints: notNull: true + - name: channel_id + type: text From 8b2c5e394000afd00cb2318439d378f96e91bbfb Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 17 Jul 2024 20:28:37 +0000 Subject: [PATCH 02/35] Store (and return) channel_id used at install time fix vet --- cmd/kots/cli/install.go | 23 +++++++++++ cmd/kots/cli/util.go | 18 +++++++++ cmd/kots/cli/util_test.go | 49 +++++++++++++++++++++++ migrations/build-ttl.sh | 3 +- pkg/app/types/app.go | 5 +++ pkg/automation/automation.go | 2 +- pkg/handlers/license.go | 3 +- pkg/kotsadm/objects/configmaps_objects.go | 2 + pkg/kotsadm/types/deployoptions.go | 1 + pkg/kotsutil/kots.go | 2 + pkg/store/kotsstore/app_store.go | 31 +++++++++++--- pkg/store/mock/mock.go | 44 ++++++++++++++++---- pkg/store/store_interface.go | 3 +- 13 files changed, 167 insertions(+), 19 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index fb9d367fcf..2fe056e1cf 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -162,6 +162,28 @@ func InstallCmd() *cobra.Command { }() upstream := pull.RewriteUpstream(args[0]) + preferredChannelSlug, err := extractPreferredChannelSlug(upstream) + if err != nil { + return errors.Wrap(err, "failed to extract preferred channel slug") + } + + // use the preferred channel slug to find the matching channel id in the license + requestedChannelID := "" + if license != nil { + if license.Spec.Channels == nil { // this is a license format without multiple channels + requestedChannelID = license.Spec.ChannelID + } else if len(license.Spec.Channels) > 0 { + for _, channel := range license.Spec.Channels { + if channel.Slug == preferredChannelSlug { + requestedChannelID = channel.ID + break + } + } + return errors.New("requested channel not found in license") + } else { + return errors.New("no channel id found in license") + } + } namespace := v.GetString("namespace") @@ -278,6 +300,7 @@ func InstallCmd() *cobra.Command { IncludeMinio: v.GetBool("with-minio"), IncludeMinioSnapshots: v.GetBool("with-minio"), StrictSecurityContext: v.GetBool("strict-security-context"), + RequestedChannelID: requestedChannelID, RegistryConfig: *registryConfig, diff --git a/cmd/kots/cli/util.go b/cmd/kots/cli/util.go index 4203831fcf..4d4ea33f56 100644 --- a/cmd/kots/cli/util.go +++ b/cmd/kots/cli/util.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/replicatedapp" "github.com/replicatedhq/kots/pkg/util" ) @@ -51,3 +52,20 @@ func splitEndpointAndNamespace(endpoint string) (string, string) { } return registryEndpoint, registryNamespace } + +func extractPreferredChannelSlug(upstreamURI string) (string, error) { + u, err := url.ParseRequestURI(upstreamURI) + if err != nil { + return "", errors.Wrap(err, "failed to parse uri") + } + + replicatedUpstream, err := replicatedapp.ParseReplicatedURL(u) + if err != nil { + return "", errors.Wrap(err, "failed to parse replicated url") + } + + if replicatedUpstream.Channel != nil { + return *replicatedUpstream.Channel, nil + } + return "stable", nil +} diff --git a/cmd/kots/cli/util_test.go b/cmd/kots/cli/util_test.go index 821490a80a..a270dbaecf 100644 --- a/cmd/kots/cli/util_test.go +++ b/cmd/kots/cli/util_test.go @@ -95,3 +95,52 @@ func Test_getHostFromEndpoint(t *testing.T) { }) } } + +func Test_extractPreferredChannelSlug(t *testing.T) { + type args struct { + upstreamURI string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + "no channel", + args{ + upstreamURI: "replicated://app-slug", + }, + "stable", // default channel + false, + }, + { + "with channel", + args{ + upstreamURI: "replicated://app-slug/channel", + }, + "channel", + false, + }, + { + "invalid uri", + args{ + upstreamURI: "junk", + }, + "", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := extractPreferredChannelSlug(tt.args.upstreamURI) + if (err != nil) != tt.wantErr { + t.Errorf("extractPreferredChannelSlug() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("extractPreferredChannelSlug() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/migrations/build-ttl.sh b/migrations/build-ttl.sh index bd80613f27..8f9daea0f5 100755 --- a/migrations/build-ttl.sh +++ b/migrations/build-ttl.sh @@ -3,9 +3,8 @@ set -e CURRENT_USER=${GITHUB_USER:-$(id -u -n)} IMAGE=ttl.sh/${CURRENT_USER}/kotsadm-migrations:24h -SCHEMAHERO_TAG=${SCHEMAHERO_TAG:-0.17.9} -docker build --build-arg SCHEMAHERO_TAG=${SCHEMAHERO_TAG} -f deploy/Dockerfile -t ${IMAGE} . +docker build -f deploy/Dockerfile -t ${IMAGE} . docker push ${IMAGE} GREEN='\033[0;32m' diff --git a/pkg/app/types/app.go b/pkg/app/types/app.go index f2682ae6cd..8930d90655 100644 --- a/pkg/app/types/app.go +++ b/pkg/app/types/app.go @@ -30,6 +30,7 @@ type App struct { InstallState string `json:"installState"` LastLicenseSync string `json:"lastLicenseSync"` ChannelChanged bool `json:"channelChanged"` + ChannelID string `json:"channel_id"` } func (a *App) GetID() string { @@ -44,6 +45,10 @@ func (a *App) GetCurrentSequence() int64 { return a.CurrentSequence } +func (a *App) GetChannelID() string { + return a.ChannelID +} + func (a *App) GetIsAirgap() bool { return a.IsAirgap } diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 3ce8d237d5..9baf382240 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -244,7 +244,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. desiredAppName := strings.Replace(appSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", appSlug) - a, err := store.GetStore().CreateApp(desiredAppName, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly) + a, err := store.GetStore().CreateApp(desiredAppName, instParams.RequestedChannelID, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly) if err != nil { return errors.Wrap(err, "failed to create app record") } diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index 017369f643..5e9fa69c11 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -334,7 +334,8 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) - a, err := store.GetStore().CreateApp(desiredAppName, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) + // TODO: FLORIAN - Validate requested channel ID here and back fill as needed! + a, err := store.GetStore().CreateApp(desiredAppName, installationParams.RequestedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) if err != nil { logger.Error(err) uploadLicenseResponse.Error = err.Error() diff --git a/pkg/kotsadm/objects/configmaps_objects.go b/pkg/kotsadm/objects/configmaps_objects.go index 70f28ccabd..b53b377104 100644 --- a/pkg/kotsadm/objects/configmaps_objects.go +++ b/pkg/kotsadm/objects/configmaps_objects.go @@ -25,7 +25,9 @@ func KotsadmConfigMap(deployOptions types.DeployOptions) *corev1.ConfigMap { "wait-duration": fmt.Sprintf("%v", deployOptions.Timeout), "with-minio": fmt.Sprintf("%v", deployOptions.IncludeMinio), "app-version-label": deployOptions.AppVersionLabel, + "requested-channel-id": deployOptions.RequestedChannelID, } + if kotsadmversion.KotsadmPullSecret(deployOptions.Namespace, deployOptions.RegistryConfig) != nil { data["kotsadm-registry"] = kotsadmversion.KotsadmRegistry(deployOptions.RegistryConfig) } diff --git a/pkg/kotsadm/types/deployoptions.go b/pkg/kotsadm/types/deployoptions.go index 5db3215c06..8b5d488017 100644 --- a/pkg/kotsadm/types/deployoptions.go +++ b/pkg/kotsadm/types/deployoptions.go @@ -56,6 +56,7 @@ type DeployOptions struct { IsMinimalRBAC bool AdditionalNamespaces []string IsGKEAutopilot bool + RequestedChannelID string IdentityConfig kotsv1beta1.IdentityConfig IngressConfig kotsv1beta1.IngressConfig diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 242bc0e54f..144a72649b 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1139,6 +1139,7 @@ type InstallationParams struct { WaitDuration time.Duration WithMinio bool AppVersionLabel string + RequestedChannelID string } func GetInstallationParams(configMapName string) (InstallationParams, error) { @@ -1174,6 +1175,7 @@ func GetInstallationParams(configMapName string) (InstallationParams, error) { autoConfig.WaitDuration, _ = time.ParseDuration(kotsadmConfigMap.Data["wait-duration"]) autoConfig.WithMinio, _ = strconv.ParseBool(kotsadmConfigMap.Data["with-minio"]) autoConfig.AppVersionLabel = kotsadmConfigMap.Data["app-version-label"] + autoConfig.RequestedChannelID = kotsadmConfigMap.Data["requested-channel-id"] if enableImageDeletion, ok := kotsadmConfigMap.Data["enable-image-deletion"]; ok { autoConfig.EnableImageDeletion, _ = strconv.ParseBool(enableImageDeletion) diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index 847f8b7df9..e944c9d827 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -146,7 +146,7 @@ func (s *KOTSStore) GetAppIDFromSlug(slug string) (string, error) { func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { db := persistence.MustGetDBSession() - query := `select id, name, license, upstream_uri, icon_uri, created_at, updated_at, slug, current_sequence, last_update_check_at, last_license_sync, is_airgap, snapshot_ttl_new, snapshot_schedule, restore_in_progress_name, restore_undeploy_status, update_checker_spec, semver_auto_deploy, install_state, channel_changed from app where id = ?` + query := `select id, name, license, upstream_uri, icon_uri, created_at, updated_at, slug, current_sequence, last_update_check_at, last_license_sync, is_airgap, snapshot_ttl_new, snapshot_schedule, restore_in_progress_name, restore_undeploy_status, update_checker_spec, semver_auto_deploy, install_state, channel_changed, channel_id from app where id = ?` rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{id}, @@ -173,8 +173,9 @@ func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { var restoreUndeployStatus gorqlite.NullString var updateCheckerSpec gorqlite.NullString var autoDeploy gorqlite.NullString + var channelID gorqlite.NullString - if err := rows.Scan(&app.ID, &app.Name, &licenseStr, &upstreamURI, &iconURI, &app.CreatedAt, &updatedAt, &app.Slug, ¤tSequence, &lastUpdateCheckAt, &lastLicenseSync, &app.IsAirgap, &snapshotTTLNew, &snapshotSchedule, &restoreInProgressName, &restoreUndeployStatus, &updateCheckerSpec, &autoDeploy, &app.InstallState, &app.ChannelChanged); err != nil { + if err := rows.Scan(&app.ID, &app.Name, &licenseStr, &upstreamURI, &iconURI, &app.CreatedAt, &updatedAt, &app.Slug, ¤tSequence, &lastUpdateCheckAt, &lastLicenseSync, &app.IsAirgap, &snapshotTTLNew, &snapshotSchedule, &restoreInProgressName, &restoreUndeployStatus, &updateCheckerSpec, &autoDeploy, &app.InstallState, &app.ChannelChanged, &channelID); err != nil { return nil, errors.Wrap(err, "failed to scan app") } @@ -187,6 +188,7 @@ func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { app.RestoreUndeployStatus = apptypes.UndeployStatus(restoreUndeployStatus.String) app.UpdateCheckerSpec = updateCheckerSpec.String app.AutoDeploy = apptypes.AutoDeploy(autoDeploy.String) + app.ChannelID = channelID.String if lastLicenseSync.Valid { app.LastLicenseSync = lastLicenseSync.Time.Format(time.RFC3339) @@ -279,10 +281,12 @@ func (s *KOTSStore) GetAppFromSlug(slug string) (*apptypes.App, error) { return s.GetApp(id) } -func (s *KOTSStore) CreateApp(name string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) { +func (s *KOTSStore) CreateApp(name string, channelID string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) { logger.Debug("creating app", zap.String("name", name), - zap.String("upstreamURI", upstreamURI)) + zap.String("upstreamURI", upstreamURI), + zap.String("channelID", channelID), + ) db := persistence.MustGetDBSession() @@ -337,10 +341,10 @@ func (s *KOTSStore) CreateApp(name string, upstreamURI string, licenseData strin id := ksuid.New().String() - query := `insert into app (id, name, icon_uri, created_at, slug, upstream_uri, license, is_all_users, install_state, registry_is_readonly) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + query := `insert into app (id, name, icon_uri, created_at, slug, upstream_uri, license, is_all_users, install_state, registry_is_readonly, channel_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ Query: query, - Arguments: []interface{}{id, name, "", time.Now().Unix(), slugProposal, upstreamURI, licenseData, true, installState, registryIsReadOnly}, + Arguments: []interface{}{id, name, "", time.Now().Unix(), slugProposal, upstreamURI, licenseData, true, installState, registryIsReadOnly, channelID}, }) if err != nil { return nil, fmt.Errorf("failed to insert app: %v: %v", err, wr.Err) @@ -594,3 +598,18 @@ func (s *KOTSStore) SetAppChannelChanged(appID string, channelChanged bool) erro return nil } + +func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { + db := persistence.MustGetDBSession() + + query := `update app set channel_id = ? where id = ?` + wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{channelID, appID}, + }) + if err != nil { + return fmt.Errorf("failed to update app channel id: %v: %v", err, wr.Err) + } + + return nil +} diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 0f65b2fea2..afac7c9ebe 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -97,18 +97,18 @@ func (mr *MockStoreMockRecorder) AddDownstreamVersionsDetails(appID, clusterID, } // CreateApp mocks base method. -func (m *MockStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { +func (m *MockStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateApp", name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) + ret := m.ctrl.Call(m, "CreateApp", name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateApp indicates an expected call of CreateApp. -func (mr *MockStoreMockRecorder) CreateApp(name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateApp(name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockStore)(nil).CreateApp), name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockStore)(nil).CreateApp), name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) } // CreateAppVersion mocks base method. @@ -1518,6 +1518,20 @@ func (mr *MockStoreMockRecorder) SetAppChannelChanged(appID, channelChanged inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelChanged", reflect.TypeOf((*MockStore)(nil).SetAppChannelChanged), appID, channelChanged) } +// SetAppChannelID mocks base method. +func (m *MockStore) SetAppChannelID(appID, channelID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetAppChannelID", appID, channelID) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetAppChannelID indicates an expected call of SetAppChannelID. +func (mr *MockStoreMockRecorder) SetAppChannelID(appID, channelID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelID", reflect.TypeOf((*MockStore)(nil).SetAppChannelID), appID, channelID) +} + // SetAppInstallState mocks base method. func (m *MockStore) SetAppInstallState(appID, state string) error { m.ctrl.T.Helper() @@ -2680,18 +2694,18 @@ func (mr *MockAppStoreMockRecorder) AddAppToAllDownstreams(appID interface{}) *g } // CreateApp mocks base method. -func (m *MockAppStore) CreateApp(name, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { +func (m *MockAppStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateApp", name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) + ret := m.ctrl.Call(m, "CreateApp", name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) ret0, _ := ret[0].(*types3.App) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateApp indicates an expected call of CreateApp. -func (mr *MockAppStoreMockRecorder) CreateApp(name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly interface{}) *gomock.Call { +func (mr *MockAppStoreMockRecorder) CreateApp(name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockAppStore)(nil).CreateApp), name, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockAppStore)(nil).CreateApp), name, channelID, upstreamURI, licenseData, isAirgapEnabled, skipImagePush, registryIsReadOnly) } // GetApp mocks base method. @@ -2872,6 +2886,20 @@ func (mr *MockAppStoreMockRecorder) SetAppChannelChanged(appID, channelChanged i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelChanged", reflect.TypeOf((*MockAppStore)(nil).SetAppChannelChanged), appID, channelChanged) } +// SetAppChannelID mocks base method. +func (m *MockAppStore) SetAppChannelID(appID, channelID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetAppChannelID", appID, channelID) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetAppChannelID indicates an expected call of SetAppChannelID. +func (mr *MockAppStoreMockRecorder) SetAppChannelID(appID, channelID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelID", reflect.TypeOf((*MockAppStore)(nil).SetAppChannelID), appID, channelID) +} + // SetAppInstallState mocks base method. func (m *MockAppStore) SetAppInstallState(appID, state string) error { m.ctrl.T.Helper() diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 049fc2de18..11396dd759 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -120,7 +120,7 @@ type AppStore interface { GetAppIDFromSlug(slug string) (appID string, err error) GetApp(appID string) (*apptypes.App, error) GetAppFromSlug(slug string) (*apptypes.App, error) - CreateApp(name string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) + CreateApp(name string, channelID string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) ListDownstreamsForApp(appID string) ([]downstreamtypes.Downstream, error) ListAppsForDownstream(clusterID string) ([]*apptypes.App, error) GetDownstream(clusterID string) (*downstreamtypes.Downstream, error) @@ -131,6 +131,7 @@ type AppStore interface { SetSnapshotSchedule(appID string, snapshotSchedule string) error RemoveApp(appID string) error SetAppChannelChanged(appID string, channelChanged bool) error + SetAppChannelID(appID string, channelID string) error } type DownstreamStore interface { From d1ebe365c0c6b2ecab33fe8daceb03809e15cd42 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 18 Jul 2024 20:02:34 +0000 Subject: [PATCH 03/35] Bump kotskinds package --- cmd/kots/cli/install.go | 8 +++++--- go.mod | 2 +- go.sum | 2 ++ pkg/handlers/license.go | 1 - 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 2fe056e1cf..01b4b71620 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -174,12 +174,14 @@ func InstallCmd() *cobra.Command { requestedChannelID = license.Spec.ChannelID } else if len(license.Spec.Channels) > 0 { for _, channel := range license.Spec.Channels { - if channel.Slug == preferredChannelSlug { - requestedChannelID = channel.ID + if channel.ChannelSlug == preferredChannelSlug { + requestedChannelID = channel.ChannelID break } } - return errors.New("requested channel not found in license") + if requestedChannelID == "" { + return errors.New("requested channel not found in license") + } } else { return errors.New("no channel id found in license") } diff --git a/go.mod b/go.mod index f949a70e56..323b0ea35a 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,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.4.5 - github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41 + github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95 github.com/replicatedhq/kurlkinds v1.5.0 github.com/replicatedhq/troubleshoot v0.95.1 github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq diff --git a/go.sum b/go.sum index 8e711a4072..14ce6f869b 100644 --- a/go.sum +++ b/go.sum @@ -1319,6 +1319,8 @@ github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2 h1:xL4u2RHh github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41 h1:xvvRq5EZ7wBlsrDZIAUpJ318cEMzTpt8zVAO2atFQlM= github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95 h1:JhwPz4Bgbz5iYl3UV2EB+HnF9oW/eCRi+hASAz+J6XI= +github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95/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/handlers/license.go b/pkg/handlers/license.go index 5e9fa69c11..44947919ab 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -334,7 +334,6 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) - // TODO: FLORIAN - Validate requested channel ID here and back fill as needed! a, err := store.GetStore().CreateApp(desiredAppName, installationParams.RequestedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) if err != nil { logger.Error(err) From e28678bfae844ebf2f3f16df39506ac3d1d88c4c Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 18 Jul 2024 23:17:47 +0000 Subject: [PATCH 04/35] Install/Update use stored app.channel_id --- cmd/kots/cli/install.go | 17 ++-- pkg/automation/automation.go | 7 +- pkg/handlers/license.go | 11 ++- pkg/kotsadm/objects/configmaps_objects.go | 2 +- pkg/kotsadm/types/deployoptions.go | 2 +- pkg/kotsutil/kots.go | 40 ++++++++- pkg/kotsutil/kots_test.go | 99 +++++++++++++++++++++++ pkg/update/update.go | 24 +++++- pkg/update/update_test.go | 38 +++++++++ 9 files changed, 221 insertions(+), 19 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 01b4b71620..aa64a93a50 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -167,23 +167,20 @@ func InstallCmd() *cobra.Command { return errors.Wrap(err, "failed to extract preferred channel slug") } - // use the preferred channel slug to find the matching channel id in the license - requestedChannelID := "" + // if we are passed a multi channel license , verify that the requested channel is in the license + // so that we can the warn the user immediately if it is not if license != nil { - if license.Spec.Channels == nil { // this is a license format without multiple channels - requestedChannelID = license.Spec.ChannelID - } else if len(license.Spec.Channels) > 0 { + if license.Spec.Channels != nil && len(license.Spec.Channels) > 0 { + foundChannelID := false for _, channel := range license.Spec.Channels { if channel.ChannelSlug == preferredChannelSlug { - requestedChannelID = channel.ChannelID + foundChannelID = true break } } - if requestedChannelID == "" { + if !foundChannelID { return errors.New("requested channel not found in license") } - } else { - return errors.New("no channel id found in license") } } @@ -302,7 +299,7 @@ func InstallCmd() *cobra.Command { IncludeMinio: v.GetBool("with-minio"), IncludeMinioSnapshots: v.GetBool("with-minio"), StrictSecurityContext: v.GetBool("strict-security-context"), - RequestedChannelID: requestedChannelID, + RequestedChannelSlug: preferredChannelSlug, RegistryConfig: *registryConfig, diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 9baf382240..a361a316a4 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -244,7 +244,12 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. desiredAppName := strings.Replace(appSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", appSlug) - a, err := store.GetStore().CreateApp(desiredAppName, instParams.RequestedChannelID, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly) + matchedChannelID, err := kotsutil.FindRequestedChannelID(instParams.RequestedChannelSlug, verifiedLicense) + if err != nil { + return errors.Wrap(err, "failed to find requested channel in license") + } + + a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly) if err != nil { return errors.Wrap(err, "failed to create app record") } diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index 44947919ab..4a2599310e 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -334,7 +334,16 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) - a, err := store.GetStore().CreateApp(desiredAppName, installationParams.RequestedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) + // verify that requested channel slug exists in the license + matchedChannelID, err := kotsutil.FindRequestedChannelID(installationParams.RequestedChannelSlug, verifiedLicense) + if err != nil { + logger.Error(err) + uploadLicenseResponse.Error = "Requested install channel not found in license" + JSON(w, http.StatusBadRequest, uploadLicenseResponse) + return + } + + a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) if err != nil { logger.Error(err) uploadLicenseResponse.Error = err.Error() diff --git a/pkg/kotsadm/objects/configmaps_objects.go b/pkg/kotsadm/objects/configmaps_objects.go index b53b377104..2082570f6b 100644 --- a/pkg/kotsadm/objects/configmaps_objects.go +++ b/pkg/kotsadm/objects/configmaps_objects.go @@ -25,7 +25,7 @@ func KotsadmConfigMap(deployOptions types.DeployOptions) *corev1.ConfigMap { "wait-duration": fmt.Sprintf("%v", deployOptions.Timeout), "with-minio": fmt.Sprintf("%v", deployOptions.IncludeMinio), "app-version-label": deployOptions.AppVersionLabel, - "requested-channel-id": deployOptions.RequestedChannelID, + "requested-channel-slug": deployOptions.RequestedChannelSlug, } if kotsadmversion.KotsadmPullSecret(deployOptions.Namespace, deployOptions.RegistryConfig) != nil { diff --git a/pkg/kotsadm/types/deployoptions.go b/pkg/kotsadm/types/deployoptions.go index 8b5d488017..14d5be2404 100644 --- a/pkg/kotsadm/types/deployoptions.go +++ b/pkg/kotsadm/types/deployoptions.go @@ -56,7 +56,7 @@ type DeployOptions struct { IsMinimalRBAC bool AdditionalNamespaces []string IsGKEAutopilot bool - RequestedChannelID string + RequestedChannelSlug string IdentityConfig kotsv1beta1.IdentityConfig IngressConfig kotsv1beta1.IngressConfig diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 144a72649b..4fca10bc24 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1139,7 +1139,7 @@ type InstallationParams struct { WaitDuration time.Duration WithMinio bool AppVersionLabel string - RequestedChannelID string + RequestedChannelSlug string } func GetInstallationParams(configMapName string) (InstallationParams, error) { @@ -1175,7 +1175,7 @@ func GetInstallationParams(configMapName string) (InstallationParams, error) { autoConfig.WaitDuration, _ = time.ParseDuration(kotsadmConfigMap.Data["wait-duration"]) autoConfig.WithMinio, _ = strconv.ParseBool(kotsadmConfigMap.Data["with-minio"]) autoConfig.AppVersionLabel = kotsadmConfigMap.Data["app-version-label"] - autoConfig.RequestedChannelID = kotsadmConfigMap.Data["requested-channel-id"] + autoConfig.RequestedChannelSlug = kotsadmConfigMap.Data["requested-channel-slug"] if enableImageDeletion, ok := kotsadmConfigMap.Data["enable-image-deletion"]; ok { autoConfig.EnableImageDeletion, _ = strconv.ParseBool(enableImageDeletion) @@ -1600,3 +1600,39 @@ func GetECVersionFromAirgapBundle(airgapBundle string) (string, error) { } return ecVersion, nil } + +func FindRequestedChannelID(requestedSlug string, license *kotsv1beta1.License) (string, error) { + matchedChannelID := "" + if requestedSlug != "" { + // if we do not have a Channels array or its empty, default to using the top level fields for backwards compatibility + if len(license.Spec.Channels) == 0 { + logger.Debug("not a multi-channel license, using top level license channel id") + matchedChannelID = license.Spec.ChannelID + } else { + for _, channel := range license.Spec.Channels { + if channel.ChannelSlug == requestedSlug { + matchedChannelID = channel.ChannelID + break + } + } + if matchedChannelID == "" { + return "", errors.New("requested install channel slug not found in license channels") + } + } + } else { // this is an install from before the channel slug was added to the configmap + logger.Debug("requested channel slug not found in configmap, using top level channel id from license") + matchedChannelID = license.Spec.ChannelID + } + return matchedChannelID, nil +} + +func FindChannel(license *kotsv1beta1.License, channelID string) *kotsv1beta1.Channel { + if len(license.Spec.Channels) > 0 { + for _, channel := range license.Spec.Channels { + if channel.ChannelID == channelID { + return &channel + } + } + } + return nil +} diff --git a/pkg/kotsutil/kots_test.go b/pkg/kotsutil/kots_test.go index 419565549d..873661383c 100644 --- a/pkg/kotsutil/kots_test.go +++ b/pkg/kotsutil/kots_test.go @@ -1057,3 +1057,102 @@ status: {} }) } } + +func TestFindRequestedChannelID(t *testing.T) { + tests := []struct { + name string + license *kotsv1beta1.License + requestedSlug string + expectedChannelID string + expectError bool + }{ + { + name: "Found slug", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id-1", + ChannelSlug: "slug-1", + IsDefault: true, + }, + { + ChannelID: "channel-id-2", + ChannelSlug: "slug-2", + IsDefault: false, + }, + }, + }, + }, + requestedSlug: "slug-2", + expectedChannelID: "channel-id-2", + expectError: false, + }, + { + name: "Empty requested slug", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "top-level-channel-id", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id-1", + ChannelSlug: "channel-slug-1", + }, + { + ChannelID: "channel-id-2", + ChannelSlug: "channel-slug-2", + }, + }, + }, + }, + requestedSlug: "", + expectedChannelID: "top-level-channel-id", + expectError: false, + }, + { + name: "Legacy license with no / empty channels", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "test-channel-id", + }, + }, + requestedSlug: "test-slug", + expectedChannelID: "test-channel-id", + expectError: false, + }, + { + name: "No matching slug should error", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "top-level-channel-id", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id-1", + ChannelSlug: "channel-slug-1", + }, + { + ChannelID: "channel-id-2", + ChannelSlug: "channel-slug-2", + }, + }, + }, + }, + requestedSlug: "non-existent-slug", + expectedChannelID: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + channelID, err := kotsutil.FindRequestedChannelID(tt.requestedSlug, tt.license) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedChannelID, channelID) + } + }) + } +} diff --git a/pkg/update/update.go b/pkg/update/update.go index badc7aefa8..dc4dea9952 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -29,7 +29,25 @@ func InitAvailableUpdatesDir() error { } func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { - updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, license.Spec.ChannelID) + + var currentChannelID string + var currentChannelName string + if app.ChannelID == "" { + // if we have no channel id , this is an install from before multi-channel was introduced + // so we'll preserve existing behavior and just use the top level channel id + currentChannelID = license.Spec.ChannelID + } else { + currentChannelID = app.ChannelID + } + + foundChannel := kotsutil.FindChannel(license, currentChannelID) + if foundChannel != nil { + currentChannelName = foundChannel.ChannelName + } else { + currentChannelName = license.Spec.ChannelName + } + + updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, currentChannelID) if err != nil { return nil, errors.Wrap(err, "failed to get current update cursor") } @@ -39,8 +57,8 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k License: license, LastUpdateCheckAt: app.LastUpdateCheckAt, CurrentCursor: updateCursor, - CurrentChannelID: license.Spec.ChannelID, - CurrentChannelName: license.Spec.ChannelName, + CurrentChannelID: currentChannelID, + CurrentChannelName: currentChannelName, ChannelChanged: app.ChannelChanged, SortOrder: "desc", // get the latest updates first ReportingInfo: reporting.GetReportingInfo(app.ID), diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 091dd8c725..57cdd1a2e2 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -150,6 +150,44 @@ func TestGetAvailableUpdates(t *testing.T) { want: []types.AvailableUpdate{}, wantErr: true, }, + { + name: "uses installed channel id when multi-channel present", + args: args{ + kotsStore: mockStore, + app: &apptypes.App{ + ID: "app-id", + ChannelID: "channel-id2", // explicitly using the non-default channel + }, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "channel-id", + ChannelName: "channel-name", + AppSlug: "app-slug", + LicenseID: "license-id", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id", + ChannelName: "channel-name", + IsDefault: true, + }, + { + ChannelID: "channel-id2", + ChannelName: "channel-name2", + IsDefault: false, + }, + }, + }, + }, + }, + channelReleases: []upstream.ChannelRelease{}, + setup: func(t *testing.T, args args, licenseEndpoint string) { + t.Setenv("USE_MOCK_REPORTING", "1") + args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.Channels[1].ChannelID).Return("1", nil) + }, + want: []types.AvailableUpdate{}, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 53fac7fec15524b261e9bf9ad65310615d8c9c95 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 19 Jul 2024 16:06:04 +0000 Subject: [PATCH 05/35] cleanup --- pkg/automation/automation.go | 2 +- pkg/handlers/license.go | 2 +- pkg/kotsutil/kots.go | 28 +++++--- pkg/kotsutil/kots_test.go | 103 ++++++++++++++++++++++++++++- pkg/update/update.go | 29 ++++---- pkg/updatechecker/updatechecker.go | 22 ++++-- 6 files changed, 153 insertions(+), 33 deletions(-) diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index a361a316a4..7ed76d7f53 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -244,7 +244,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. desiredAppName := strings.Replace(appSlug, "-", " ", 0) upstreamURI := fmt.Sprintf("replicated://%s", appSlug) - matchedChannelID, err := kotsutil.FindRequestedChannelID(instParams.RequestedChannelSlug, verifiedLicense) + matchedChannelID, err := kotsutil.FindChannelIDInLicense(instParams.RequestedChannelSlug, verifiedLicense) if err != nil { return errors.Wrap(err, "failed to find requested channel in license") } diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index 4a2599310e..e13094cc84 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -335,7 +335,7 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) // verify that requested channel slug exists in the license - matchedChannelID, err := kotsutil.FindRequestedChannelID(installationParams.RequestedChannelSlug, verifiedLicense) + matchedChannelID, err := kotsutil.FindChannelIDInLicense(installationParams.RequestedChannelSlug, verifiedLicense) if err != nil { logger.Error(err) uploadLicenseResponse.Error = "Requested install channel not found in license" diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 4fca10bc24..f9e4bc52e2 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1601,7 +1601,7 @@ func GetECVersionFromAirgapBundle(airgapBundle string) (string, error) { return ecVersion, nil } -func FindRequestedChannelID(requestedSlug string, license *kotsv1beta1.License) (string, error) { +func FindChannelIDInLicense(requestedSlug string, license *kotsv1beta1.License) (string, error) { matchedChannelID := "" if requestedSlug != "" { // if we do not have a Channels array or its empty, default to using the top level fields for backwards compatibility @@ -1626,13 +1626,25 @@ func FindRequestedChannelID(requestedSlug string, license *kotsv1beta1.License) return matchedChannelID, nil } -func FindChannel(license *kotsv1beta1.License, channelID string) *kotsv1beta1.Channel { - if len(license.Spec.Channels) > 0 { - for _, channel := range license.Spec.Channels { - if channel.ChannelID == channelID { - return &channel - } +func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { + if channelID == "" || len(license.Spec.Channels) == 0 { + if license.Spec.ChannelID != channelID { + return nil, errors.New("channel not found in license") + } + // this is an install from before multi channel support, so emulate it using the top level info + return &kotsv1beta1.Channel{ + ChannelID: license.Spec.ChannelID, + ChannelName: license.Spec.ChannelName, + IsDefault: true, + IsSemverRequired: license.Spec.IsSemverRequired, + }, nil + } + + for _, channel := range license.Spec.Channels { + if channel.ChannelID == channelID { + return &channel, nil } } - return nil + + return nil, errors.New("channel not found in license") } diff --git a/pkg/kotsutil/kots_test.go b/pkg/kotsutil/kots_test.go index 873661383c..5b90ec047c 100644 --- a/pkg/kotsutil/kots_test.go +++ b/pkg/kotsutil/kots_test.go @@ -1058,7 +1058,7 @@ status: {} } } -func TestFindRequestedChannelID(t *testing.T) { +func TestFindChannelIDInLicense(t *testing.T) { tests := []struct { name string license *kotsv1beta1.License @@ -1145,7 +1145,7 @@ func TestFindRequestedChannelID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - channelID, err := kotsutil.FindRequestedChannelID(tt.requestedSlug, tt.license) + channelID, err := kotsutil.FindChannelIDInLicense(tt.requestedSlug, tt.license) if tt.expectError { require.Error(t, err) @@ -1156,3 +1156,102 @@ func TestFindRequestedChannelID(t *testing.T) { }) } } + +func TestFindChannelInLicense(t *testing.T) { + tests := []struct { + name string + license *kotsv1beta1.License + requestedID string + expectedChannel *kotsv1beta1.Channel + expectError bool + }{ + { + name: "Find multi channel license", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id-1", + ChannelName: "name-1", + IsDefault: true, + IsSemverRequired: true, + }, + { + ChannelID: "channel-id-2", + ChannelName: "name-2", + IsDefault: false, + IsSemverRequired: false, + }, + }, + }, + }, + requestedID: "channel-id-2", + expectedChannel: &kotsv1beta1.Channel{ + ChannelID: "channel-id-2", + ChannelName: "name-2", + IsDefault: false, + IsSemverRequired: false, + }, + expectError: false, + }, + { + name: "Legacy license with no / empty channels", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "test-channel-id", + ChannelName: "test-channel-name", + IsSemverRequired: true, + }, + }, + requestedID: "test-channel-id", + expectedChannel: &kotsv1beta1.Channel{ + ChannelID: "test-channel-id", + ChannelName: "test-channel-name", + IsSemverRequired: true, + IsDefault: true, + }, + expectError: false, + }, + { + name: "No matching ID should error", + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "channel-id-1", + ChannelName: "name-1", + IsSemverRequired: true, + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-id-1", + ChannelName: "name-1", + IsDefault: true, + IsSemverRequired: true, + }, + { + ChannelID: "channel-id-2", + ChannelName: "name-2", + IsDefault: false, + IsSemverRequired: false, + }, + }, + }, + }, + requestedID: "non-existent-id", + expectedChannel: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + channel, err := kotsutil.FindChannelInLicense(tt.requestedID, tt.license) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, channel) + require.Equal(t, tt.expectedChannel, channel) + } + }) + } +} diff --git a/pkg/update/update.go b/pkg/update/update.go index dc4dea9952..8021cfac23 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -29,25 +29,20 @@ func InitAvailableUpdatesDir() error { } func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { - - var currentChannelID string - var currentChannelName string + var err error + var licenseChan *kotsv1beta1.Channel if app.ChannelID == "" { - // if we have no channel id , this is an install from before multi-channel was introduced - // so we'll preserve existing behavior and just use the top level channel id - currentChannelID = license.Spec.ChannelID - } else { - currentChannelID = app.ChannelID - } - - foundChannel := kotsutil.FindChannel(license, currentChannelID) - if foundChannel != nil { - currentChannelName = foundChannel.ChannelName + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } } else { - currentChannelName = license.Spec.ChannelName + if licenseChan, err = kotsutil.FindChannelInLicense(app.ChannelID, license); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } } - updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, currentChannelID) + updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, licenseChan.ChannelID) if err != nil { return nil, errors.Wrap(err, "failed to get current update cursor") } @@ -57,8 +52,8 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k License: license, LastUpdateCheckAt: app.LastUpdateCheckAt, CurrentCursor: updateCursor, - CurrentChannelID: currentChannelID, - CurrentChannelName: currentChannelName, + CurrentChannelID: licenseChan.ChannelID, + CurrentChannelName: licenseChan.ChannelName, ChannelChanged: app.ChannelChanged, SortOrder: "desc", // get the latest updates first ReportingInfo: reporting.GetReportingInfo(app.ID), diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index 44438d7134..78347f60e6 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -13,6 +13,7 @@ import ( apptypes "github.com/replicatedhq/kots/pkg/app/types" license "github.com/replicatedhq/kots/pkg/kotsadmlicense" upstream "github.com/replicatedhq/kots/pkg/kotsadmupstream" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/preflight" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" @@ -26,6 +27,7 @@ import ( upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" cron "github.com/robfig/cron/v3" "go.uber.org/zap" "k8s.io/apimachinery/pkg/util/wait" @@ -226,7 +228,19 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- return nil, errors.Wrap(err, "failed to get app") } - updateCursor, err := store.GetCurrentUpdateCursor(a.ID, latestLicense.Spec.ChannelID) + var licenseChan *kotsv1beta1.Channel + if a.ChannelID == "" { + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(latestLicense.Spec.ChannelID, latestLicense); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + } + + updateCursor, err := store.GetCurrentUpdateCursor(a.ID, licenseChan.ChannelID) if err != nil { return nil, errors.Wrap(err, "failed to get current update cursor") } @@ -235,8 +249,8 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- License: latestLicense, LastUpdateCheckAt: a.LastUpdateCheckAt, CurrentCursor: updateCursor, - CurrentChannelID: latestLicense.Spec.ChannelID, - CurrentChannelName: latestLicense.Spec.ChannelName, + CurrentChannelID: licenseChan.ChannelID, + CurrentChannelName: licenseChan.ChannelName, ChannelChanged: a.ChannelChanged, Silent: false, ReportingInfo: reporting.GetReportingInfo(a.ID), @@ -266,7 +280,7 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- return nil, errors.Errorf("no app versions found for app %s in downstream %s", opts.AppID, d.ClusterID) } - filteredUpdates := removeOldUpdates(updates.Updates, appVersions, latestLicense.Spec.IsSemverRequired) + filteredUpdates := removeOldUpdates(updates.Updates, appVersions, licenseChan.IsSemverRequired) var availableReleases []types.UpdateCheckRelease availableSequence := appVersions.AllVersions[0].Sequence + 1 From 1355d88f08447340ac738f653d67516a96a24d0c Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 19 Jul 2024 17:22:29 +0000 Subject: [PATCH 06/35] Start supporting IsSemversion checks using the license channel entry downstream vers check uses IsSemverRequired from channel license --- pkg/handlers/update_checker_spec.go | 21 +++++++++++++++- pkg/store/kotsstore/app_store.go | 22 +++++++++++++++++ pkg/store/kotsstore/downstream_store.go | 33 ++++++++++++++++++++++--- pkg/store/kotsstore/version_store.go | 15 ++++++++++- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index 5dbc2b163c..4ace704efd 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -11,6 +11,7 @@ import ( "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/updatechecker" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" cron "github.com/robfig/cron/v3" ) @@ -56,8 +57,26 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque return } + var licenseChan *kotsv1beta1.Channel + if foundApp.ChannelID == "" { + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + updateCheckerSpecResponse.Error = "failed to find channel in license" + logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) + JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) + return + } + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { + updateCheckerSpecResponse.Error = "failed to find channel in license" + logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) + JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) + return + } + } + // Check if the deploy update configuration is valid based on app channel - if license.Spec.IsSemverRequired { + if licenseChan.IsSemverRequired { if configureAutomaticUpdatesRequest.AutoDeploy == apptypes.AutoDeploySequence { updateCheckerSpecResponse.Error = "automatic updates based on sequence type are not supported for semantic versioning apps" JSON(w, http.StatusUnprocessableEntity, updateCheckerSpecResponse) diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index e944c9d827..927793e54d 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -599,6 +599,28 @@ func (s *KOTSStore) SetAppChannelChanged(appID string, channelChanged bool) erro return nil } +func (s *KOTSStore) GetAppChannelID(appID string) (string, error) { + db := persistence.MustGetDBSession() + query := `select channel_id from app where id = ?` + rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{appID}, + }) + if err != nil { + return "", fmt.Errorf("failed to query: %v: %v", err, rows.Err) + } + if !rows.Next() { + return "", ErrNotFound + } + + var channelID gorqlite.NullString + if err := rows.Scan(&channelID); err != nil { + return "", errors.Wrap(err, "failed to scan channel id") + } + + return channelID.String, nil +} + func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { db := persistence.MustGetDBSession() diff --git a/pkg/store/kotsstore/downstream_store.go b/pkg/store/kotsstore/downstream_store.go index 1a87320104..65eaa6dfbe 100644 --- a/pkg/store/kotsstore/downstream_store.go +++ b/pkg/store/kotsstore/downstream_store.go @@ -409,7 +409,21 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo if err != nil { return nil, errors.Wrap(err, "failed to get app license") } - downstreamtypes.SortDownstreamVersions(result.AllVersions, license.Spec.IsSemverRequired) + + foundChannelID, err := s.GetAppChannelID(appID) + var licenseChan *kotsv1beta1.Channel + if foundChannelID == "" { + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + } + + downstreamtypes.SortDownstreamVersions(result.AllVersions, licenseChan.IsSemverRequired) // retrieve additional details about the latest downloaded version, // since it's used for detecting things like if a certain feature is enabled or not. @@ -422,7 +436,7 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo if err := s.AddDownstreamVersionDetails(appID, clusterID, v, false); err != nil { return nil, errors.Wrap(err, "failed to add details to latest downloaded version") } - v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, result, license.Spec.IsSemverRequired) + v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, result, licenseChan.IsSemverRequired) break } @@ -673,8 +687,21 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string, if err != nil { return errors.Wrap(err, "failed to get app license") } + foundChannelID, err := s.GetAppChannelID(appID) + var licenseChan *kotsv1beta1.Channel + if foundChannelID == "" { + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + errors.Wrap(err, "failed to find channel in license") + } + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { + errors.Wrap(err, "failed to find channel in license") + } + } + for _, v := range versions { - v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, allVersions, license.Spec.IsSemverRequired) + v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, allVersions, licenseChan.IsSemverRequired) } } diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 92492cc581..2ffae17acf 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -292,9 +292,22 @@ func (s *KOTSStore) GetAppVersionBaseSequence(appID string, versionLabel string) return -1, errors.Wrap(err, "failed to get app license") } + foundChannelID, err := s.GetAppChannelID(appID) + var licenseChan *kotsv1beta1.Channel + if foundChannelID == "" { + // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced + if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + return -1, errors.Wrap(err, "failed to find channel in license") + } + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { + return -1, errors.Wrap(err, "failed to find channel in license") + } + } + // add to the top of the list and sort appVersions.AllVersions = append([]*downstreamtypes.DownstreamVersion{mockVersion}, appVersions.AllVersions...) - downstreamtypes.SortDownstreamVersions(appVersions.AllVersions, license.Spec.IsSemverRequired) + downstreamtypes.SortDownstreamVersions(appVersions.AllVersions, licenseChan.IsSemverRequired) var baseVersion *downstreamtypes.DownstreamVersion for i, v := range appVersions.AllVersions { From b5ae7c4bfd3503b157d815a09f1482721c845bbe Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 19 Jul 2024 22:14:39 +0000 Subject: [PATCH 07/35] Backfill app.channel_id when required Update tests --- pkg/handlers/update_checker_spec.go | 21 +++++++++++---------- pkg/kotsutil/kots.go | 11 +++++++++++ pkg/store/kotsstore/downstream_store.go | 19 +++++++++---------- pkg/store/kotsstore/version_store.go | 14 +++++++------- pkg/update/update.go | 7 +++++-- pkg/update/update_test.go | 9 +++++++-- pkg/updatechecker/updatechecker.go | 14 +++++++------- 7 files changed, 57 insertions(+), 38 deletions(-) diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index 4ace704efd..b96e62df20 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -59,20 +59,21 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque var licenseChan *kotsv1beta1.Channel if foundApp.ChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { - updateCheckerSpecResponse.Error = "failed to find channel in license" - logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) - JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) - return - } - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { - updateCheckerSpecResponse.Error = "failed to find channel in license" + backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) + if err := store.GetStore().SetAppChannelID(foundApp.ID, backfillID); err != nil { + updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) return } + foundApp.ChannelID = backfillID + } + + if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { + updateCheckerSpecResponse.Error = "failed to find channel in license" + logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) + JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) + return } // Check if the deploy update configuration is valid based on app channel diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index f9e4bc52e2..62736a59fe 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1648,3 +1648,14 @@ func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kots return nil, errors.New("channel not found in license") } + +func GetBackfillChannelIDFromLicense(license *kotsv1beta1.License) string { + for _, channel := range license.Spec.Channels { + if channel.IsDefault { + return channel.ChannelID + } + } + // either this isn't a multi channel license or the default channel is not set + // either way we should fall back to the top level channel id for backwards compatibility + return license.Spec.ChannelID +} diff --git a/pkg/store/kotsstore/downstream_store.go b/pkg/store/kotsstore/downstream_store.go index 65eaa6dfbe..f3650c6c6d 100644 --- a/pkg/store/kotsstore/downstream_store.go +++ b/pkg/store/kotsstore/downstream_store.go @@ -413,15 +413,14 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo foundChannelID, err := s.GetAppChannelID(appID) var licenseChan *kotsv1beta1.Channel if foundChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + foundChannelID = kotsutil.GetBackfillChannelIDFromLicense(license) + if err := s.SetAppChannelID(appID, foundChannelID); err != nil { + return nil, errors.Wrap(err, "failed to backfill app channel id from license") } } + if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } downstreamtypes.SortDownstreamVersions(result.AllVersions, licenseChan.IsSemverRequired) @@ -690,9 +689,9 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string, foundChannelID, err := s.GetAppChannelID(appID) var licenseChan *kotsv1beta1.Channel if foundChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { - errors.Wrap(err, "failed to find channel in license") + foundChannelID = kotsutil.GetBackfillChannelIDFromLicense(license) + if err := s.SetAppChannelID(appID, foundChannelID); err != nil { + return errors.Wrap(err, "failed to backfill app channel id from license") } } else { if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 2ffae17acf..bed33b332d 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -295,14 +295,14 @@ func (s *KOTSStore) GetAppVersionBaseSequence(appID string, versionLabel string) foundChannelID, err := s.GetAppChannelID(appID) var licenseChan *kotsv1beta1.Channel if foundChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { - return -1, errors.Wrap(err, "failed to find channel in license") - } - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { - return -1, errors.Wrap(err, "failed to find channel in license") + backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) + if err := s.SetAppChannelID(appID, backfillID); err != nil { + return -1, errors.Wrap(err, "failed to backfill app channel id from license") } + foundChannelID = backfillID + } + if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { + return -1, errors.Wrap(err, "failed to find channel in license") } // add to the top of the list and sort diff --git a/pkg/update/update.go b/pkg/update/update.go index 8021cfac23..f423e6bad6 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -32,8 +32,11 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k var err error var licenseChan *kotsv1beta1.Channel if app.ChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(license.Spec.ChannelID, license); err != nil { + backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) + if err := kotsStore.SetAppChannelID(app.ID, backfillID); err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id") + } + if licenseChan, err = kotsutil.FindChannelInLicense(backfillID, license); err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } } else { diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 57cdd1a2e2..957cd101a7 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -43,7 +43,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", + ID: "app-id", + ChannelID: "", // using legacy non-multi chan license }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -58,6 +59,7 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, @@ -68,7 +70,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", + ID: "app-id", + ChannelID: "", // using legacy non-multi chan license }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -100,6 +103,7 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{ @@ -145,6 +149,7 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint + mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index 78347f60e6..d44969ed99 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -230,14 +230,14 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- var licenseChan *kotsv1beta1.Channel if a.ChannelID == "" { - // TODO: Backfill app.ChannelID in the database, this is an install from before multi-channel was introduced - if licenseChan, err = kotsutil.FindChannelInLicense(latestLicense.Spec.ChannelID, latestLicense); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + backfillID := kotsutil.GetBackfillChannelIDFromLicense(latestLicense) + if err := store.SetAppChannelID(a.ID, backfillID); err != nil { + return nil, errors.Wrap(err, "failed to backfill app channel id from license") } + a.ChannelID = backfillID + } + if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") } updateCursor, err := store.GetCurrentUpdateCursor(a.ID, licenseChan.ChannelID) From f2dec755780fefa3365908e8e2e4ec083a03b9bd Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 19 Jul 2024 23:20:05 +0000 Subject: [PATCH 08/35] kots pull checks if channel slug in license --- cmd/kots/cli/install.go | 28 ++++++++++++-------------- cmd/kots/cli/pull.go | 44 ++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index aa64a93a50..2b5f55cc7d 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -167,21 +167,10 @@ func InstallCmd() *cobra.Command { return errors.Wrap(err, "failed to extract preferred channel slug") } - // if we are passed a multi channel license , verify that the requested channel is in the license - // so that we can the warn the user immediately if it is not - if license != nil { - if license.Spec.Channels != nil && len(license.Spec.Channels) > 0 { - foundChannelID := false - for _, channel := range license.Spec.Channels { - if channel.ChannelSlug == preferredChannelSlug { - foundChannelID = true - break - } - } - if !foundChannelID { - return errors.New("requested channel not found in license") - } - } + // If we are passed a multi-channel license, verify that the requested channel is in the license + // so that we can warn the user immediately if it is not. + if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) { + return errors.New("requested channel not found in license") } namespace := v.GetString("namespace") @@ -1098,3 +1087,12 @@ func checkPreflightResults(response *handlers.GetPreflightResultResponse, skipPr return true, nil } + +func slugInLicenseChannels(slug string, license *kotsv1beta1.License) bool { + for _, channel := range license.Spec.Channels { + if channel.ChannelSlug == slug { + return true + } + } + return false +} diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index a2df9b1f3d..68bda2ac97 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -11,7 +11,6 @@ import ( "github.com/replicatedhq/kots/pkg/pull" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -43,11 +42,13 @@ func PullCmd() *cobra.Command { } } - appSlug, err := getAppSlugForPull(args[0], v.GetString("license-file")) + license, err := getLicense(v) if err != nil { - return errors.Wrap(err, "failed to determine app slug") + return errors.Wrap(err, "failed to get license") } + appSlug := getAppSlugForPull(args[0], license) + namespace, err := getNamespaceOrDefault(v.GetString("namespace")) if err != nil { return errors.Wrap(err, "failed to get namespace") @@ -98,6 +99,17 @@ func PullCmd() *cobra.Command { } upstream := pull.RewriteUpstream(args[0]) + preferredChannelSlug, err := extractPreferredChannelSlug(upstream) + if err != nil { + return errors.Wrap(err, "failed to extract preferred channel slug") + } + + // If we are passed a multi-channel license, verify that the requested channel is in the license + // so that we can warn the user immediately if it is not. + if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) { + return errors.New("requested channel not found in license") + } + renderDir, err := pull.Pull(upstream, pullOptions) if err != nil { return errors.Wrap(err, "failed to pull") @@ -146,28 +158,10 @@ func PullCmd() *cobra.Command { return cmd } -func getAppSlugForPull(uri string, licenseFile string) (string, error) { +func getAppSlugForPull(uri string, license *kotsv1beta1.License) string { appSlug := strings.Split(uri, "/")[0] - if licenseFile == "" { - return appSlug, nil - } - - licenseData, err := os.ReadFile(licenseFile) - if err != nil { - return "", errors.Wrap(err, "failed to read license file") - } - - decode := scheme.Codecs.UniversalDeserializer().Decode - decoded, gvk, err := decode(licenseData, nil, nil) - if err != nil { - return "", errors.Wrap(err, "unable to decode license file") + if license == nil { + return appSlug } - - if gvk.Group != "kots.io" || gvk.Version != "v1beta1" || gvk.Kind != "License" { - return "", errors.New("not an application license") - } - - license := decoded.(*kotsv1beta1.License) - - return license.Spec.AppSlug, nil + return license.Spec.AppSlug } From cf7faa8767fa447d9c35b660ab62ac533396e0d4 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Sun, 21 Jul 2024 23:14:37 +0000 Subject: [PATCH 09/35] Airgap license channel id checks use the new channels obj --- pkg/handlers/upgrade_service.go | 5 ++++- pkg/pull/pull.go | 2 +- pkg/update/update.go | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/handlers/upgrade_service.go b/pkg/handlers/upgrade_service.go index 2fdf54228a..6776d83e9d 100644 --- a/pkg/handlers/upgrade_service.go +++ b/pkg/handlers/upgrade_service.go @@ -118,7 +118,10 @@ func canStartUpgradeService(a *apptypes.App, r StartUpgradeServiceRequest) (bool if err != nil { return false, "", errors.Wrap(err, "failed to find airgap metadata") } - if currLicense.Spec.ChannelID != airgap.Spec.ChannelID || r.ChannelID != airgap.Spec.ChannelID { + if _, err := kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, currLicense); err != nil { + return false, "channel mismatch, channel not in license", nil + } + if r.ChannelID != airgap.Spec.ChannelID { return false, "channel mismatch", nil } isDeployable, nonDeployableCause, err := update.IsAirgapUpdateDeployable(a, airgap) diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 65020d7140..369b188f18 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -236,7 +236,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { logger.Infof("Expecting to install version %s but airgap bundle version is %s.", fetchOptions.AppVersionLabel, airgap.Spec.VersionLabel) } - if fetchOptions.License.Spec.ChannelID != airgap.Spec.ChannelID { + if _, err = kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, fetchOptions.License); err != nil { return "", util.ActionableError{ NoRetry: true, // if this is airgap upload, make sure to free up tmp space Message: fmt.Sprintf("License (%s) and airgap bundle (%s) channels do not match.", fetchOptions.License.Spec.ChannelName, airgap.Spec.ChannelName), diff --git a/pkg/update/update.go b/pkg/update/update.go index f423e6bad6..66e62d6bea 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -104,8 +104,8 @@ func GetAvailableAirgapUpdates(app *apptypes.App, license *kotsv1beta1.License) if airgap.Spec.AppSlug != license.Spec.AppSlug { return nil } - if airgap.Spec.ChannelID != license.Spec.ChannelID { - return nil + if _, err = kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, license); err != nil { + return nil // skip airgap updates that are not for the current channel, preserving previous behavior } deployable, nonDeployableCause, err := IsAirgapUpdateDeployable(app, airgap) From ba4eed907089b663fb613fdcdbfd2ad4c4d72134 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Mon, 22 Jul 2024 16:45:06 +0000 Subject: [PATCH 10/35] pr feedback - logging --- pkg/kotsutil/kots.go | 4 ++-- pkg/update/update.go | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 62736a59fe..f4c6c32754 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1629,7 +1629,7 @@ func FindChannelIDInLicense(requestedSlug string, license *kotsv1beta1.License) func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { if channelID == "" || len(license.Spec.Channels) == 0 { if license.Spec.ChannelID != channelID { - return nil, errors.New("channel not found in license") + return nil, errors.New("channel not found non-multi channel license") } // this is an install from before multi channel support, so emulate it using the top level info return &kotsv1beta1.Channel{ @@ -1646,7 +1646,7 @@ func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kots } } - return nil, errors.New("channel not found in license") + return nil, errors.New("channel not found in multi channel format license") } func GetBackfillChannelIDFromLicense(license *kotsv1beta1.License) string { diff --git a/pkg/update/update.go b/pkg/update/update.go index 66e62d6bea..b50d379b55 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -8,12 +8,14 @@ import ( "github.com/pkg/errors" apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/reporting" storepkg "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/update/types" upstreampkg "github.com/replicatedhq/kots/pkg/upstream" upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "go.uber.org/zap" ) // a ephemeral directory to store available updates @@ -37,7 +39,7 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k return nil, errors.Wrap(err, "failed to backfill channel id") } if licenseChan, err = kotsutil.FindChannelInLicense(backfillID, license); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + return nil, errors.Wrap(err, "failed to find backfilled channel in license") } } else { if licenseChan, err = kotsutil.FindChannelInLicense(app.ChannelID, license); err != nil { @@ -105,6 +107,10 @@ func GetAvailableAirgapUpdates(app *apptypes.App, license *kotsv1beta1.License) return nil } if _, err = kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, license); err != nil { + logger.Info("skipping airgap update check for channel not found in current license", + zap.String("airgap_channelName", airgap.Spec.ChannelName), + zap.String("airgap_channelID", airgap.Spec.ChannelID), + ) return nil // skip airgap updates that are not for the current channel, preserving previous behavior } From bd383738d5e31cc58205059d44a2e6aff8904414 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Mon, 22 Jul 2024 19:26:15 +0000 Subject: [PATCH 11/35] pr feedback - fetch latest license at install check --- cmd/kots/cli/install.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 2b5f55cc7d..54eda7339e 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -169,8 +169,20 @@ func InstallCmd() *cobra.Command { // If we are passed a multi-channel license, verify that the requested channel is in the license // so that we can warn the user immediately if it is not. - if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) { - return errors.New("requested channel not found in license") + if license != nil { + if isAirgap && !slugInLicenseChannels(preferredChannelSlug, license) { + return errors.New("requested channel not found in supplied license") + } + log.ActionWithSpinner("Checking for license update") + updatedLicense, err := replicatedapp.GetLatestLicense(license) + if err != nil { + log.FinishSpinnerWithError() + return errors.Wrap(err, "failed to get latest license") + } + log.FinishSpinner() + if !slugInLicenseChannels(preferredChannelSlug, updatedLicense.License) { + return errors.New("requested channel not found in latest license") + } } namespace := v.GetString("namespace") From c8cab806031c5063700bd4671ef2a2bac9656862 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Mon, 22 Jul 2024 20:12:30 +0000 Subject: [PATCH 12/35] clean up --- cmd/kots/cli/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 54eda7339e..3c1cfc451d 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -167,8 +167,7 @@ func InstallCmd() *cobra.Command { return errors.Wrap(err, "failed to extract preferred channel slug") } - // If we are passed a multi-channel license, verify that the requested channel is in the license - // so that we can warn the user immediately if it is not. + // fetch the latest version of the license to verify channel access if online install if license != nil { if isAirgap && !slugInLicenseChannels(preferredChannelSlug, license) { return errors.New("requested channel not found in supplied license") @@ -183,6 +182,7 @@ func InstallCmd() *cobra.Command { if !slugInLicenseChannels(preferredChannelSlug, updatedLicense.License) { return errors.New("requested channel not found in latest license") } + license = updatedLicense.License } namespace := v.GetString("namespace") From fb0e0f027cc0bb38c0f5161874a9d1a6ae21c22c Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 23 Jul 2024 01:29:30 +0000 Subject: [PATCH 13/35] Update tests --- pkg/handlers/upgrade_service.go | 10 +++ pkg/handlers/upgrade_service_test.go | 94 +++++++++++++++++++++++++++ pkg/kotsutil/kots.go | 2 +- pkg/update/required.go | 7 +- pkg/update/required_test.go | 96 +++++++++++++++++++++++++--- 5 files changed, 197 insertions(+), 12 deletions(-) diff --git a/pkg/handlers/upgrade_service.go b/pkg/handlers/upgrade_service.go index 6776d83e9d..9999d5bb93 100644 --- a/pkg/handlers/upgrade_service.go +++ b/pkg/handlers/upgrade_service.go @@ -75,6 +75,16 @@ func (h *Handler) StartUpgradeService(w http.ResponseWriter, r *http.Request) { return } + if foundApp.IsAirgap && foundApp.ChannelID != request.ChannelID { + // The update is deployable, so we should update the apps stored channelID similar to install + // time when channelID is set via the requested-channel-id in the configmap. + if err := store.GetStore().SetAppChannelID(foundApp.ID, request.ChannelID); err != nil { + response.Error = "failed update requested channel ID" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + } + } + if err := startUpgradeService(foundApp, request); err != nil { response.Error = "failed to start upgrade service" logger.Error(errors.Wrap(err, response.Error)) diff --git a/pkg/handlers/upgrade_service_test.go b/pkg/handlers/upgrade_service_test.go index 97de2268b4..59aacf031c 100644 --- a/pkg/handlers/upgrade_service_test.go +++ b/pkg/handlers/upgrade_service_test.go @@ -86,6 +86,37 @@ spec: signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pZEdWemRHTjFjM1J2YldWeUluMHNJbk53WldNaU9uc2liR2xqWlc1elpVbEVJam9pTVhaMWMwOXZhM2hCVm5BeGRHdFNSM1Y1ZUc1R01qTlFTbU54SWl3aWJHbGpaVzV6WlZSNWNHVWlPaUp3Y205a0lpd2lZM1Z6ZEc5dFpYSk9ZVzFsSWpvaVZHVnpkQ0JEZFhOMGIyMWxjaUlzSW1Gd2NGTnNkV2NpT2lKdGVTMWhjSEFpTENKamFHRnVibVZzU1VRaU9pSXhkblZ6U1ZsYVRFRldlRTFITm5FM05qQlBTbTFTUzJvMWFUVWlMQ0pqYUdGdWJtVnNUbUZ0WlNJNklrMTVJRU5vWVc1dVpXd2lMQ0pzYVdObGJuTmxVMlZ4ZFdWdVkyVWlPamNzSW1WdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5eVpYQnNhV05oZEdWa0xtRndjQ0lzSW1WdWRHbDBiR1Z0Wlc1MGN5STZleUppYjI5c1gyWnBaV3hrSWpwN0luUnBkR3hsSWpvaVFtOXZiQ0JHYVdWc1pDSXNJblpoYkhWbElqcDBjblZsTENKMllXeDFaVlI1Y0dVaU9pSkNiMjlzWldGdUluMHNJbVY0Y0dseVpYTmZZWFFpT25zaWRHbDBiR1VpT2lKRmVIQnBjbUYwYVc5dUlpd2laR1Z6WTNKcGNIUnBiMjRpT2lKTWFXTmxibk5sSUVWNGNHbHlZWFJwYjI0aUxDSjJZV3gxWlNJNklqSXdNekF0TURjdE1qZFVNREE2TURBNk1EQmFJaXdpZG1Gc2RXVlVlWEJsSWpvaVUzUnlhVzVuSW4wc0ltaHBaR1JsYmw5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtocFpHUmxiaUJHYVdWc1pDSXNJblpoYkhWbElqb2lkR2hwY3lCcGN5QnpaV055WlhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lMQ0pwYzBocFpHUmxiaUk2ZEhKMVpYMHNJbWx1ZEY5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtsdWRDQkdhV1ZzWkNJc0luWmhiSFZsSWpveE1qTXNJblpoYkhWbFZIbHdaU0k2SWtsdWRHVm5aWElpZlN3aWMzUnlhVzVuWDJacFpXeGtJanA3SW5ScGRHeGxJam9pVTNSeWFXNW5SbWxsYkdRaUxDSjJZV3gxWlNJNkluTnBibWRzWlNCc2FXNWxJSFJsZUhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lmU3dpZEdWNGRGOW1hV1ZzWkNJNmV5SjBhWFJzWlNJNklsUmxlSFFnUm1sbGJHUWlMQ0oyWVd4MVpTSTZJbTExYkhScFhHNXNhVzVsWEc1MFpYaDBJaXdpZG1Gc2RXVlVlWEJsSWpvaVZHVjRkQ0o5ZlN3aWFYTkJhWEpuWVhCVGRYQndiM0owWldRaU9uUnlkV1VzSW1selIybDBUM0J6VTNWd2NHOXlkR1ZrSWpwMGNuVmxMQ0pwYzFOdVlYQnphRzkwVTNWd2NHOXlkR1ZrSWpwMGNuVmxmWDA9IiwiaW5uZXJTaWduYXR1cmUiOiJleUpzYVdObGJuTmxVMmxuYm1GMGRYSmxJam9pYUhneE1XTXZUR1ozUTNoVE5YRmtRWEJGU1hGdVRrMU9NMHBLYTJzNFZHZFhSVVpzVDFKVlJ6UjJjR1YzZEZoV1YzbG1lamRZY0hBd1ExazJZamRyUVRSS2N6TklhR3d3YkZJMFdUQTFMemN2UVVkQ2FEZFZNSGczUkhaTVozUXpVM00wYm5GTFZTdFhXRXBTVHpKWVFVRnZSME4xZFRWR1RGcHJRVWhYY1RSUVFtMXphSFY2Y1ZsdmNucHhlbGhGWVZWVlpFUlVkVXhDTW1nNWFIZ3dXRWhQUmxwUk16bHVkbTlPUjJaT2R5OTRTVmRaZEhSUGRYZHZhMncyTVZsb1JVeFZlRmQxU1ZSRmMwTlVhM2xtTVRNd09IazVSbFJzWlRKeVYyZEVlSEZNYTBSUFNXVXlPRWwzUzJSQkwySXdWVUl5VEZGbVRWcHdWemwyUTNCSkwybHlWek5uYmpaeU5WWjNWMjB2U1dweWJtNDNSelJrVmpadVYzcFRkMGhQUTJSdWEwMTRNRXQ1VVVOa0wxQjFaWEpUYjNSdVEwOXRTMDEzWlRSTGJqaERkMU5YVVRRNGRURkRNbTFpV1VzeGRYTlpOM1YzUFQwaUxDSndkV0pzYVdOTFpYa2lPaUl0TFMwdExVSkZSMGxPSUZCVlFreEpReUJMUlZrdExTMHRMVnh1VFVsSlFrbHFRVTVDWjJ0eGFHdHBSemwzTUVKQlVVVkdRVUZQUTBGUk9FRk5TVWxDUTJkTFEwRlJSVUZ6TkhKdlVIcDFhV1JNZVhOMmIxWTJkemxhTkZ4dVdHRmliME5tWTJNeGFHZFZhQ3N3V1VkS2NFNURSVXhyTjBaTFF5OTJhemR6ZERsR05tY3dUMjlrU0VSbGVYZFJXa2hLZFU1TVpsUnNRbEJHUTJOaU5seHVObTlzVEZOeWNGQTRjbFUzU0d4SGJsRkVSMFJNYVhkS1EyaGtSRGRVVUdSM2FXdHBkMHRGY201aldqaEdaalZsU25vd2RETmlUWFpyVDJaVVluSkJiRnh1WWtGQ1kwbzVNVmxVT1hKdVVXOXFkVWN4UldKUVRqaEZWblI2TWxZNE5IZHViR2Q0TUhCd2JEVjRPSFpOYlhwcE1ISnVibEZVV1VGamJ6WnFhMnBJTTF4dVRuTlVkWE4xUzFkdlJGUjVNWE5yZGtSUk9IbEJZV0ptWTNNME4zWnNRazAwU0RGT1JFNHZSSFJhWWxZdllubDJia0o2YkM4eFZrVnpURmRqWlZWcFRGeHVSWEYxT0VkeWF5dFFVRGQyUkdSd2JFUjNjWFpQV2t4RmRYazNkamhuUm01U09WUlVSV3ByTlVvNWRuWlVTR2RtU25VemVubEVPR2xLWTBSRE5YcHFPVnh1YjFGSlJFRlJRVUpjYmkwdExTMHRSVTVFSUZCVlFreEpReUJMUlZrdExTMHRMVnh1SWl3aWEyVjVVMmxuYm1GMGRYSmxJam9pWlhsS2VtRlhaSFZaV0ZJeFkyMVZhVTlwU2pCUldIQjJXVE5LVms1NmFGaFNSMlJzVVRKb2NtTklXa1ZVVlRsRldqQktXVTFGUmtaVFJFNUZVMGhLYkUxclRUTkxNSEJFVkROR2VGTnROVVJVVlRWVlltMDFiVnBGUm5sWldIQjZaRVJqTVZaSGFFeFBXRUpVVWtacmRrd3diek5aTUZaSlVteFdWRXd5T1VoV1JXeHNWa1ZPTUZSSE1WWlJNR04zVkd4R2JGa3pTblJUUm1zMFZVWk9hMVpWU2pCVU1WbDNZbXQwY0ZSclZuQmpia0poVFZjNWFtSldiSEZaYTNob1UyeHNWV0pGUmtWWGJVWnZWakZLVUZkcWJGSmhXRVp1V2xkb1EyRnVRak5TUjNNd1lWWkpOVTVXVmxkV1ZUVnlUMGhLYjFsVlRYbGhiVGcwVjBkYWVGbHFWbFppYlhoeFpFWkZkMDU1Y3pCaFZsSkpWRVpPTm1WRk1IcGxWWFJ2VFVaR1ZtRXdWVFJSVnpsSFVsaEtVRTFZUmxCU01WcFJVMVJDTmxsV2FIcFdWWEJ0WTBSU2JFMVVRazlPVjNSU1ZucFdUMU5XWTNaU1ZYUkZVMGhzYlU5VmJGaGtNMUl3WTFWc1lXTlhSakJTYTA1RVlVWmtjbUo2VmtSU00wSllUREkxUmsxWVl6SmxWM1JKVlZoQk1sVXhTbEppU0Zwd1VrVXdNRlpFVWt0VU1rWnNVVmQwYzFSV1VrMVVWV055V1RCYVRHSXpaRTlUVm05NVlraE9SR1JzVG5aUmFrWmFaVmRPVGxOVlNteGFiRXB1Wld0U2RVMHhSVGxRVTBselNXMWtjMkl5U21oaVJYUnNaVlZzYTBscWIybFpiVkpzV2xSVk1rNVVXWGRaTWxwcFRrUk9hazlYU1hsUFIwcHRUMVJvYkZsWFRtaGFiVVV5VGtSWmFXWlJQVDBpZlE9PSJ9 `, mockServer.URL) + testMultiChannelLicense := fmt.Sprintf(`apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: testcustomer +spec: + appSlug: my-app + channelID: 1vusIYZLAVxMG6q760OJmRKj5i5 + channelName: My Channel + customerName: Test Customer + endpoint: %s + channels: + - channelId: 1vusIYZLAVxMG6q760OJmRKj5i5 + channelName: My Channel + channelSlug: my-channel + isDefault: true + isSemverRequired: false + entitlements: + expires_at: + description: License Expiration + title: Expiration + value: "2030-07-27T00:00:00Z" + valueType: String + isAirgapSupported: true + isGitOpsSupported: true + isSnapshotSupported: true + licenseID: 1vusOokxAVp1tkRGuyxnF23PJcq + licenseSequence: 7 + licenseType: prod + signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pZEdWemRHTjFjM1J2YldWeUluMHNJbk53WldNaU9uc2liR2xqWlc1elpVbEVJam9pTVhaMWMwOXZhM2hCVm5BeGRHdFNSM1Y1ZUc1R01qTlFTbU54SWl3aWJHbGpaVzV6WlZSNWNHVWlPaUp3Y205a0lpd2lZM1Z6ZEc5dFpYSk9ZVzFsSWpvaVZHVnpkQ0JEZFhOMGIyMWxjaUlzSW1Gd2NGTnNkV2NpT2lKdGVTMWhjSEFpTENKamFHRnVibVZzU1VRaU9pSXhkblZ6U1ZsYVRFRldlRTFITm5FM05qQlBTbTFTUzJvMWFUVWlMQ0pqYUdGdWJtVnNUbUZ0WlNJNklrMTVJRU5vWVc1dVpXd2lMQ0pzYVdObGJuTmxVMlZ4ZFdWdVkyVWlPamNzSW1WdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5eVpYQnNhV05oZEdWa0xtRndjQ0lzSW1WdWRHbDBiR1Z0Wlc1MGN5STZleUppYjI5c1gyWnBaV3hrSWpwN0luUnBkR3hsSWpvaVFtOXZiQ0JHYVdWc1pDSXNJblpoYkhWbElqcDBjblZsTENKMllXeDFaVlI1Y0dVaU9pSkNiMjlzWldGdUluMHNJbVY0Y0dseVpYTmZZWFFpT25zaWRHbDBiR1VpT2lKRmVIQnBjbUYwYVc5dUlpd2laR1Z6WTNKcGNIUnBiMjRpT2lKTWFXTmxibk5sSUVWNGNHbHlZWFJwYjI0aUxDSjJZV3gxWlNJNklqSXdNekF0TURjdE1qZFVNREE2TURBNk1EQmFJaXdpZG1Gc2RXVlVlWEJsSWpvaVUzUnlhVzVuSW4wc0ltaHBaR1JsYmw5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtocFpHUmxiaUJHYVdWc1pDSXNJblpoYkhWbElqb2lkR2hwY3lCcGN5QnpaV055WlhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lMQ0pwYzBocFpHUmxiaUk2ZEhKMVpYMHNJbWx1ZEY5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtsdWRDQkdhV1ZzWkNJc0luWmhiSFZsSWpveE1qTXNJblpoYkhWbFZIbHdaU0k2SWtsdWRHVm5aWElpZlN3aWMzUnlhVzVuWDJacFpXeGtJanA3SW5ScGRHeGxJam9pVTNSeWFXNW5SbWxsYkdRaUxDSjJZV3gxWlNJNkluTnBibWRzWlNCc2FXNWxJSFJsZUhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lmU3dpZEdWNGRGOW1hV1ZzWkNJNmV5SjBhWFJzWlNJNklsUmxlSFFnUm1sbGJHUWlMQ0oyWVd4MVpTSTZJbTExYkhScFhHNXNhVzVsWEc1MFpYaDBJaXdpZG1Gc2RXVlVlWEJsSWpvaVZHVjRkQ0o5ZlN3aWFYTkJhWEpuWVhCVGRYQndiM0owWldRaU9uUnlkV1VzSW1selIybDBUM0J6VTNWd2NHOXlkR1ZrSWpwMGNuVmxMQ0pwYzFOdVlYQnphRzkwVTNWd2NHOXlkR1ZrSWpwMGNuVmxmWDA9IiwiaW5uZXJTaWduYXR1cmUiOiJleUpzYVdObGJuTmxVMmxuYm1GMGRYSmxJam9pYUhneE1XTXZUR1ozUTNoVE5YRmtRWEJGU1hGdVRrMU9NMHBLYTJzNFZHZFhSVVpzVDFKVlJ6UjJjR1YzZEZoV1YzbG1lamRZY0hBd1ExazJZamRyUVRSS2N6TklhR3d3YkZJMFdUQTFMemN2UVVkQ2FEZFZNSGczUkhaTVozUXpVM00wYm5GTFZTdFhXRXBTVHpKWVFVRnZSME4xZFRWR1RGcHJRVWhYY1RSUVFtMXphSFY2Y1ZsdmNucHhlbGhGWVZWVlpFUlVkVXhDTW1nNWFIZ3dXRWhQUmxwUk16bHVkbTlPUjJaT2R5OTRTVmRaZEhSUGRYZHZhMncyTVZsb1JVeFZlRmQxU1ZSRmMwTlVhM2xtTVRNd09IazVSbFJzWlRKeVYyZEVlSEZNYTBSUFNXVXlPRWwzUzJSQkwySXdWVUl5VEZGbVRWcHdWemwyUTNCSkwybHlWek5uYmpaeU5WWjNWMjB2U1dweWJtNDNSelJrVmpadVYzcFRkMGhQUTJSdWEwMTRNRXQ1VVVOa0wxQjFaWEpUYjNSdVEwOXRTMDEzWlRSTGJqaERkMU5YVVRRNGRURkRNbTFpV1VzeGRYTlpOM1YzUFQwaUxDSndkV0pzYVdOTFpYa2lPaUl0TFMwdExVSkZSMGxPSUZCVlFreEpReUJMUlZrdExTMHRMVnh1VFVsSlFrbHFRVTVDWjJ0eGFHdHBSemwzTUVKQlVVVkdRVUZQUTBGUk9FRk5TVWxDUTJkTFEwRlJSVUZ6TkhKdlVIcDFhV1JNZVhOMmIxWTJkemxhTkZ4dVdHRmliME5tWTJNeGFHZFZhQ3N3V1VkS2NFNURSVXhyTjBaTFF5OTJhemR6ZERsR05tY3dUMjlrU0VSbGVYZFJXa2hLZFU1TVpsUnNRbEJHUTJOaU5seHVObTlzVEZOeWNGQTRjbFUzU0d4SGJsRkVSMFJNYVhkS1EyaGtSRGRVVUdSM2FXdHBkMHRGY201aldqaEdaalZsU25vd2RETmlUWFpyVDJaVVluSkJiRnh1WWtGQ1kwbzVNVmxVT1hKdVVXOXFkVWN4UldKUVRqaEZWblI2TWxZNE5IZHViR2Q0TUhCd2JEVjRPSFpOYlhwcE1ISnVibEZVV1VGamJ6WnFhMnBJTTF4dVRuTlVkWE4xUzFkdlJGUjVNWE5yZGtSUk9IbEJZV0ptWTNNME4zWnNRazAwU0RGT1JFNHZSSFJhWWxZdllubDJia0o2YkM4eFZrVnpURmRqWlZWcFRGeHVSWEYxT0VkeWF5dFFVRGQyUkdSd2JFUjNjWFpQV2t4RmRYazNkamhuUm01U09WUlVSV3ByTlVvNWRuWlVTR2RtU25VemVubEVPR2xLWTBSRE5YcHFPVnh1YjFGSlJFRlJRVUpjYmkwdExTMHRSVTVFSUZCVlFreEpReUJMUlZrdExTMHRMVnh1SWl3aWEyVjVVMmxuYm1GMGRYSmxJam9pWlhsS2VtRlhaSFZaV0ZJeFkyMVZhVTlwU2pCUldIQjJXVE5LVms1NmFGaFNSMlJzVVRKb2NtTklXa1ZVVlRsRldqQktXVTFGUmtaVFJFNUZVMGhLYkUxclRUTkxNSEJFVkROR2VGTnROVVJVVlRWVlltMDFiVnBGUm5sWldIQjZaRVJqTVZaSGFFeFBXRUpVVWtacmRrd3diek5aTUZaSlVteFdWRXd5T1VoV1JXeHNWa1ZPTUZSSE1WWlJNR04zVkd4R2JGa3pTblJUUm1zMFZVWk9hMVpWU2pCVU1WbDNZbXQwY0ZSclZuQmpia0poVFZjNWFtSldiSEZaYTNob1UyeHNWV0pGUmtWWGJVWnZWakZLVUZkcWJGSmhXRVp1V2xkb1EyRnVRak5TUjNNd1lWWkpOVTVXVmxkV1ZUVnlUMGhLYjFsVlRYbGhiVGcwVjBkYWVGbHFWbFppYlhoeFpFWkZkMDU1Y3pCaFZsSkpWRVpPTm1WRk1IcGxWWFJ2VFVaR1ZtRXdWVFJSVnpsSFVsaEtVRTFZUmxCU01WcFJVMVJDTmxsV2FIcFdWWEJ0WTBSU2JFMVVRazlPVjNSU1ZucFdUMU5XWTNaU1ZYUkZVMGhzYlU5VmJGaGtNMUl3WTFWc1lXTlhSakJTYTA1RVlVWmtjbUo2VmtSU00wSllUREkxUmsxWVl6SmxWM1JKVlZoQk1sVXhTbEppU0Zwd1VrVXdNRlpFVWt0VU1rWnNVVmQwYzFSV1VrMVVWV055V1RCYVRHSXpaRTlUVm05NVlraE9SR1JzVG5aUmFrWmFaVmRPVGxOVlNteGFiRXB1Wld0U2RVMHhSVGxRVTBselNXMWtjMkl5U21oaVJYUnNaVlZzYTBscWIybFpiVkpzV2xSVk1rNVVXWGRaTWxwcFRrUk9hazlYU1hsUFIwcHRUMVJvYkZsWFRtaGFiVVV5VGtSWmFXWlJQVDBpZlE9PSJ9 +`, mockServer.URL) + onlineApp := &apptypes.App{ ID: "app-id", Slug: "app-slug", @@ -104,6 +135,15 @@ spec: License: testLicense, } + airgapAppMultiChannel := &apptypes.App{ + ID: "app-id", + Slug: "app-slug", + Name: "app-name", + IsAirgap: true, + IsGitOps: false, + License: testMultiChannelLicense, + } + type args struct { app *apptypes.App request handlers.StartUpgradeServiceRequest @@ -213,6 +253,60 @@ spec: RegistryNamespace: "namespace", RegistryIsReadOnly: false, + ReportingInfo: reporting.GetReportingInfo(airgapApp.ID), + }, + }, + { + name: "airgap with multi-channel license", + args: args{ + app: airgapAppMultiChannel, + request: handlers.StartUpgradeServiceRequest{ + VersionLabel: "1.0.0", + UpdateCursor: "1", + ChannelID: "channel-id", + }, + }, + mockStoreExpectations: func() { + mockStore.EXPECT().GetRegistryDetailsForApp(airgapAppMultiChannel.ID).Return(registrytypes.RegistrySettings{ + Hostname: "hostname", + Username: "username", + Password: "password", + Namespace: "namespace", + IsReadOnly: false, + }, nil) + mockStore.EXPECT().GetAppVersionBaseArchive(airgapAppMultiChannel.ID, "1.0.0").Return("base-archive", int64(1), nil) + mockStore.EXPECT().GetNextAppSequence(airgapAppMultiChannel.ID).Return(int64(2), nil) + }, + wantParams: &upgradeservicetypes.UpgradeServiceParams{ + Port: "", // port is random, we just check it's not empty + + AppID: airgapAppMultiChannel.ID, + AppSlug: airgapAppMultiChannel.Slug, + AppName: airgapAppMultiChannel.Name, + AppIsAirgap: airgapAppMultiChannel.IsAirgap, + AppIsGitOps: airgapAppMultiChannel.IsGitOps, + AppLicense: airgapAppMultiChannel.License, + AppArchive: "base-archive", + + Source: "Airgap Update", + BaseSequence: 1, + NextSequence: 2, + + UpdateVersionLabel: "1.0.0", + UpdateCursor: "1", + UpdateChannelID: "channel-id", + UpdateECVersion: "airgap-update-ec-version", + UpdateKOTSBin: "", // tmp file name is random, we just check it's not empty + UpdateAirgapBundle: updateAirgapBundle, + + CurrentECVersion: "current-ec-version", + + RegistryEndpoint: "hostname", + RegistryUsername: "username", + RegistryPassword: "password", + RegistryNamespace: "namespace", + RegistryIsReadOnly: false, + ReportingInfo: reporting.GetReportingInfo(airgapApp.ID), }, }, diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index f4c6c32754..cdbdc75900 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1629,7 +1629,7 @@ func FindChannelIDInLicense(requestedSlug string, license *kotsv1beta1.License) func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { if channelID == "" || len(license.Spec.Channels) == 0 { if license.Spec.ChannelID != channelID { - return nil, errors.New("channel not found non-multi channel license") + return nil, errors.New("channel not found in non-multi channel license") } // this is an install from before multi channel support, so emulate it using the top level info return &kotsv1beta1.Channel{ diff --git a/pkg/update/required.go b/pkg/update/required.go index 3ed40dca22..1b9954bf65 100644 --- a/pkg/update/required.go +++ b/pkg/update/required.go @@ -64,9 +64,14 @@ func getRequiredAirgapUpdates(airgap *kotsv1beta1.Airgap, license *kotsv1beta1.L for _, appVersion := range installedVersions { requiredSemver, requiredSemverErr := semver.ParseTolerant(requiredRelease.VersionLabel) + licenseChan, err := kotsutil.FindChannelInLicense(appVersion.ChannelID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + // semvers can be compared across channels // if a semmver is missing, fallback to comparing the cursor but only if channel is the same - if license.Spec.IsSemverRequired && appVersion.Semver != nil && requiredSemverErr == nil { + if licenseChan.IsSemverRequired && appVersion.Semver != nil && requiredSemverErr == nil { if (*appVersion.Semver).GTE(requiredSemver) { laterReleaseInstalled = true break diff --git a/pkg/update/required_test.go b/pkg/update/required_test.go index 973be2d57b..afc8b33758 100644 --- a/pkg/update/required_test.go +++ b/pkg/update/required_test.go @@ -12,6 +12,29 @@ import ( func Test_getRequiredAirgapUpdates(t *testing.T) { channelID := "channel-id" + channelName := "channel-name" + + testLicense := &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "default-channel-id", + ChannelName: "Default Channel", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "default-channel-id", + ChannelName: "Default Channel", + IsDefault: true, + IsSemverRequired: true, + }, + { + ChannelID: channelID, + ChannelName: channelName, + IsDefault: false, + IsSemverRequired: true, + }, + }, + }, + } + tests := []struct { name string airgap *kotsv1beta1.Airgap @@ -62,9 +85,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { }, }, }, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{}, - }, + license: testLicense, installedVersions: []*downstreamtypes.DownstreamVersion{ { ChannelID: channelID, @@ -96,9 +117,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { }, }, }, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{}, - }, + license: testLicense, installedVersions: []*downstreamtypes.DownstreamVersion{ { ChannelID: channelID, @@ -130,9 +149,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { }, }, }, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{}, - }, + license: testLicense, installedVersions: []*downstreamtypes.DownstreamVersion{ { ChannelID: channelID, @@ -164,8 +181,67 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { }, }, }, + license: testLicense, + channelChanged: true, + installedVersions: []*downstreamtypes.DownstreamVersion{ + { + ChannelID: "different-channel", + VersionLabel: "0.1.117", + UpdateCursor: "117", + }, + }, + wantNoSemver: []string{}, + wantSemver: []string{}, + }, + { + name: "check across multiple channels with multi chan license", + airgap: &kotsv1beta1.Airgap{ + Spec: kotsv1beta1.AirgapSpec{ + ChannelID: channelID, + RequiredReleases: []kotsv1beta1.AirgapReleaseMeta{ + { + VersionLabel: "0.1.123", + UpdateCursor: "123", + }, + { + VersionLabel: "0.1.120", + UpdateCursor: "120", + }, + { + VersionLabel: "0.1.115", + UpdateCursor: "115", + }, + }, + }, + }, license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{}, + Spec: kotsv1beta1.LicenseSpec{ + ChannelID: "stable-channel", // intentionally fully avoiding the default channel + ChannelName: "Stable Channel", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "stable-channel", + ChannelName: "Stable Channel", + ChannelSlug: "stable-channel", + IsDefault: true, + IsSemverRequired: true, + }, + { + ChannelID: "different-channel", + ChannelName: "Different Channel", + ChannelSlug: "different-channel", + IsDefault: true, + IsSemverRequired: false, + }, + { + ChannelID: channelID, + ChannelName: channelName, + ChannelSlug: channelID, + IsDefault: false, + IsSemverRequired: true, + }, + }, + }, }, channelChanged: true, installedVersions: []*downstreamtypes.DownstreamVersion{ From f0c5f747479e9ba9bdaea646582528b2194b5cc4 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 23 Jul 2024 20:17:26 +0000 Subject: [PATCH 14/35] Wordsmith error message for product --- pkg/handlers/license.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index e13094cc84..35fb542a3b 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -338,7 +338,7 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { matchedChannelID, err := kotsutil.FindChannelIDInLicense(installationParams.RequestedChannelSlug, verifiedLicense) if err != nil { logger.Error(err) - uploadLicenseResponse.Error = "Requested install channel not found in license" + uploadLicenseResponse.Error = "Your current license does not grant access to the channel you requested. Please generate a support bundle and contact support for assistance." JSON(w, http.StatusBadRequest, uploadLicenseResponse) return } From 7ac8b948a900d7fb25dbbec9c3251c3c8dac35bc Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 24 Jul 2024 15:36:56 +0000 Subject: [PATCH 15/35] fix - kots pull/install only prevalidate multichan licenses fix license check for multichannel installs check --- cmd/kots/cli/install.go | 11 +++++++++-- cmd/kots/cli/pull.go | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 3c1cfc451d..763f3044dd 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -169,7 +169,7 @@ func InstallCmd() *cobra.Command { // fetch the latest version of the license to verify channel access if online install if license != nil { - if isAirgap && !slugInLicenseChannels(preferredChannelSlug, license) { + if isAirgap && haveMultiChannelLicense(license) && !slugInLicenseChannels(preferredChannelSlug, license) { return errors.New("requested channel not found in supplied license") } log.ActionWithSpinner("Checking for license update") @@ -179,7 +179,7 @@ func InstallCmd() *cobra.Command { return errors.Wrap(err, "failed to get latest license") } log.FinishSpinner() - if !slugInLicenseChannels(preferredChannelSlug, updatedLicense.License) { + if haveMultiChannelLicense(updatedLicense.License) && !slugInLicenseChannels(preferredChannelSlug, updatedLicense.License) { return errors.New("requested channel not found in latest license") } license = updatedLicense.License @@ -1108,3 +1108,10 @@ func slugInLicenseChannels(slug string, license *kotsv1beta1.License) bool { } return false } + +func haveMultiChannelLicense(license *kotsv1beta1.License) bool { + if license == nil { + return false + } + return len(license.Spec.Channels) > 0 +} diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index 68bda2ac97..bbfffc0988 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -106,7 +106,7 @@ func PullCmd() *cobra.Command { // If we are passed a multi-channel license, verify that the requested channel is in the license // so that we can warn the user immediately if it is not. - if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) { + if haveMultiChannelLicense(license) && !slugInLicenseChannels(preferredChannelSlug, license) { return errors.New("requested channel not found in license") } From 8d78738ca066fbb72110adaae41df1edb56033e9 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 25 Jul 2024 10:24:42 -0500 Subject: [PATCH 16/35] Update cmd/kots/cli/install.go Co-authored-by: Salah Al Saleh --- cmd/kots/cli/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 763f3044dd..1aa2dbd197 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -1109,7 +1109,7 @@ func slugInLicenseChannels(slug string, license *kotsv1beta1.License) bool { return false } -func haveMultiChannelLicense(license *kotsv1beta1.License) bool { +func isMultiChannelLicense(license *kotsv1beta1.License) bool { if license == nil { return false } From 8fb86a2db6a4b3d7d3585fe1d8133ca4f33ced08 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 25 Jul 2024 20:25:35 +0000 Subject: [PATCH 17/35] pr feedback Add debug log line --- cmd/kots/cli/install.go | 37 +++----------------- cmd/kots/cli/pull.go | 11 +++--- pkg/handlers/upgrade_service.go | 10 ------ pkg/multichannel/multichannel.go | 60 ++++++++++++++++++++++++++++++++ pkg/operator/operator.go | 4 +++ pkg/store/kotsstore/app_store.go | 2 ++ 6 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 pkg/multichannel/multichannel.go diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index 1aa2dbd197..e129c7d28a 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -33,6 +33,7 @@ import ( "github.com/replicatedhq/kots/pkg/kurl" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/metrics" + "github.com/replicatedhq/kots/pkg/multichannel" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" "github.com/replicatedhq/kots/pkg/print" "github.com/replicatedhq/kots/pkg/pull" @@ -166,23 +167,9 @@ func InstallCmd() *cobra.Command { if err != nil { return errors.Wrap(err, "failed to extract preferred channel slug") } - - // fetch the latest version of the license to verify channel access if online install - if license != nil { - if isAirgap && haveMultiChannelLicense(license) && !slugInLicenseChannels(preferredChannelSlug, license) { - return errors.New("requested channel not found in supplied license") - } - log.ActionWithSpinner("Checking for license update") - updatedLicense, err := replicatedapp.GetLatestLicense(license) - if err != nil { - log.FinishSpinnerWithError() - return errors.Wrap(err, "failed to get latest license") - } - log.FinishSpinner() - if haveMultiChannelLicense(updatedLicense.License) && !slugInLicenseChannels(preferredChannelSlug, updatedLicense.License) { - return errors.New("requested channel not found in latest license") - } - license = updatedLicense.License + license, err = multichannel.VerifyAndUpdateLicense(log, license, preferredChannelSlug, isAirgap) + if err != nil { + return errors.Wrap(err, "failed to verify and update license") } namespace := v.GetString("namespace") @@ -1099,19 +1086,3 @@ func checkPreflightResults(response *handlers.GetPreflightResultResponse, skipPr return true, nil } - -func slugInLicenseChannels(slug string, license *kotsv1beta1.License) bool { - for _, channel := range license.Spec.Channels { - if channel.ChannelSlug == slug { - return true - } - } - return false -} - -func isMultiChannelLicense(license *kotsv1beta1.License) bool { - if license == nil { - return false - } - return len(license.Spec.Channels) > 0 -} diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index bbfffc0988..63940f758e 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/multichannel" "github.com/replicatedhq/kots/pkg/pull" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -104,10 +105,14 @@ func PullCmd() *cobra.Command { return errors.Wrap(err, "failed to extract preferred channel slug") } + log := logger.NewCLILogger(cmd.OutOrStdout()) + log.Initialize() + // If we are passed a multi-channel license, verify that the requested channel is in the license // so that we can warn the user immediately if it is not. - if haveMultiChannelLicense(license) && !slugInLicenseChannels(preferredChannelSlug, license) { - return errors.New("requested channel not found in license") + license, err = multichannel.VerifyAndUpdateLicense(log, license, preferredChannelSlug, false) + if err != nil { + return errors.Wrap(err, "failed to verify and update license") } renderDir, err := pull.Pull(upstream, pullOptions) @@ -115,8 +120,6 @@ func PullCmd() *cobra.Command { return errors.Wrap(err, "failed to pull") } - log := logger.NewCLILogger(cmd.OutOrStdout()) - log.Initialize() log.Info("Kubernetes application files created in %s", renderDir) if len(v.GetStringSlice("downstream")) == 0 { log.Info("To deploy, run kubectl apply -k %s", path.Join(renderDir, "overlays", "midstream")) diff --git a/pkg/handlers/upgrade_service.go b/pkg/handlers/upgrade_service.go index 9999d5bb93..6776d83e9d 100644 --- a/pkg/handlers/upgrade_service.go +++ b/pkg/handlers/upgrade_service.go @@ -75,16 +75,6 @@ func (h *Handler) StartUpgradeService(w http.ResponseWriter, r *http.Request) { return } - if foundApp.IsAirgap && foundApp.ChannelID != request.ChannelID { - // The update is deployable, so we should update the apps stored channelID similar to install - // time when channelID is set via the requested-channel-id in the configmap. - if err := store.GetStore().SetAppChannelID(foundApp.ID, request.ChannelID); err != nil { - response.Error = "failed update requested channel ID" - logger.Error(errors.Wrap(err, response.Error)) - JSON(w, http.StatusInternalServerError, response) - } - } - if err := startUpgradeService(foundApp, request); err != nil { response.Error = "failed to start upgrade service" logger.Error(errors.Wrap(err, response.Error)) diff --git a/pkg/multichannel/multichannel.go b/pkg/multichannel/multichannel.go new file mode 100644 index 0000000000..fbdbd419f3 --- /dev/null +++ b/pkg/multichannel/multichannel.go @@ -0,0 +1,60 @@ +package multichannel + +import ( + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/replicatedapp" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" +) + +func isSlugInLicenseChannels(slug string, license *kotsv1beta1.License) bool { + for _, channel := range license.Spec.Channels { + if channel.ChannelSlug == slug { + return true + } + } + return false +} + +func isMultiChannelLicense(license *kotsv1beta1.License) bool { + if license == nil { + return false + } + // whether a license is multi-channel is determined by the presence of channels in the license + // if there are no channels, it is not multi-channel - and was generated before channels + // were introduced. + return len(license.Spec.Channels) > 0 +} + +func canInstallFromChannel(slug string, license *kotsv1beta1.License) bool { + if !isMultiChannelLicense(license) { + return true + } + return isSlugInLicenseChannels(slug, license) +} + +// VerifyAndUpdateLicense will update (if not airgapped), verify that the request channel slug is present, and return the possibly updated license. +// Note that this is a noop if the license passed in is nil. +func VerifyAndUpdateLicense(log *logger.CLILogger, license *kotsv1beta1.License, preferredChannelSlug string, isAirgap bool) (*kotsv1beta1.License, error) { + if license == nil { + return nil, nil + } + if isAirgap { + if isMultiChannelLicense(license) && !isSlugInLicenseChannels(preferredChannelSlug, license) { + return nil, errors.New("requested channel not found in supplied license") + } + return license, nil + } + log.ActionWithSpinner("Checking for license update") + updatedLicense, err := replicatedapp.GetLatestLicense(license) + if err != nil { + log.FinishSpinnerWithError() + return nil, errors.Wrap(err, "failed to get latest license") + } + log.FinishSpinner() + if canInstallFromChannel(preferredChannelSlug, updatedLicense.License) { + return updatedLicense.License, nil + } + return nil, errors.New("requested channel not found in latest license") + +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index de011fdaa2..921d18ce49 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -1022,6 +1022,10 @@ func (o *Operator) reconcileDeployment(cm *corev1.ConfigMap) (finalError error) } } + if err := store.GetStore().SetAppChannelID(appID, cm.Data["channel-id"]); err != nil { + return errors.Wrap(err, "failed to set app channel id") + } + if err := o.store.SetAppChannelChanged(appID, false); err != nil { return errors.Wrap(err, "failed to reset channel changed flag") } diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index 927793e54d..25e2f4f723 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -622,6 +622,8 @@ func (s *KOTSStore) GetAppChannelID(appID string) (string, error) { } func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { + logger.Debug("setting app channel id", + zap.String("appID", appID), zap.String("channelID", channelID)) db := persistence.MustGetDBSession() query := `update app set channel_id = ? where id = ?` From a5e4bd9f79e9611357969efe0ba0be7da5f65581 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 26 Jul 2024 22:49:11 +0000 Subject: [PATCH 18/35] Actually use/set AppChannelID's in Upstream/FetchOptions --- cmd/kots/cli/pull.go | 5 +++++ pkg/airgap/airgap.go | 1 + pkg/airgap/types/types.go | 1 + pkg/automation/automation.go | 1 + pkg/handlers/license.go | 13 +++++++++-- pkg/multichannel/multichannel.go | 1 - pkg/online/online.go | 1 + pkg/online/types/types.go | 1 + pkg/pull/pull.go | 2 ++ pkg/registry/registry.go | 1 + pkg/render/render.go | 2 +- pkg/rewrite/rewrite.go | 2 ++ pkg/store/kotsstore/airgap_store.go | 4 ++-- pkg/store/kotsstore/version_store.go | 1 - pkg/tests/pull/cases/airgap/testcase.yaml | 3 ++- .../pull/cases/configcontext/testcase.yaml | 3 ++- .../pull/cases/customhostnames/testcase.yaml | 3 ++- pkg/tests/pull/cases/kotskinds/testcase.yaml | 3 ++- pkg/tests/pull/cases/multidoc/testcase.yaml | 3 ++- .../pull/cases/needsconfig/testcase.yaml | 3 ++- .../pull/cases/replicatedhelm/testcase.yaml | 3 ++- .../cases/required-helm-values/testcase.yaml | 3 ++- .../cases/samechartvariations/testcase.yaml | 3 ++- pkg/tests/pull/cases/simple/testcase.yaml | 3 ++- .../pull/cases/subchart-alias/testcase.yaml | 3 ++- .../pull/cases/subchart-crds/testcase.yaml | 3 ++- pkg/tests/pull/cases/subcharts/testcase.yaml | 3 ++- .../taganddigest-norewrite/testcase.yaml | 3 ++- .../cases/taganddigest-rewrite/testcase.yaml | 3 ++- .../pull/cases/v1beta2-charts/testcase.yaml | 3 ++- .../cases/outdated-kotskinds/testcase.yaml | 1 + pkg/tests/renderdir/renderdir_test.go | 2 +- pkg/upstream/fetch.go | 1 + pkg/upstream/fetch_test.go | 22 +++++++++++++++++++ pkg/upstream/replicated.go | 9 ++++++-- pkg/upstream/types/types.go | 1 + 36 files changed, 94 insertions(+), 26 deletions(-) diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index 63940f758e..fadd36e0ee 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/multichannel" "github.com/replicatedhq/kots/pkg/pull" @@ -114,6 +115,10 @@ func PullCmd() *cobra.Command { if err != nil { return errors.Wrap(err, "failed to verify and update license") } + pullOptions.AppChannelID, err = kotsutil.FindChannelIDInLicense(preferredChannelSlug, license) + if err != nil { // should never happen since we just verified the channel + return errors.Wrap(err, "failed to find channel ID in license") + } renderDir, err := pull.Pull(upstream, pullOptions) if err != nil { diff --git a/pkg/airgap/airgap.go b/pkg/airgap/airgap.go index d0aa6f9135..a8d60be407 100644 --- a/pkg/airgap/airgap.go +++ b/pkg/airgap/airgap.go @@ -229,6 +229,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: instParams.AppVersionLabel, + AppChannelID: opts.PendingApp.ChannelID, SkipCompatibilityCheck: opts.SkipCompatibilityCheck, } diff --git a/pkg/airgap/types/types.go b/pkg/airgap/types/types.go index b6b8234808..3e15e9be16 100644 --- a/pkg/airgap/types/types.go +++ b/pkg/airgap/types/types.go @@ -5,6 +5,7 @@ type PendingApp struct { Slug string Name string LicenseData string + ChannelID string } type InstallStatus struct { diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 7ed76d7f53..0a9bb9baff 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -321,6 +321,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. Name: a.Name, LicenseData: string(license), VersionLabel: instParams.AppVersionLabel, + ChannelID: a.ChannelID, }, UpstreamURI: upstreamURI, IsAutomated: true, diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index 35fb542a3b..5e009b01f3 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -358,11 +358,13 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { ID: a.ID, Slug: a.Slug, Name: a.Name, + ChannelID: a.ChannelID, LicenseData: uploadLicenseRequest.LicenseData, VersionLabel: installationParams.AppVersionLabel, }, UpstreamURI: upstreamURI, } + kotsKinds, err := online.CreateAppFromOnline(createAppOpts) if err != nil { logger.Error(err) @@ -440,6 +442,7 @@ func (h *Handler) ResumeInstallOnline(w http.ResponseWriter, r *http.Request) { Slug: a.Slug, Name: a.Name, VersionLabel: installationParams.AppVersionLabel, + ChannelID: a.ChannelID, } // the license data is left in the table @@ -466,6 +469,7 @@ func (h *Handler) ResumeInstallOnline(w http.ResponseWriter, r *http.Request) { PendingApp: &pendingApp, UpstreamURI: fmt.Sprintf("replicated://%s", kotsLicense.Spec.AppSlug), } + kotsKinds, err := online.CreateAppFromOnline(createAppOpts) if err != nil { logger.Error(err) @@ -676,10 +680,15 @@ func licenseResponseFromLicense(license *kotsv1beta1.License, app *apptypes.App) return entitlements[i].Title < entitlements[j].Title }) + channel, err := kotsutil.FindChannelInLicense(app.ChannelID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + response := LicenseResponse{ ID: license.Spec.LicenseID, Assignee: license.Spec.CustomerName, - ChannelName: license.Spec.ChannelName, + ChannelName: channel.ChannelName, LicenseSequence: license.Spec.LicenseSequence, LicenseType: license.Spec.LicenseType, Entitlements: entitlements, @@ -688,7 +697,7 @@ func licenseResponseFromLicense(license *kotsv1beta1.License, app *apptypes.App) IsGitOpsSupported: license.Spec.IsGitOpsSupported, IsIdentityServiceSupported: license.Spec.IsIdentityServiceSupported, IsGeoaxisSupported: license.Spec.IsGeoaxisSupported, - IsSemverRequired: license.Spec.IsSemverRequired, + IsSemverRequired: channel.IsSemverRequired, IsSnapshotSupported: license.Spec.IsSnapshotSupported, IsDisasterRecoverySupported: license.Spec.IsDisasterRecoverySupported, LastSyncedAt: app.LastLicenseSync, diff --git a/pkg/multichannel/multichannel.go b/pkg/multichannel/multichannel.go index fbdbd419f3..7a972ff607 100644 --- a/pkg/multichannel/multichannel.go +++ b/pkg/multichannel/multichannel.go @@ -56,5 +56,4 @@ func VerifyAndUpdateLicense(log *logger.CLILogger, license *kotsv1beta1.License, return updatedLicense.License, nil } return nil, errors.New("requested channel not found in latest license") - } diff --git a/pkg/online/online.go b/pkg/online/online.go index 8ba1ae7ba4..3d6751526e 100644 --- a/pkg/online/online.go +++ b/pkg/online/online.go @@ -155,6 +155,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: opts.PendingApp.VersionLabel, + AppChannelID: opts.PendingApp.ChannelID, ReportingInfo: reporting.GetReportingInfo(opts.PendingApp.ID), SkipCompatibilityCheck: opts.SkipCompatibilityCheck, } diff --git a/pkg/online/types/types.go b/pkg/online/types/types.go index 992a0cab12..cc812568b0 100644 --- a/pkg/online/types/types.go +++ b/pkg/online/types/types.go @@ -6,6 +6,7 @@ type PendingApp struct { Name string LicenseData string VersionLabel string + ChannelID string } type InstallStatus struct { diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 369b188f18..41b02ec125 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -68,6 +68,7 @@ type PullOptions struct { AppSlug string AppSequence int64 AppVersionLabel string + AppChannelID string IsGitOps bool StorageClassName string HTTPProxyEnvValue string @@ -131,6 +132,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { AppSlug: pullOptions.AppSlug, AppSequence: pullOptions.AppSequence, AppVersionLabel: pullOptions.AppVersionLabel, + AppChannelID: pullOptions.AppChannelID, LocalRegistry: pullOptions.RewriteImageOptions, ReportingInfo: pullOptions.ReportingInfo, SkipCompatibilityCheck: pullOptions.SkipCompatibilityCheck, diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index c7f28c14b6..c15143abc2 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -147,6 +147,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin }, AppID: a.ID, AppSlug: a.Slug, + AppChannelID: a.ChannelID, IsGitOps: a.IsGitOps, AppSequence: nextAppSequence, ReportingInfo: reporting.GetReportingInfo(a.ID), diff --git a/pkg/render/render.go b/pkg/render/render.go index a5bb8cc3d4..cc0c4f07de 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -125,7 +125,6 @@ func RenderDir(opts types.RenderDirOptions) error { if os.Getenv("KOTSADM_TARGET_NAMESPACE") != "" { appNamespace = os.Getenv("KOTSADM_TARGET_NAMESPACE") } - reOptions := rewrite.RewriteOptions{ RootDir: opts.ArchiveDir, UpstreamURI: fmt.Sprintf("replicated://%s", license.Spec.AppSlug), @@ -142,6 +141,7 @@ func RenderDir(opts types.RenderDirOptions) error { IsAirgap: opts.App.IsAirgap, AppID: opts.App.ID, AppSlug: opts.App.Slug, + AppChannelID: opts.App.ChannelID, IsGitOps: opts.App.IsGitOps, AppSequence: opts.Sequence, ReportingInfo: opts.ReportingInfo, diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index edfaceeb27..b87cac98a2 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -46,6 +46,7 @@ type RewriteOptions struct { RegistrySettings registrytypes.RegistrySettings AppID string AppSlug string + AppChannelID string IsGitOps bool AppSequence int64 ReportingInfo *reportingtypes.ReportingInfo @@ -81,6 +82,7 @@ func Rewrite(rewriteOptions RewriteOptions) error { License: rewriteOptions.License, AppSequence: rewriteOptions.AppSequence, AppSlug: rewriteOptions.AppSlug, + AppChannelID: rewriteOptions.AppChannelID, LocalRegistry: rewriteOptions.RegistrySettings, ReportingInfo: rewriteOptions.ReportingInfo, SkipCompatibilityCheck: true, // we're rewriting an existing version, no need to check for compatibility diff --git a/pkg/store/kotsstore/airgap_store.go b/pkg/store/kotsstore/airgap_store.go index 3e6a013c16..8571759b2e 100644 --- a/pkg/store/kotsstore/airgap_store.go +++ b/pkg/store/kotsstore/airgap_store.go @@ -28,7 +28,7 @@ func (s *KOTSStore) GetPendingAirgapUploadApp() (*airgaptypes.PendingApp, error) return nil, errors.Wrap(err, "failed to scan pending app id") } - query = `select id, slug, name, license from app where id = ?` + query = `select id, slug, name, license, channel_id from app where id = ?` rows, err = db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{id}, @@ -41,7 +41,7 @@ func (s *KOTSStore) GetPendingAirgapUploadApp() (*airgaptypes.PendingApp, error) } pendingApp := airgaptypes.PendingApp{} - if err := rows.Scan(&pendingApp.ID, &pendingApp.Slug, &pendingApp.Name, &pendingApp.LicenseData); err != nil { + if err := rows.Scan(&pendingApp.ID, &pendingApp.Slug, &pendingApp.Name, &pendingApp.LicenseData, &pendingApp.ChannelID); err != nil { return nil, errors.Wrap(err, "failed to scan pending app") } diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index bed33b332d..ab475be5f6 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -452,7 +452,6 @@ func (s *KOTSStore) UpdateAppVersion(appID string, sequence int64, baseSequence func (s *KOTSStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir string, source string, skipPreflights bool, renderer rendertypes.Renderer) (int64, error) { db := persistence.MustGetDBSession() - appVersionStatements, newSequence, err := s.createAppVersionStatements(appID, baseSequence, filesInDir, source, skipPreflights, renderer) if err != nil { return 0, errors.Wrap(err, "failed to construct app version statements") diff --git a/pkg/tests/pull/cases/airgap/testcase.yaml b/pkg/tests/pull/cases/airgap/testcase.yaml index 406773976c..e692e0491a 100644 --- a/pkg/tests/pull/cases/airgap/testcase.yaml +++ b/pkg/tests/pull/cases/airgap/testcase.yaml @@ -15,4 +15,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/configcontext/testcase.yaml b/pkg/tests/pull/cases/configcontext/testcase.yaml index a2dc01e2cb..3028be9469 100644 --- a/pkg/tests/pull/cases/configcontext/testcase.yaml +++ b/pkg/tests/pull/cases/configcontext/testcase.yaml @@ -13,4 +13,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/customhostnames/testcase.yaml b/pkg/tests/pull/cases/customhostnames/testcase.yaml index 05385c5044..a674006680 100644 --- a/pkg/tests/pull/cases/customhostnames/testcase.yaml +++ b/pkg/tests/pull/cases/customhostnames/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/kotskinds/testcase.yaml b/pkg/tests/pull/cases/kotskinds/testcase.yaml index 080188080e..74ac8eb6d2 100644 --- a/pkg/tests/pull/cases/kotskinds/testcase.yaml +++ b/pkg/tests/pull/cases/kotskinds/testcase.yaml @@ -9,4 +9,5 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - SkipHelmChartCheck: true \ No newline at end of file + SkipHelmChartCheck: true + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/multidoc/testcase.yaml b/pkg/tests/pull/cases/multidoc/testcase.yaml index fae3553d4c..e079057b94 100644 --- a/pkg/tests/pull/cases/multidoc/testcase.yaml +++ b/pkg/tests/pull/cases/multidoc/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/needsconfig/testcase.yaml b/pkg/tests/pull/cases/needsconfig/testcase.yaml index 82693ef8fe..14c185060f 100644 --- a/pkg/tests/pull/cases/needsconfig/testcase.yaml +++ b/pkg/tests/pull/cases/needsconfig/testcase.yaml @@ -9,4 +9,5 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - SkipHelmChartCheck: true \ No newline at end of file + SkipHelmChartCheck: true + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/replicatedhelm/testcase.yaml b/pkg/tests/pull/cases/replicatedhelm/testcase.yaml index 8ba924c060..9847d0bf3c 100644 --- a/pkg/tests/pull/cases/replicatedhelm/testcase.yaml +++ b/pkg/tests/pull/cases/replicatedhelm/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/required-helm-values/testcase.yaml b/pkg/tests/pull/cases/required-helm-values/testcase.yaml index 32e8a5ee3e..68f359fade 100644 --- a/pkg/tests/pull/cases/required-helm-values/testcase.yaml +++ b/pkg/tests/pull/cases/required-helm-values/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/samechartvariations/testcase.yaml b/pkg/tests/pull/cases/samechartvariations/testcase.yaml index 0bce10bb5a..5112be1431 100644 --- a/pkg/tests/pull/cases/samechartvariations/testcase.yaml +++ b/pkg/tests/pull/cases/samechartvariations/testcase.yaml @@ -15,4 +15,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1YHCrcZzBxY2nJF5kcTCN9PHpk0 \ No newline at end of file diff --git a/pkg/tests/pull/cases/simple/testcase.yaml b/pkg/tests/pull/cases/simple/testcase.yaml index ad55b19a33..16134bc0ed 100644 --- a/pkg/tests/pull/cases/simple/testcase.yaml +++ b/pkg/tests/pull/cases/simple/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subchart-alias/testcase.yaml b/pkg/tests/pull/cases/subchart-alias/testcase.yaml index 8e34a604d2..41817e2614 100644 --- a/pkg/tests/pull/cases/subchart-alias/testcase.yaml +++ b/pkg/tests/pull/cases/subchart-alias/testcase.yaml @@ -13,4 +13,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subchart-crds/testcase.yaml b/pkg/tests/pull/cases/subchart-crds/testcase.yaml index 348f723ac5..cf73ee0ed0 100644 --- a/pkg/tests/pull/cases/subchart-crds/testcase.yaml +++ b/pkg/tests/pull/cases/subchart-crds/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subcharts/testcase.yaml b/pkg/tests/pull/cases/subcharts/testcase.yaml index 34e8fb76ed..f3985532ae 100644 --- a/pkg/tests/pull/cases/subcharts/testcase.yaml +++ b/pkg/tests/pull/cases/subcharts/testcase.yaml @@ -13,4 +13,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml b/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml index 96cad188f6..fcdb47d101 100644 --- a/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml +++ b/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml @@ -8,4 +8,5 @@ PullOptions: SharedPassword: dummy-pass RewriteImages: false Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml b/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml index ca965930fa..aa9968cf32 100644 --- a/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml +++ b/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml @@ -14,4 +14,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml b/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml index d0bc3fb357..8a5b7e1213 100644 --- a/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml +++ b/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml @@ -14,4 +14,5 @@ PullOptions: Password: fake-pass IsReadOnly: true Downstreams: - - this-cluster \ No newline at end of file + - this-cluster + AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml b/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml index 3d61cfa465..f5ae296ebf 100644 --- a/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml +++ b/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml @@ -4,6 +4,7 @@ RenderDirOptions: App: ID: app-id Slug: my-app + ChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 Downstreams: - Name: this-cluster Sequence: 1 \ No newline at end of file diff --git a/pkg/tests/renderdir/renderdir_test.go b/pkg/tests/renderdir/renderdir_test.go index 3eb9d5ccb1..2d47c3ed6a 100644 --- a/pkg/tests/renderdir/renderdir_test.go +++ b/pkg/tests/renderdir/renderdir_test.go @@ -73,7 +73,7 @@ func TestKotsRenderDir(t *testing.T) { Name: spec.Name, RenderDirOptions: spec.RenderDirOptions, } - + test.RenderDirOptions.App.ChannelID = "1vusIYZLAVxMG6q760OJmRKj5i5" tests = append(tests, test) } require.NoError(t, err) diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go index 7e634294c6..09d96981ac 100644 --- a/pkg/upstream/fetch.go +++ b/pkg/upstream/fetch.go @@ -60,6 +60,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*ty fetchOptions.LocalRegistry, fetchOptions.ReportingInfo, fetchOptions.SkipCompatibilityCheck, + fetchOptions.AppChannelID, ) } diff --git a/pkg/upstream/fetch_test.go b/pkg/upstream/fetch_test.go index bda113b936..a958e1e65a 100644 --- a/pkg/upstream/fetch_test.go +++ b/pkg/upstream/fetch_test.go @@ -37,16 +37,22 @@ ACgAAA==`, airgapVersionLabel string currentVersionLabel string expectedLabel string + expectedChannelID string + expectedChannelName string }{ { airgapVersionLabel: "10.9.8", currentVersionLabel: "1.2.0", expectedLabel: "10.9.8", + expectedChannelID: "channel-2", + expectedChannelName: "ChannelTwo", }, { airgapVersionLabel: "", currentVersionLabel: "1.2.0", expectedLabel: "1.2.0", + expectedChannelID: "channel-2", + expectedChannelName: "ChannelTwo", }, } @@ -59,6 +65,19 @@ ACgAAA==`, Spec: kotsv1beta1.LicenseSpec{ Endpoint: "http://localhost", AppSlug: "app-slug", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "channel-1", + ChannelName: "ChannelOne", + ChannelSlug: "channel-one", + IsDefault: true, + }, + { + ChannelID: "channel-2", + ChannelName: "ChannelTwo", + ChannelSlug: "channel-two", + }, + }, }, }, Airgap: &kotsv1beta1.Airgap{ @@ -72,9 +91,12 @@ ACgAAA==`, }, }, }, + AppChannelID: "channel-2", } u, err := FetchUpstream("replicated://app-slug", fetchOptions) req.NoError(err) assert.Equal(t, test.expectedLabel, u.VersionLabel) + assert.Equal(t, test.expectedChannelID, u.ChannelID) + assert.Equal(t, test.expectedChannelName, u.ChannelName) } } diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 8569db4841..91dfd2be54 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -131,6 +131,7 @@ func downloadReplicated( registry registrytypes.RegistrySettings, reportingInfo *reportingtypes.ReportingInfo, skipCompatibilityCheck bool, + appChannelID string, ) (*types.Upstream, error) { var release *Release @@ -204,8 +205,12 @@ func downloadReplicated( // get channel name from license, if one was provided channelID, channelName := "", "" if license != nil { - channelID = license.Spec.ChannelID - channelName = license.Spec.ChannelName + channel, err := kotsutil.FindChannelInLicense(appChannelID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + channelID = channel.ChannelID + channelName = channel.ChannelName } if existingIdentityConfig == nil { diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go index 126792a551..8a647b82e1 100644 --- a/pkg/upstream/types/types.go +++ b/pkg/upstream/types/types.go @@ -115,6 +115,7 @@ type FetchOptions struct { LocalRegistry registrytypes.RegistrySettings ReportingInfo *reportingtypes.ReportingInfo SkipCompatibilityCheck bool + AppChannelID string } func (u *Upstream) GetUpstreamDir(options WriteOptions) string { From 5ca3f8f4038dae359aa194d7f5e77c9dd3972bbb Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Mon, 29 Jul 2024 10:21:50 -0500 Subject: [PATCH 19/35] Update pkg/multichannel/multichannel.go Co-authored-by: Salah Al Saleh --- pkg/multichannel/multichannel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/multichannel/multichannel.go b/pkg/multichannel/multichannel.go index 7a972ff607..d2f8ce997f 100644 --- a/pkg/multichannel/multichannel.go +++ b/pkg/multichannel/multichannel.go @@ -40,7 +40,7 @@ func VerifyAndUpdateLicense(log *logger.CLILogger, license *kotsv1beta1.License, return nil, nil } if isAirgap { - if isMultiChannelLicense(license) && !isSlugInLicenseChannels(preferredChannelSlug, license) { + if !canInstallFromChannel(preferredChannelSlug, license) { return nil, errors.New("requested channel not found in supplied license") } return license, nil From 4ac83cbd87ac3773b221cb9dec4cd8d4a3ff8680 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Mon, 29 Jul 2024 19:08:38 +0000 Subject: [PATCH 20/35] Consolidate backfilling to GetOrBackfillLicenseChannel --- pkg/handlers/update_checker_spec.go | 20 ++++++++--------- pkg/store/kotsstore/app_store.go | 26 +++++++++++++++++++++ pkg/store/kotsstore/downstream_store.go | 28 ++++++----------------- pkg/store/kotsstore/version_store.go | 14 +++--------- pkg/store/mock/mock.go | 30 +++++++++++++++++++++++++ pkg/store/store_interface.go | 1 + pkg/update/update.go | 10 ++++----- pkg/update/update_test.go | 30 ++++++++++++++++++++++--- pkg/updatechecker/updatechecker.go | 15 +++++++------ 9 files changed, 116 insertions(+), 58 deletions(-) diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index b96e62df20..8b7bdcf926 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -59,21 +59,21 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque var licenseChan *kotsv1beta1.Channel if foundApp.ChannelID == "" { - backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) - if err := store.GetStore().SetAppChannelID(foundApp.ID, backfillID); err != nil { + licenseChan, err = store.GetStore().BackfillChannelIDFromLicense(foundApp.ID, license) + if err != nil { updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) return } - foundApp.ChannelID = backfillID - } - - if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { - updateCheckerSpecResponse.Error = "failed to find channel in license" - logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) - JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) - return + foundApp.ChannelID = licenseChan.ChannelID + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { + updateCheckerSpecResponse.Error = "failed to find channel in license" + logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) + JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) + return + } } // Check if the deploy update configuration is valid based on app channel diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index 25e2f4f723..579a3d9e78 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -13,6 +13,7 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/persistence" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" "github.com/rqlite/gorqlite" "github.com/segmentio/ksuid" @@ -637,3 +638,28 @@ func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { return nil } + +func (s *KOTSStore) BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { + backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) + if err := s.SetAppChannelID(appID, backfillID); err != nil { + return nil, errors.Wrap(err, "failed to backfill app channel id from license") + } + return kotsutil.FindChannelInLicense(backfillID, license) +} + +func (s *KOTSStore) GetOrBackfillLicenseChannel(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { + foundChannelID, err := s.GetAppChannelID(appID) + if err != nil { + return nil, errors.Wrap(err, "failed to get app channel id") + } + + if foundChannelID == "" { + return s.BackfillChannelIDFromLicense(appID, license) + } + + licenseChan, err := kotsutil.FindChannelInLicense(foundChannelID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") + } + return licenseChan, nil +} diff --git a/pkg/store/kotsstore/downstream_store.go b/pkg/store/kotsstore/downstream_store.go index f3650c6c6d..1b6f298a8e 100644 --- a/pkg/store/kotsstore/downstream_store.go +++ b/pkg/store/kotsstore/downstream_store.go @@ -410,16 +410,9 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo return nil, errors.Wrap(err, "failed to get app license") } - foundChannelID, err := s.GetAppChannelID(appID) - var licenseChan *kotsv1beta1.Channel - if foundChannelID == "" { - foundChannelID = kotsutil.GetBackfillChannelIDFromLicense(license) - if err := s.SetAppChannelID(appID, foundChannelID); err != nil { - return nil, errors.Wrap(err, "failed to backfill app channel id from license") - } - } - if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to get or backfill channel") } downstreamtypes.SortDownstreamVersions(result.AllVersions, licenseChan.IsSemverRequired) @@ -686,17 +679,10 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string, if err != nil { return errors.Wrap(err, "failed to get app license") } - foundChannelID, err := s.GetAppChannelID(appID) - var licenseChan *kotsv1beta1.Channel - if foundChannelID == "" { - foundChannelID = kotsutil.GetBackfillChannelIDFromLicense(license) - if err := s.SetAppChannelID(appID, foundChannelID); err != nil { - return errors.Wrap(err, "failed to backfill app channel id from license") - } - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { - errors.Wrap(err, "failed to find channel in license") - } + + licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) + if err != nil { + return errors.Wrap(err, "failed to get or backfill channel") } for _, v := range versions { diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index ab475be5f6..9c4b20b5a1 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -292,17 +292,9 @@ func (s *KOTSStore) GetAppVersionBaseSequence(appID string, versionLabel string) return -1, errors.Wrap(err, "failed to get app license") } - foundChannelID, err := s.GetAppChannelID(appID) - var licenseChan *kotsv1beta1.Channel - if foundChannelID == "" { - backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) - if err := s.SetAppChannelID(appID, backfillID); err != nil { - return -1, errors.Wrap(err, "failed to backfill app channel id from license") - } - foundChannelID = backfillID - } - if licenseChan, err = kotsutil.FindChannelInLicense(foundChannelID, license); err != nil { - return -1, errors.Wrap(err, "failed to find channel in license") + licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) + if err != nil { + return -1, errors.Wrap(err, "failed to get or backfill channel") } // add to the top of the list and sort diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index afac7c9ebe..fec8354466 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -96,6 +96,21 @@ func (mr *MockStoreMockRecorder) AddDownstreamVersionsDetails(appID, clusterID, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDownstreamVersionsDetails", reflect.TypeOf((*MockStore)(nil).AddDownstreamVersionsDetails), appID, clusterID, versions, checkIfDeployable) } +// BackfillChannelIDFromLicense mocks base method. +func (m *MockStore) BackfillChannelIDFromLicense(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BackfillChannelIDFromLicense", appID, license) + ret0, _ := ret[0].(*v1beta10.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BackfillChannelIDFromLicense indicates an expected call of BackfillChannelIDFromLicense. +func (mr *MockStoreMockRecorder) BackfillChannelIDFromLicense(appID, license interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackfillChannelIDFromLicense", reflect.TypeOf((*MockStore)(nil).BackfillChannelIDFromLicense), appID, license) +} + // CreateApp mocks base method. func (m *MockStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() @@ -2693,6 +2708,21 @@ func (mr *MockAppStoreMockRecorder) AddAppToAllDownstreams(appID interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAppToAllDownstreams", reflect.TypeOf((*MockAppStore)(nil).AddAppToAllDownstreams), appID) } +// BackfillChannelIDFromLicense mocks base method. +func (m *MockAppStore) BackfillChannelIDFromLicense(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BackfillChannelIDFromLicense", appID, license) + ret0, _ := ret[0].(*v1beta10.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BackfillChannelIDFromLicense indicates an expected call of BackfillChannelIDFromLicense. +func (mr *MockAppStoreMockRecorder) BackfillChannelIDFromLicense(appID, license interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackfillChannelIDFromLicense", reflect.TypeOf((*MockAppStore)(nil).BackfillChannelIDFromLicense), appID, license) +} + // CreateApp mocks base method. func (m *MockAppStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 11396dd759..677d24e561 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -132,6 +132,7 @@ type AppStore interface { RemoveApp(appID string) error SetAppChannelChanged(appID string, channelChanged bool) error SetAppChannelID(appID string, channelID string) error + BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) } type DownstreamStore interface { diff --git a/pkg/update/update.go b/pkg/update/update.go index b50d379b55..b2cc7dae44 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -34,13 +34,11 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k var err error var licenseChan *kotsv1beta1.Channel if app.ChannelID == "" { - backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) - if err := kotsStore.SetAppChannelID(app.ID, backfillID); err != nil { - return nil, errors.Wrap(err, "failed to backfill channel id") - } - if licenseChan, err = kotsutil.FindChannelInLicense(backfillID, license); err != nil { - return nil, errors.Wrap(err, "failed to find backfilled channel in license") + licenseChan, err = kotsStore.BackfillChannelIDFromLicense(app.ID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id from license") } + app.ChannelID = licenseChan.ChannelID } else { if licenseChan, err = kotsutil.FindChannelInLicense(app.ChannelID, license); err != nil { return nil, errors.Wrap(err, "failed to find channel in license") diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 957cd101a7..4b8d541591 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -59,7 +59,15 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill + mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( + &kotsv1beta1.Channel{ + ChannelID: "channel-id", + ChannelName: "channel-name", + ChannelSlug: "channel-name", + IsDefault: true, + }, + nil, + ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, @@ -103,7 +111,15 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill + mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( + &kotsv1beta1.Channel{ + ChannelID: "channel-id", + ChannelName: "channel-name", + ChannelSlug: "channel-name", + IsDefault: true, + }, + nil, + ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{ @@ -149,7 +165,15 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().SetAppChannelID(args.app.ID, args.license.Spec.ChannelID).Return(nil) // expect a backfill + mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( + &kotsv1beta1.Channel{ + ChannelID: "channel-id", + ChannelName: "channel-name", + ChannelSlug: "channel-name", + IsDefault: true, + }, + nil, + ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index d44969ed99..f8ecf91c92 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -230,14 +230,15 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- var licenseChan *kotsv1beta1.Channel if a.ChannelID == "" { - backfillID := kotsutil.GetBackfillChannelIDFromLicense(latestLicense) - if err := store.SetAppChannelID(a.ID, backfillID); err != nil { - return nil, errors.Wrap(err, "failed to backfill app channel id from license") + licenseChan, err = store.BackfillChannelIDFromLicense(a.ID, latestLicense) + if err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id from license") + } + a.ChannelID = licenseChan.ChannelID + } else { + if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { + return nil, errors.Wrap(err, "failed to find channel in license") } - a.ChannelID = backfillID - } - if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") } updateCursor, err := store.GetCurrentUpdateCursor(a.ID, licenseChan.ChannelID) From 2b16bf153a0c66009d031af22b490bb5cefb0a13 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 10:31:40 -0500 Subject: [PATCH 21/35] Update migrations/tables/app.yaml Co-authored-by: Salah Al Saleh --- migrations/tables/app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/tables/app.yaml b/migrations/tables/app.yaml index ccdc0b51ba..84caf03014 100644 --- a/migrations/tables/app.yaml +++ b/migrations/tables/app.yaml @@ -85,5 +85,5 @@ spec: default: 0 constraints: notNull: true - - name: channel_id + - name: selected_channel_id type: text From c39c4553a3b154a1bdfe09fdfea78df9464d3fd1 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 15:38:55 +0000 Subject: [PATCH 22/35] pr feedback - rename channel_id -> selected_channel_id --- pkg/app/types/app.go | 6 ++-- pkg/automation/automation.go | 2 +- pkg/handlers/license.go | 6 ++-- pkg/handlers/update_checker_spec.go | 6 ++-- pkg/operator/operator.go | 2 +- pkg/registry/registry.go | 2 +- pkg/render/render.go | 2 +- pkg/store/kotsstore/app_store.go | 18 +++++----- pkg/store/mock/mock.go | 52 +++++++++++++-------------- pkg/store/store_interface.go | 2 +- pkg/tests/renderdir/renderdir_test.go | 2 +- pkg/update/update.go | 6 ++-- pkg/update/update_test.go | 12 +++---- pkg/updatechecker/updatechecker.go | 6 ++-- 14 files changed, 62 insertions(+), 62 deletions(-) diff --git a/pkg/app/types/app.go b/pkg/app/types/app.go index 8930d90655..a933ee64d2 100644 --- a/pkg/app/types/app.go +++ b/pkg/app/types/app.go @@ -30,7 +30,7 @@ type App struct { InstallState string `json:"installState"` LastLicenseSync string `json:"lastLicenseSync"` ChannelChanged bool `json:"channelChanged"` - ChannelID string `json:"channel_id"` + SelectedChannelID string `json:"selected_channel_id"` } func (a *App) GetID() string { @@ -45,8 +45,8 @@ func (a *App) GetCurrentSequence() int64 { return a.CurrentSequence } -func (a *App) GetChannelID() string { - return a.ChannelID +func (a *App) GetSelectedChannelID() string { + return a.SelectedChannelID } func (a *App) GetIsAirgap() bool { diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 0a9bb9baff..557568f36a 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -321,7 +321,7 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. Name: a.Name, LicenseData: string(license), VersionLabel: instParams.AppVersionLabel, - ChannelID: a.ChannelID, + ChannelID: a.SelectedChannelID, }, UpstreamURI: upstreamURI, IsAutomated: true, diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index 5e009b01f3..b82cfdc2cf 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -358,7 +358,7 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { ID: a.ID, Slug: a.Slug, Name: a.Name, - ChannelID: a.ChannelID, + ChannelID: a.SelectedChannelID, LicenseData: uploadLicenseRequest.LicenseData, VersionLabel: installationParams.AppVersionLabel, }, @@ -442,7 +442,7 @@ func (h *Handler) ResumeInstallOnline(w http.ResponseWriter, r *http.Request) { Slug: a.Slug, Name: a.Name, VersionLabel: installationParams.AppVersionLabel, - ChannelID: a.ChannelID, + ChannelID: a.SelectedChannelID, } // the license data is left in the table @@ -680,7 +680,7 @@ func licenseResponseFromLicense(license *kotsv1beta1.License, app *apptypes.App) return entitlements[i].Title < entitlements[j].Title }) - channel, err := kotsutil.FindChannelInLicense(app.ChannelID, license) + channel, err := kotsutil.FindChannelInLicense(app.SelectedChannelID, license) if err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index 8b7bdcf926..50c4ec1efb 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -58,7 +58,7 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque } var licenseChan *kotsv1beta1.Channel - if foundApp.ChannelID == "" { + if foundApp.SelectedChannelID == "" { licenseChan, err = store.GetStore().BackfillChannelIDFromLicense(foundApp.ID, license) if err != nil { updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" @@ -66,9 +66,9 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) return } - foundApp.ChannelID = licenseChan.ChannelID + foundApp.SelectedChannelID = licenseChan.ChannelID } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil { + if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.SelectedChannelID, license); err != nil { updateCheckerSpecResponse.Error = "failed to find channel in license" logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 921d18ce49..fd66675d64 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -1022,7 +1022,7 @@ func (o *Operator) reconcileDeployment(cm *corev1.ConfigMap) (finalError error) } } - if err := store.GetStore().SetAppChannelID(appID, cm.Data["channel-id"]); err != nil { + if err := store.GetStore().SetAppSelectedChannelID(appID, cm.Data["channel-id"]); err != nil { return errors.Wrap(err, "failed to set app channel id") } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index c15143abc2..6fb28305b6 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -147,7 +147,7 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin }, AppID: a.ID, AppSlug: a.Slug, - AppChannelID: a.ChannelID, + AppChannelID: a.SelectedChannelID, IsGitOps: a.IsGitOps, AppSequence: nextAppSequence, ReportingInfo: reporting.GetReportingInfo(a.ID), diff --git a/pkg/render/render.go b/pkg/render/render.go index cc0c4f07de..0bc2fc8b63 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -141,7 +141,7 @@ func RenderDir(opts types.RenderDirOptions) error { IsAirgap: opts.App.IsAirgap, AppID: opts.App.ID, AppSlug: opts.App.Slug, - AppChannelID: opts.App.ChannelID, + AppChannelID: opts.App.SelectedChannelID, IsGitOps: opts.App.IsGitOps, AppSequence: opts.Sequence, ReportingInfo: opts.ReportingInfo, diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index 579a3d9e78..db58c20f5c 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -174,9 +174,9 @@ func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { var restoreUndeployStatus gorqlite.NullString var updateCheckerSpec gorqlite.NullString var autoDeploy gorqlite.NullString - var channelID gorqlite.NullString + var selectedChannelId gorqlite.NullString - if err := rows.Scan(&app.ID, &app.Name, &licenseStr, &upstreamURI, &iconURI, &app.CreatedAt, &updatedAt, &app.Slug, ¤tSequence, &lastUpdateCheckAt, &lastLicenseSync, &app.IsAirgap, &snapshotTTLNew, &snapshotSchedule, &restoreInProgressName, &restoreUndeployStatus, &updateCheckerSpec, &autoDeploy, &app.InstallState, &app.ChannelChanged, &channelID); err != nil { + if err := rows.Scan(&app.ID, &app.Name, &licenseStr, &upstreamURI, &iconURI, &app.CreatedAt, &updatedAt, &app.Slug, ¤tSequence, &lastUpdateCheckAt, &lastLicenseSync, &app.IsAirgap, &snapshotTTLNew, &snapshotSchedule, &restoreInProgressName, &restoreUndeployStatus, &updateCheckerSpec, &autoDeploy, &app.InstallState, &app.ChannelChanged, &selectedChannelId); err != nil { return nil, errors.Wrap(err, "failed to scan app") } @@ -189,7 +189,7 @@ func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { app.RestoreUndeployStatus = apptypes.UndeployStatus(restoreUndeployStatus.String) app.UpdateCheckerSpec = updateCheckerSpec.String app.AutoDeploy = apptypes.AutoDeploy(autoDeploy.String) - app.ChannelID = channelID.String + app.SelectedChannelID = selectedChannelId.String if lastLicenseSync.Valid { app.LastLicenseSync = lastLicenseSync.Time.Format(time.RFC3339) @@ -600,9 +600,9 @@ func (s *KOTSStore) SetAppChannelChanged(appID string, channelChanged bool) erro return nil } -func (s *KOTSStore) GetAppChannelID(appID string) (string, error) { +func (s *KOTSStore) GetAppSelectedChannelID(appID string) (string, error) { db := persistence.MustGetDBSession() - query := `select channel_id from app where id = ?` + query := `select selected_channel_id from app where id = ?` rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{appID}, @@ -622,12 +622,12 @@ func (s *KOTSStore) GetAppChannelID(appID string) (string, error) { return channelID.String, nil } -func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { +func (s *KOTSStore) SetAppSelectedChannelID(appID string, channelID string) error { logger.Debug("setting app channel id", zap.String("appID", appID), zap.String("channelID", channelID)) db := persistence.MustGetDBSession() - query := `update app set channel_id = ? where id = ?` + query := `update app set selected_channel_id = ? where id = ?` wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{channelID, appID}, @@ -641,14 +641,14 @@ func (s *KOTSStore) SetAppChannelID(appID string, channelID string) error { func (s *KOTSStore) BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) - if err := s.SetAppChannelID(appID, backfillID); err != nil { + if err := s.SetAppSelectedChannelID(appID, backfillID); err != nil { return nil, errors.Wrap(err, "failed to backfill app channel id from license") } return kotsutil.FindChannelInLicense(backfillID, license) } func (s *KOTSStore) GetOrBackfillLicenseChannel(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { - foundChannelID, err := s.GetAppChannelID(appID) + foundChannelID, err := s.GetAppSelectedChannelID(appID) if err != nil { return nil, errors.Wrap(err, "failed to get app channel id") } diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index fec8354466..d382769e87 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -1533,20 +1533,6 @@ func (mr *MockStoreMockRecorder) SetAppChannelChanged(appID, channelChanged inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelChanged", reflect.TypeOf((*MockStore)(nil).SetAppChannelChanged), appID, channelChanged) } -// SetAppChannelID mocks base method. -func (m *MockStore) SetAppChannelID(appID, channelID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetAppChannelID", appID, channelID) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetAppChannelID indicates an expected call of SetAppChannelID. -func (mr *MockStoreMockRecorder) SetAppChannelID(appID, channelID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelID", reflect.TypeOf((*MockStore)(nil).SetAppChannelID), appID, channelID) -} - // SetAppInstallState mocks base method. func (m *MockStore) SetAppInstallState(appID, state string) error { m.ctrl.T.Helper() @@ -1575,6 +1561,20 @@ func (mr *MockStoreMockRecorder) SetAppIsAirgap(appID, isAirgap interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppIsAirgap", reflect.TypeOf((*MockStore)(nil).SetAppIsAirgap), appID, isAirgap) } +// SetAppSelectedChannelID mocks base method. +func (m *MockStore) SetAppSelectedChannelID(appID, channelID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetAppSelectedChannelID", appID, channelID) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetAppSelectedChannelID indicates an expected call of SetAppSelectedChannelID. +func (mr *MockStoreMockRecorder) SetAppSelectedChannelID(appID, channelID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppSelectedChannelID", reflect.TypeOf((*MockStore)(nil).SetAppSelectedChannelID), appID, channelID) +} + // SetAppStatus mocks base method. func (m *MockStore) SetAppStatus(appID string, resourceStates types4.ResourceStates, updatedAt time.Time, sequence int64) error { m.ctrl.T.Helper() @@ -2916,32 +2916,32 @@ func (mr *MockAppStoreMockRecorder) SetAppChannelChanged(appID, channelChanged i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelChanged", reflect.TypeOf((*MockAppStore)(nil).SetAppChannelChanged), appID, channelChanged) } -// SetAppChannelID mocks base method. -func (m *MockAppStore) SetAppChannelID(appID, channelID string) error { +// SetAppInstallState mocks base method. +func (m *MockAppStore) SetAppInstallState(appID, state string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetAppChannelID", appID, channelID) + ret := m.ctrl.Call(m, "SetAppInstallState", appID, state) ret0, _ := ret[0].(error) return ret0 } -// SetAppChannelID indicates an expected call of SetAppChannelID. -func (mr *MockAppStoreMockRecorder) SetAppChannelID(appID, channelID interface{}) *gomock.Call { +// SetAppInstallState indicates an expected call of SetAppInstallState. +func (mr *MockAppStoreMockRecorder) SetAppInstallState(appID, state interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppChannelID", reflect.TypeOf((*MockAppStore)(nil).SetAppChannelID), appID, channelID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppInstallState", reflect.TypeOf((*MockAppStore)(nil).SetAppInstallState), appID, state) } -// SetAppInstallState mocks base method. -func (m *MockAppStore) SetAppInstallState(appID, state string) error { +// SetAppSelectedChannelID mocks base method. +func (m *MockAppStore) SetAppSelectedChannelID(appID, channelID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetAppInstallState", appID, state) + ret := m.ctrl.Call(m, "SetAppSelectedChannelID", appID, channelID) ret0, _ := ret[0].(error) return ret0 } -// SetAppInstallState indicates an expected call of SetAppInstallState. -func (mr *MockAppStoreMockRecorder) SetAppInstallState(appID, state interface{}) *gomock.Call { +// SetAppSelectedChannelID indicates an expected call of SetAppSelectedChannelID. +func (mr *MockAppStoreMockRecorder) SetAppSelectedChannelID(appID, channelID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppInstallState", reflect.TypeOf((*MockAppStore)(nil).SetAppInstallState), appID, state) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAppSelectedChannelID", reflect.TypeOf((*MockAppStore)(nil).SetAppSelectedChannelID), appID, channelID) } // SetAutoDeploy mocks base method. diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 677d24e561..e392a5deef 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -131,7 +131,7 @@ type AppStore interface { SetSnapshotSchedule(appID string, snapshotSchedule string) error RemoveApp(appID string) error SetAppChannelChanged(appID string, channelChanged bool) error - SetAppChannelID(appID string, channelID string) error + SetAppSelectedChannelID(appID string, channelID string) error BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) } diff --git a/pkg/tests/renderdir/renderdir_test.go b/pkg/tests/renderdir/renderdir_test.go index 2d47c3ed6a..7d43c343e2 100644 --- a/pkg/tests/renderdir/renderdir_test.go +++ b/pkg/tests/renderdir/renderdir_test.go @@ -73,7 +73,7 @@ func TestKotsRenderDir(t *testing.T) { Name: spec.Name, RenderDirOptions: spec.RenderDirOptions, } - test.RenderDirOptions.App.ChannelID = "1vusIYZLAVxMG6q760OJmRKj5i5" + test.RenderDirOptions.App.SelectedChannelID = "1vusIYZLAVxMG6q760OJmRKj5i5" tests = append(tests, test) } require.NoError(t, err) diff --git a/pkg/update/update.go b/pkg/update/update.go index b2cc7dae44..5c9640af28 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -33,14 +33,14 @@ func InitAvailableUpdatesDir() error { func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { var err error var licenseChan *kotsv1beta1.Channel - if app.ChannelID == "" { + if app.SelectedChannelID == "" { licenseChan, err = kotsStore.BackfillChannelIDFromLicense(app.ID, license) if err != nil { return nil, errors.Wrap(err, "failed to backfill channel id from license") } - app.ChannelID = licenseChan.ChannelID + app.SelectedChannelID = licenseChan.ChannelID } else { - if licenseChan, err = kotsutil.FindChannelInLicense(app.ChannelID, license); err != nil { + if licenseChan, err = kotsutil.FindChannelInLicense(app.SelectedChannelID, license); err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } } diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 4b8d541591..59a017a592 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -43,8 +43,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", - ChannelID: "", // using legacy non-multi chan license + ID: "app-id", + SelectedChannelID: "", // using legacy non-multi chan license }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -78,8 +78,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", - ChannelID: "", // using legacy non-multi chan license + ID: "app-id", + SelectedChannelID: "", // using legacy non-multi chan license }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -184,8 +184,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", - ChannelID: "channel-id2", // explicitly using the non-default channel + ID: "app-id", + SelectedChannelID: "channel-id2", // explicitly using the non-default channel }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index f8ecf91c92..8e3dcdab55 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -229,14 +229,14 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- } var licenseChan *kotsv1beta1.Channel - if a.ChannelID == "" { + if a.SelectedChannelID == "" { licenseChan, err = store.BackfillChannelIDFromLicense(a.ID, latestLicense) if err != nil { return nil, errors.Wrap(err, "failed to backfill channel id from license") } - a.ChannelID = licenseChan.ChannelID + a.SelectedChannelID = licenseChan.ChannelID } else { - if licenseChan, err = kotsutil.FindChannelInLicense(a.ChannelID, latestLicense); err != nil { + if licenseChan, err = kotsutil.FindChannelInLicense(a.SelectedChannelID, latestLicense); err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } } From 8e0c2dfde4f49af9dd059261f156af04db891878 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 10:55:14 -0500 Subject: [PATCH 23/35] Update pkg/kotsutil/kots.go Co-authored-by: Salah Al Saleh --- pkg/kotsutil/kots.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index cdbdc75900..33291e7244 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1649,7 +1649,7 @@ func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kots return nil, errors.New("channel not found in multi channel format license") } -func GetBackfillChannelIDFromLicense(license *kotsv1beta1.License) string { +func GetDefaultChannelIDFromLicense(license *kotsv1beta1.License) string { for _, channel := range license.Spec.Channels { if channel.IsDefault { return channel.ChannelID From 61ea5d54becda901b8e5e86a0a67f8569d7b5f45 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 15:56:36 +0000 Subject: [PATCH 24/35] move multichannel.go to license package --- cmd/kots/cli/install.go | 4 ++-- cmd/kots/cli/pull.go | 4 ++-- pkg/kotsutil/kots.go | 5 ++++- pkg/{multichannel => license}/multichannel.go | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) rename pkg/{multichannel => license}/multichannel.go (98%) diff --git a/cmd/kots/cli/install.go b/cmd/kots/cli/install.go index e129c7d28a..5b8f7a1e5b 100644 --- a/cmd/kots/cli/install.go +++ b/cmd/kots/cli/install.go @@ -31,9 +31,9 @@ import ( kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/kurl" + kotslicense "github.com/replicatedhq/kots/pkg/license" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/metrics" - "github.com/replicatedhq/kots/pkg/multichannel" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" "github.com/replicatedhq/kots/pkg/print" "github.com/replicatedhq/kots/pkg/pull" @@ -167,7 +167,7 @@ func InstallCmd() *cobra.Command { if err != nil { return errors.Wrap(err, "failed to extract preferred channel slug") } - license, err = multichannel.VerifyAndUpdateLicense(log, license, preferredChannelSlug, isAirgap) + license, err = kotslicense.VerifyAndUpdateLicense(log, license, preferredChannelSlug, isAirgap) if err != nil { return errors.Wrap(err, "failed to verify and update license") } diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index fadd36e0ee..a6d760eb19 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" + kotslicense "github.com/replicatedhq/kots/pkg/license" "github.com/replicatedhq/kots/pkg/logger" - "github.com/replicatedhq/kots/pkg/multichannel" "github.com/replicatedhq/kots/pkg/pull" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -111,7 +111,7 @@ func PullCmd() *cobra.Command { // If we are passed a multi-channel license, verify that the requested channel is in the license // so that we can warn the user immediately if it is not. - license, err = multichannel.VerifyAndUpdateLicense(log, license, preferredChannelSlug, false) + license, err = kotslicense.VerifyAndUpdateLicense(log, license, preferredChannelSlug, false) if err != nil { return errors.Wrap(err, "failed to verify and update license") } diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index cdbdc75900..6deb6411a6 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1627,7 +1627,10 @@ func FindChannelIDInLicense(requestedSlug string, license *kotsv1beta1.License) } func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { - if channelID == "" || len(license.Spec.Channels) == 0 { + if channelID == "" { + return nil, errors.New("channelID is required") + } + if len(license.Spec.Channels) == 0 { if license.Spec.ChannelID != channelID { return nil, errors.New("channel not found in non-multi channel license") } diff --git a/pkg/multichannel/multichannel.go b/pkg/license/multichannel.go similarity index 98% rename from pkg/multichannel/multichannel.go rename to pkg/license/multichannel.go index d2f8ce997f..31f3ea6b5c 100644 --- a/pkg/multichannel/multichannel.go +++ b/pkg/license/multichannel.go @@ -1,4 +1,4 @@ -package multichannel +package license import ( "github.com/pkg/errors" From f2624c8e7d5bb2a98151a79980d78de23806130f Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 16:18:27 +0000 Subject: [PATCH 25/35] general pr feedback fixup tests --- pkg/handlers/update_checker_spec.go | 25 ++++-------- pkg/pull/pull.go | 2 +- pkg/store/kotsstore/app_store.go | 6 +-- pkg/store/mock/mock.go | 60 ++++++++++++++--------------- pkg/store/store_interface.go | 2 +- pkg/update/update.go | 15 +++----- pkg/update/update_test.go | 36 ++++++++--------- pkg/updatechecker/updatechecker.go | 15 ++------ 8 files changed, 66 insertions(+), 95 deletions(-) diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index 50c4ec1efb..6f76581419 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -11,7 +11,6 @@ import ( "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/updatechecker" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" cron "github.com/robfig/cron/v3" ) @@ -57,24 +56,14 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque return } - var licenseChan *kotsv1beta1.Channel - if foundApp.SelectedChannelID == "" { - licenseChan, err = store.GetStore().BackfillChannelIDFromLicense(foundApp.ID, license) - if err != nil { - updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" - logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) - JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) - return - } - foundApp.SelectedChannelID = licenseChan.ChannelID - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.SelectedChannelID, license); err != nil { - updateCheckerSpecResponse.Error = "failed to find channel in license" - logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) - JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) - return - } + licenseChan, err := store.GetStore().GetOrBackfillLicenseChannel(foundApp.ID, license) + if err != nil { + updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" + logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) + JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) + return } + foundApp.SelectedChannelID = licenseChan.ChannelID // Check if the deploy update configuration is valid based on app channel if licenseChan.IsSemverRequired { diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 41b02ec125..04c6c46ea2 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -241,7 +241,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { if _, err = kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, fetchOptions.License); err != nil { return "", util.ActionableError{ NoRetry: true, // if this is airgap upload, make sure to free up tmp space - Message: fmt.Sprintf("License (%s) and airgap bundle (%s) channels do not match.", fetchOptions.License.Spec.ChannelName, airgap.Spec.ChannelName), + Message: fmt.Sprintf("Requested channel (%s) not found in license.", airgap.Spec.ChannelName), } } diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index db58c20f5c..a388a2d866 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -639,8 +639,8 @@ func (s *KOTSStore) SetAppSelectedChannelID(appID string, channelID string) erro return nil } -func (s *KOTSStore) BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { - backfillID := kotsutil.GetBackfillChannelIDFromLicense(license) +func (s *KOTSStore) backfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { + backfillID := kotsutil.GetDefaultChannelIDFromLicense(license) if err := s.SetAppSelectedChannelID(appID, backfillID); err != nil { return nil, errors.Wrap(err, "failed to backfill app channel id from license") } @@ -654,7 +654,7 @@ func (s *KOTSStore) GetOrBackfillLicenseChannel(appID string, license *kotsv1bet } if foundChannelID == "" { - return s.BackfillChannelIDFromLicense(appID, license) + return s.backfillChannelIDFromLicense(appID, license) } licenseChan, err := kotsutil.FindChannelInLicense(foundChannelID, license) diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index d382769e87..9bd37e73a2 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -96,21 +96,6 @@ func (mr *MockStoreMockRecorder) AddDownstreamVersionsDetails(appID, clusterID, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDownstreamVersionsDetails", reflect.TypeOf((*MockStore)(nil).AddDownstreamVersionsDetails), appID, clusterID, versions, checkIfDeployable) } -// BackfillChannelIDFromLicense mocks base method. -func (m *MockStore) BackfillChannelIDFromLicense(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BackfillChannelIDFromLicense", appID, license) - ret0, _ := ret[0].(*v1beta10.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BackfillChannelIDFromLicense indicates an expected call of BackfillChannelIDFromLicense. -func (mr *MockStoreMockRecorder) BackfillChannelIDFromLicense(appID, license interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackfillChannelIDFromLicense", reflect.TypeOf((*MockStore)(nil).BackfillChannelIDFromLicense), appID, license) -} - // CreateApp mocks base method. func (m *MockStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() @@ -912,6 +897,21 @@ func (mr *MockStoreMockRecorder) GetNextAppSequence(appID interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextAppSequence", reflect.TypeOf((*MockStore)(nil).GetNextAppSequence), appID) } +// GetOrBackfillLicenseChannel mocks base method. +func (m *MockStore) GetOrBackfillLicenseChannel(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrBackfillLicenseChannel", appID, license) + ret0, _ := ret[0].(*v1beta10.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrBackfillLicenseChannel indicates an expected call of GetOrBackfillLicenseChannel. +func (mr *MockStoreMockRecorder) GetOrBackfillLicenseChannel(appID, license interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrBackfillLicenseChannel", reflect.TypeOf((*MockStore)(nil).GetOrBackfillLicenseChannel), appID, license) +} + // GetParentSequenceForSequence mocks base method. func (m *MockStore) GetParentSequenceForSequence(appID, clusterID string, sequence int64) (int64, error) { m.ctrl.T.Helper() @@ -2708,21 +2708,6 @@ func (mr *MockAppStoreMockRecorder) AddAppToAllDownstreams(appID interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAppToAllDownstreams", reflect.TypeOf((*MockAppStore)(nil).AddAppToAllDownstreams), appID) } -// BackfillChannelIDFromLicense mocks base method. -func (m *MockAppStore) BackfillChannelIDFromLicense(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BackfillChannelIDFromLicense", appID, license) - ret0, _ := ret[0].(*v1beta10.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BackfillChannelIDFromLicense indicates an expected call of BackfillChannelIDFromLicense. -func (mr *MockAppStoreMockRecorder) BackfillChannelIDFromLicense(appID, license interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackfillChannelIDFromLicense", reflect.TypeOf((*MockAppStore)(nil).BackfillChannelIDFromLicense), appID, license) -} - // CreateApp mocks base method. func (m *MockAppStore) CreateApp(name, channelID, upstreamURI, licenseData string, isAirgapEnabled, skipImagePush, registryIsReadOnly bool) (*types3.App, error) { m.ctrl.T.Helper() @@ -2798,6 +2783,21 @@ func (mr *MockAppStoreMockRecorder) GetDownstream(clusterID interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownstream", reflect.TypeOf((*MockAppStore)(nil).GetDownstream), clusterID) } +// GetOrBackfillLicenseChannel mocks base method. +func (m *MockAppStore) GetOrBackfillLicenseChannel(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrBackfillLicenseChannel", appID, license) + ret0, _ := ret[0].(*v1beta10.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrBackfillLicenseChannel indicates an expected call of GetOrBackfillLicenseChannel. +func (mr *MockAppStoreMockRecorder) GetOrBackfillLicenseChannel(appID, license interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrBackfillLicenseChannel", reflect.TypeOf((*MockAppStore)(nil).GetOrBackfillLicenseChannel), appID, license) +} + // IsGitOpsEnabledForApp mocks base method. func (m *MockAppStore) IsGitOpsEnabledForApp(appID string) (bool, error) { m.ctrl.T.Helper() diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index e392a5deef..4a07e43cb2 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -132,7 +132,7 @@ type AppStore interface { RemoveApp(appID string) error SetAppChannelChanged(appID string, channelChanged bool) error SetAppSelectedChannelID(appID string, channelID string) error - BackfillChannelIDFromLicense(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) + GetOrBackfillLicenseChannel(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) } type DownstreamStore interface { diff --git a/pkg/update/update.go b/pkg/update/update.go index 5c9640af28..81b900893d 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -33,17 +33,12 @@ func InitAvailableUpdatesDir() error { func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { var err error var licenseChan *kotsv1beta1.Channel - if app.SelectedChannelID == "" { - licenseChan, err = kotsStore.BackfillChannelIDFromLicense(app.ID, license) - if err != nil { - return nil, errors.Wrap(err, "failed to backfill channel id from license") - } - app.SelectedChannelID = licenseChan.ChannelID - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(app.SelectedChannelID, license); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } + + licenseChan, err = kotsStore.GetOrBackfillLicenseChannel(app.ID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id from license") } + app.SelectedChannelID = licenseChan.ChannelID updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, licenseChan.ChannelID) if err != nil { diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 59a017a592..ba5ad644e4 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -9,6 +9,7 @@ import ( "github.com/golang/mock/gomock" apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/kotsutil" storepkg "github.com/replicatedhq/kots/pkg/store" mock_store "github.com/replicatedhq/kots/pkg/store/mock" "github.com/replicatedhq/kots/pkg/update/types" @@ -59,13 +60,9 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( - &kotsv1beta1.Channel{ - ChannelID: "channel-id", - ChannelName: "channel-name", - ChannelSlug: "channel-name", - IsDefault: true, - }, + licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) + mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( + licenseChan, nil, ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) @@ -111,13 +108,9 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( - &kotsv1beta1.Channel{ - ChannelID: "channel-id", - ChannelName: "channel-name", - ChannelSlug: "channel-name", - IsDefault: true, - }, + licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) + mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( + licenseChan, nil, ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) @@ -165,13 +158,9 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - mockStore.EXPECT().BackfillChannelIDFromLicense(args.app.ID, args.license).Return( - &kotsv1beta1.Channel{ - ChannelID: "channel-id", - ChannelName: "channel-name", - ChannelSlug: "channel-name", - IsDefault: true, - }, + licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) + mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( + licenseChan, nil, ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) @@ -212,6 +201,11 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint + licenseChan, _ := kotsutil.FindChannelInLicense("channel-id2", args.license) + mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( + licenseChan, + nil, + ) mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.Channels[1].ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index 8e3dcdab55..261599b11f 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -13,7 +13,6 @@ import ( apptypes "github.com/replicatedhq/kots/pkg/app/types" license "github.com/replicatedhq/kots/pkg/kotsadmlicense" upstream "github.com/replicatedhq/kots/pkg/kotsadmupstream" - "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/preflight" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" @@ -229,17 +228,11 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- } var licenseChan *kotsv1beta1.Channel - if a.SelectedChannelID == "" { - licenseChan, err = store.BackfillChannelIDFromLicense(a.ID, latestLicense) - if err != nil { - return nil, errors.Wrap(err, "failed to backfill channel id from license") - } - a.SelectedChannelID = licenseChan.ChannelID - } else { - if licenseChan, err = kotsutil.FindChannelInLicense(a.SelectedChannelID, latestLicense); err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } + licenseChan, err = store.GetOrBackfillLicenseChannel(a.ID, latestLicense) + if err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id from license") } + a.SelectedChannelID = licenseChan.ChannelID updateCursor, err := store.GetCurrentUpdateCursor(a.ID, licenseChan.ChannelID) if err != nil { From 45ababaa752aae2fe08ccd71a64a7748d056a4a3 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 30 Jul 2024 21:49:25 +0000 Subject: [PATCH 26/35] getRequiredAirgapUpdates uses selectedChannelID to select license chan --- pkg/replicatedapp/api.go | 1 - pkg/update/required.go | 6 +++--- pkg/update/required_test.go | 32 +++++++++++++++++++------------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pkg/replicatedapp/api.go b/pkg/replicatedapp/api.go index fa7662f3c6..7ac29c1f10 100644 --- a/pkg/replicatedapp/api.go +++ b/pkg/replicatedapp/api.go @@ -42,7 +42,6 @@ type LicenseData struct { func GetLatestLicense(license *kotsv1beta1.License) (*LicenseData, error) { url := fmt.Sprintf("%s/license/%s", license.Spec.Endpoint, license.Spec.AppSlug) - licenseData, err := getLicenseFromAPI(url, license.Spec.LicenseID) if err != nil { return nil, errors.Wrap(err, "failed to get license from api") diff --git a/pkg/update/required.go b/pkg/update/required.go index 1b9954bf65..8adddad47f 100644 --- a/pkg/update/required.go +++ b/pkg/update/required.go @@ -41,7 +41,7 @@ func IsAirgapUpdateDeployable(app *apptypes.App, airgap *kotsv1beta1.Airgap) (bo if err != nil { return false, "", errors.Wrap(err, "failed to load license") } - requiredUpdates, err := getRequiredAirgapUpdates(airgap, license, appVersions.AllVersions, app.ChannelChanged) + requiredUpdates, err := getRequiredAirgapUpdates(airgap, license, appVersions.AllVersions, app.ChannelChanged, app.SelectedChannelID) if err != nil { return false, "", errors.Wrap(err, "failed to get missing required versions") } @@ -51,7 +51,7 @@ func IsAirgapUpdateDeployable(app *apptypes.App, airgap *kotsv1beta1.Airgap) (bo return true, "", nil } -func getRequiredAirgapUpdates(airgap *kotsv1beta1.Airgap, license *kotsv1beta1.License, installedVersions []*downstreamtypes.DownstreamVersion, channelChanged bool) ([]string, error) { +func getRequiredAirgapUpdates(airgap *kotsv1beta1.Airgap, license *kotsv1beta1.License, installedVersions []*downstreamtypes.DownstreamVersion, channelChanged bool, selectedChannelID string) ([]string, error) { requiredUpdates := make([]string, 0) // If no versions are installed, we can consider this an initial install. // If the channel changed, we can consider this an initial install. @@ -64,7 +64,7 @@ func getRequiredAirgapUpdates(airgap *kotsv1beta1.Airgap, license *kotsv1beta1.L for _, appVersion := range installedVersions { requiredSemver, requiredSemverErr := semver.ParseTolerant(requiredRelease.VersionLabel) - licenseChan, err := kotsutil.FindChannelInLicense(appVersion.ChannelID, license) + licenseChan, err := kotsutil.FindChannelInLicense(selectedChannelID, license) if err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } diff --git a/pkg/update/required_test.go b/pkg/update/required_test.go index afc8b33758..0a8c834544 100644 --- a/pkg/update/required_test.go +++ b/pkg/update/required_test.go @@ -43,6 +43,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { channelChanged bool wantSemver []string wantNoSemver []string + selectedChannelID string }{ { name: "nothing is installed yet", @@ -63,6 +64,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { installedVersions: []*downstreamtypes.DownstreamVersion{}, wantNoSemver: []string{}, wantSemver: []string{}, + selectedChannelID: channelID, }, { name: "latest satisfies all prerequsites", @@ -93,8 +95,9 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { UpdateCursor: "124", }, }, - wantNoSemver: []string{}, - wantSemver: []string{}, + wantNoSemver: []string{}, + wantSemver: []string{}, + selectedChannelID: channelID, }, { name: "need some prerequsites", @@ -125,8 +128,9 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { UpdateCursor: "117", }, }, - wantNoSemver: []string{"0.1.120", "0.1.123"}, - wantSemver: []string{"0.1.120", "0.1.123"}, + wantNoSemver: []string{"0.1.120", "0.1.123"}, + wantSemver: []string{"0.1.120", "0.1.123"}, + selectedChannelID: channelID, }, { name: "need all prerequsites", @@ -157,8 +161,9 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { UpdateCursor: "113", }, }, - wantNoSemver: []string{"0.1.115", "0.1.120", "0.1.123"}, - wantSemver: []string{"0.1.115", "0.1.120", "0.1.123"}, + wantNoSemver: []string{"0.1.115", "0.1.120", "0.1.123"}, + wantSemver: []string{"0.1.115", "0.1.120", "0.1.123"}, + selectedChannelID: channelID, }, { name: "check across multiple channels", @@ -190,8 +195,9 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { UpdateCursor: "117", }, }, - wantNoSemver: []string{}, - wantSemver: []string{}, + wantNoSemver: []string{}, + wantSemver: []string{}, + selectedChannelID: channelID, }, { name: "check across multiple channels with multi chan license", @@ -251,14 +257,14 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { UpdateCursor: "117", }, }, - wantNoSemver: []string{}, - wantSemver: []string{}, + wantNoSemver: []string{}, + wantSemver: []string{}, + selectedChannelID: channelID, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - for _, v := range tt.installedVersions { s := semver.MustParse(v.VersionLabel) v.Semver = &s @@ -269,13 +275,13 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { // cursor based tt.license.Spec.IsSemverRequired = false - got, err := getRequiredAirgapUpdates(tt.airgap, tt.license, tt.installedVersions, tt.channelChanged) + got, err := getRequiredAirgapUpdates(tt.airgap, tt.license, tt.installedVersions, tt.channelChanged, tt.selectedChannelID) req.NoError(err) req.Equal(tt.wantNoSemver, got) // semver based tt.license.Spec.IsSemverRequired = true - got, err = getRequiredAirgapUpdates(tt.airgap, tt.license, tt.installedVersions, tt.channelChanged) + got, err = getRequiredAirgapUpdates(tt.airgap, tt.license, tt.installedVersions, tt.channelChanged, tt.selectedChannelID) req.NoError(err) req.Equal(tt.wantSemver, got) }) From f38716c447c2a52fc73fa9cb5605707cde55bacd Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 31 Jul 2024 17:21:33 +0000 Subject: [PATCH 27/35] Replicate app api calls pass selectedChannelId --- pkg/automation/automation.go | 28 +++++++++++----------- pkg/handlers/license.go | 42 ++++++++++++++++----------------- pkg/handlers/upgrade_service.go | 2 +- pkg/kotsadmlicense/license.go | 4 ++-- pkg/license/multichannel.go | 3 ++- pkg/replicatedapp/api.go | 9 ++++++- pkg/replicatedapp/api_test.go | 11 +++++---- pkg/replicatedapp/upstream.go | 3 ++- pkg/upstream/replicated.go | 17 ++++++------- 9 files changed, 65 insertions(+), 54 deletions(-) diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index 557568f36a..e38e2c16dd 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -203,8 +203,21 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. return errors.Wrap(err, "failed to verify license signature") } + instParams, err := kotsutil.GetInstallationParams(kotsadmtypes.KotsadmConfigMap) + if err != nil { + return errors.Wrap(err, "failed to get existing kotsadm config map") + } + + desiredAppName := strings.Replace(appSlug, "-", " ", 0) + upstreamURI := fmt.Sprintf("replicated://%s", appSlug) + + matchedChannelID, err := kotsutil.FindChannelIDInLicense(instParams.RequestedChannelSlug, verifiedLicense) + if err != nil { + return errors.Wrap(err, "failed to find requested channel in license") + } + if !kotsadm.IsAirgap() { - licenseData, err := replicatedapp.GetLatestLicense(verifiedLicense) + licenseData, err := replicatedapp.GetLatestLicense(verifiedLicense, matchedChannelID) if err != nil { return errors.Wrap(err, "failed to get latest license") } @@ -236,19 +249,6 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. } } - instParams, err := kotsutil.GetInstallationParams(kotsadmtypes.KotsadmConfigMap) - if err != nil { - return errors.Wrap(err, "failed to get existing kotsadm config map") - } - - desiredAppName := strings.Replace(appSlug, "-", " ", 0) - upstreamURI := fmt.Sprintf("replicated://%s", appSlug) - - matchedChannelID, err := kotsutil.FindChannelIDInLicense(instParams.RequestedChannelSlug, verifiedLicense) - if err != nil { - return errors.Wrap(err, "failed to find requested channel in license") - } - a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly) if err != nil { return errors.Wrap(err, "failed to create app record") diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index b82cfdc2cf..d903ce8a17 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -272,10 +272,30 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { return } + installationParams, err := kotsutil.GetInstallationParams(kotsadmtypes.KotsadmConfigMap) + if err != nil { + logger.Error(err) + uploadLicenseResponse.Error = err.Error() + JSON(w, http.StatusInternalServerError, uploadLicenseResponse) + return + } + + desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0) + upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) + + // verify that requested channel slug exists in the license + matchedChannelID, err := kotsutil.FindChannelIDInLicense(installationParams.RequestedChannelSlug, verifiedLicense) + if err != nil { + logger.Error(err) + uploadLicenseResponse.Error = "Your current license does not grant access to the channel you requested. Please generate a support bundle and contact support for assistance." + JSON(w, http.StatusBadRequest, uploadLicenseResponse) + return + } + if !kotsadm.IsAirgap() { // sync license logger.Info("syncing license with server to retrieve latest version") - licenseData, err := replicatedapp.GetLatestLicense(verifiedLicense) + licenseData, err := replicatedapp.GetLatestLicense(verifiedLicense, matchedChannelID) if err != nil { logger.Error(errors.Wrap(err, "failed to get latest license")) uploadLicenseResponse.Error = err.Error() @@ -323,26 +343,6 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { } } - installationParams, err := kotsutil.GetInstallationParams(kotsadmtypes.KotsadmConfigMap) - if err != nil { - logger.Error(err) - uploadLicenseResponse.Error = err.Error() - JSON(w, http.StatusInternalServerError, uploadLicenseResponse) - return - } - - desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0) - upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug) - - // verify that requested channel slug exists in the license - matchedChannelID, err := kotsutil.FindChannelIDInLicense(installationParams.RequestedChannelSlug, verifiedLicense) - if err != nil { - logger.Error(err) - uploadLicenseResponse.Error = "Your current license does not grant access to the channel you requested. Please generate a support bundle and contact support for assistance." - JSON(w, http.StatusBadRequest, uploadLicenseResponse) - return - } - a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly) if err != nil { logger.Error(err) diff --git a/pkg/handlers/upgrade_service.go b/pkg/handlers/upgrade_service.go index 6776d83e9d..1dab0b6908 100644 --- a/pkg/handlers/upgrade_service.go +++ b/pkg/handlers/upgrade_service.go @@ -134,7 +134,7 @@ func canStartUpgradeService(a *apptypes.App, r StartUpgradeServiceRequest) (bool return true, "", nil } - ll, err := replicatedapp.GetLatestLicense(currLicense) + ll, err := replicatedapp.GetLatestLicense(currLicense, a.SelectedChannelID) if err != nil { return false, "", errors.Wrap(err, "failed to get latest license") } diff --git a/pkg/kotsadmlicense/license.go b/pkg/kotsadmlicense/license.go index 78a2ec83b8..74fb9532f4 100644 --- a/pkg/kotsadmlicense/license.go +++ b/pkg/kotsadmlicense/license.go @@ -46,7 +46,7 @@ func Sync(a *apptypes.App, licenseString string, failOnVersionCreate bool) (*kot updatedLicense = verifiedLicense } else { // get from the api - licenseData, err := replicatedapp.GetLatestLicense(currentLicense) + licenseData, err := replicatedapp.GetLatestLicense(currentLicense, a.SelectedChannelID) if err != nil { return nil, false, errors.Wrap(err, "failed to get latest license") } @@ -120,7 +120,7 @@ func Change(a *apptypes.App, newLicenseString string) (*kotsv1beta1.License, err } if !a.IsAirgap { - licenseData, err := replicatedapp.GetLatestLicense(newLicense) + licenseData, err := replicatedapp.GetLatestLicense(newLicense, a.SelectedChannelID) if err != nil { return nil, errors.Wrap(err, "failed to get latest license") } diff --git a/pkg/license/multichannel.go b/pkg/license/multichannel.go index 31f3ea6b5c..db7f58d444 100644 --- a/pkg/license/multichannel.go +++ b/pkg/license/multichannel.go @@ -46,7 +46,8 @@ func VerifyAndUpdateLicense(log *logger.CLILogger, license *kotsv1beta1.License, return license, nil } log.ActionWithSpinner("Checking for license update") - updatedLicense, err := replicatedapp.GetLatestLicense(license) + // we fetch the latest license to ensure that the license is up to date, before proceeding + updatedLicense, err := replicatedapp.GetLatestLicense(license, "") if err != nil { log.FinishSpinnerWithError() return nil, errors.Wrap(err, "failed to get latest license") diff --git a/pkg/replicatedapp/api.go b/pkg/replicatedapp/api.go index 7ac29c1f10..eebcad4878 100644 --- a/pkg/replicatedapp/api.go +++ b/pkg/replicatedapp/api.go @@ -40,8 +40,15 @@ type LicenseData struct { License *kotsv1beta1.License } -func GetLatestLicense(license *kotsv1beta1.License) (*LicenseData, error) { +// GetLatestLicense will return the latest license from the replicated api, if selectedChannelID is provided +// it will be passed along to the api. +func GetLatestLicense(license *kotsv1beta1.License, selectedChannelID string) (*LicenseData, error) { url := fmt.Sprintf("%s/license/%s", license.Spec.Endpoint, license.Spec.AppSlug) + + if selectedChannelID != "" { + url = fmt.Sprintf("%s?selectedChannelId=%s", url, selectedChannelID) + } + licenseData, err := getLicenseFromAPI(url, license.Spec.LicenseID) if err != nil { return nil, errors.Wrap(err, "failed to get license from api") diff --git a/pkg/replicatedapp/api_test.go b/pkg/replicatedapp/api_test.go index 4d81d0f891..07103e0bee 100644 --- a/pkg/replicatedapp/api_test.go +++ b/pkg/replicatedapp/api_test.go @@ -30,7 +30,7 @@ func Test_getRequest(t *testing.T) { channel: nil, channelSequence: "", versionLabel: nil, - expectedURL: "https://replicated-app/release/sluggy1?channelSequence=&isSemverSupported=true&licenseSequence=23", + expectedURL: "https://replicated-app/release/sluggy1?channelSequence=&isSemverSupported=true&licenseSequence=23&selectedChannelId=channel", }, { endpoint: "http://localhost:30016", @@ -38,7 +38,7 @@ func Test_getRequest(t *testing.T) { channel: &beta, channelSequence: "", versionLabel: nil, - expectedURL: "http://localhost:30016/release/sluggy2/beta?channelSequence=&isSemverSupported=true&licenseSequence=23", + expectedURL: "http://localhost:30016/release/sluggy2/beta?channelSequence=&isSemverSupported=true&licenseSequence=23&selectedChannelId=channel", }, { endpoint: "https://replicated-app", @@ -46,7 +46,7 @@ func Test_getRequest(t *testing.T) { channel: &unstable, channelSequence: "10", versionLabel: nil, - expectedURL: "https://replicated-app/release/sluggy3/unstable?channelSequence=10&isSemverSupported=true&licenseSequence=23", + expectedURL: "https://replicated-app/release/sluggy3/unstable?channelSequence=10&isSemverSupported=true&licenseSequence=23&selectedChannelId=channel", }, { endpoint: "https://replicated-app", @@ -54,7 +54,7 @@ func Test_getRequest(t *testing.T) { channel: &unstable, channelSequence: "", versionLabel: &version, - expectedURL: "https://replicated-app/release/sluggy3/unstable?channelSequence=&isSemverSupported=true&licenseSequence=23&versionLabel=1.1.0", + expectedURL: "https://replicated-app/release/sluggy3/unstable?channelSequence=&isSemverSupported=true&licenseSequence=23&selectedChannelId=channel&versionLabel=1.1.0", }, } @@ -65,6 +65,7 @@ func Test_getRequest(t *testing.T) { Endpoint: test.endpoint, AppSlug: test.appSlug, LicenseSequence: 23, + ChannelID: "channel", }, } r := &ReplicatedUpstream{ @@ -77,7 +78,7 @@ func Test_getRequest(t *testing.T) { if test.channel != nil { cursor.ChannelName = *test.channel } - request, err := r.GetRequest("GET", license, cursor) + request, err := r.GetRequest("GET", license, cursor, channel) req.NoError(err) assert.Equal(t, test.expectedURL, request.URL.String()) } diff --git a/pkg/replicatedapp/upstream.go b/pkg/replicatedapp/upstream.go index c96aa418b1..4f52d6ca25 100644 --- a/pkg/replicatedapp/upstream.go +++ b/pkg/replicatedapp/upstream.go @@ -41,7 +41,7 @@ func ParseReplicatedURL(u *url.URL) (*ReplicatedUpstream, error) { return &replicatedUpstream, nil } -func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.License, cursor ReplicatedCursor) (*http.Request, error) { +func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.License, cursor ReplicatedCursor, selectedChannelID string) (*http.Request, error) { u, err := url.Parse(license.Spec.Endpoint) if err != nil { return nil, errors.Wrap(err, "failed to parse endpoint from license") @@ -64,6 +64,7 @@ func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.Lice } urlValues.Add("licenseSequence", fmt.Sprintf("%d", license.Spec.LicenseSequence)) urlValues.Add("isSemverSupported", "true") + urlValues.Add("selectedChannelId", selectedChannelID) url := fmt.Sprintf("%s://%s?%s", u.Scheme, urlPath, urlValues.Encode()) diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 91dfd2be54..5ee1d75432 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -81,7 +81,7 @@ func getUpdatesReplicated(fetchOptions *types.FetchOptions) (*types.UpdateCheckR return nil, errors.New("No license was provided") } - pendingReleases, updateCheckTime, err := listPendingChannelReleases(fetchOptions.License, fetchOptions.LastUpdateCheckAt, currentCursor, fetchOptions.ChannelChanged, fetchOptions.SortOrder, fetchOptions.ReportingInfo) + pendingReleases, updateCheckTime, err := listPendingChannelReleases(fetchOptions.License, fetchOptions.LastUpdateCheckAt, currentCursor, fetchOptions.ChannelChanged, fetchOptions.SortOrder, fetchOptions.ReportingInfo, fetchOptions.CurrentChannelID) if err != nil { return nil, errors.Wrap(err, "failed to list replicated app releases") } @@ -131,7 +131,7 @@ func downloadReplicated( registry registrytypes.RegistrySettings, reportingInfo *reportingtypes.ReportingInfo, skipCompatibilityCheck bool, - appChannelID string, + appSelectedChannelID string, ) (*types.Upstream, error) { var release *Release @@ -169,12 +169,12 @@ func downloadReplicated( } } - downloadedRelease, err := downloadReplicatedApp(replicatedUpstream, license, updateCursor, reportingInfo) + downloadedRelease, err := downloadReplicatedApp(replicatedUpstream, license, updateCursor, reportingInfo, appSelectedChannelID) if err != nil { return nil, errors.Wrap(err, "failed to download replicated app") } - licenseData, err := replicatedapp.GetLatestLicense(license) + licenseData, err := replicatedapp.GetLatestLicense(license, appSelectedChannelID) if err != nil { return nil, errors.Wrap(err, "failed to get latest license") } @@ -205,7 +205,7 @@ func downloadReplicated( // get channel name from license, if one was provided channelID, channelName := "", "" if license != nil { - channel, err := kotsutil.FindChannelInLicense(appChannelID, license) + channel, err := kotsutil.FindChannelInLicense(appSelectedChannelID, license) if err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } @@ -349,8 +349,8 @@ func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp. return &release, nil } -func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, license *kotsv1beta1.License, cursor replicatedapp.ReplicatedCursor, reportingInfo *reportingtypes.ReportingInfo) (*Release, error) { - getReq, err := replicatedUpstream.GetRequest("GET", license, cursor) +func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, license *kotsv1beta1.License, cursor replicatedapp.ReplicatedCursor, reportingInfo *reportingtypes.ReportingInfo, selectedAppChannel string) (*Release, error) { + getReq, err := replicatedUpstream.GetRequest("GET", license, cursor, selectedAppChannel) if err != nil { return nil, errors.Wrap(err, "failed to create http request") } @@ -446,7 +446,7 @@ func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, return &release, nil } -func listPendingChannelReleases(license *kotsv1beta1.License, lastUpdateCheckAt *time.Time, currentCursor replicatedapp.ReplicatedCursor, channelChanged bool, sortOrder string, reportingInfo *reportingtypes.ReportingInfo) ([]ChannelRelease, *time.Time, error) { +func listPendingChannelReleases(license *kotsv1beta1.License, lastUpdateCheckAt *time.Time, currentCursor replicatedapp.ReplicatedCursor, channelChanged bool, sortOrder string, reportingInfo *reportingtypes.ReportingInfo, selectedChannelID string) ([]ChannelRelease, *time.Time, error) { u, err := url.Parse(license.Spec.Endpoint) if err != nil { return nil, nil, errors.Wrap(err, "failed to parse endpoint from license") @@ -466,6 +466,7 @@ func listPendingChannelReleases(license *kotsv1beta1.License, lastUpdateCheckAt urlValues.Set("channelSequence", sequence) urlValues.Add("licenseSequence", fmt.Sprintf("%d", license.Spec.LicenseSequence)) urlValues.Add("isSemverSupported", "true") + urlValues.Add("selectedChannelId", selectedChannelID) if lastUpdateCheckAt != nil { urlValues.Add("lastUpdateCheckAt", lastUpdateCheckAt.UTC().Format(time.RFC3339)) From ab0f4d7786d91a4ab76ffa3c37e9d4b6109a5303 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 31 Jul 2024 22:44:25 +0000 Subject: [PATCH 28/35] remove selectedAppChannel update from operator --- pkg/operator/operator.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index fd66675d64..de011fdaa2 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -1022,10 +1022,6 @@ func (o *Operator) reconcileDeployment(cm *corev1.ConfigMap) (finalError error) } } - if err := store.GetStore().SetAppSelectedChannelID(appID, cm.Data["channel-id"]); err != nil { - return errors.Wrap(err, "failed to set app channel id") - } - if err := o.store.SetAppChannelChanged(appID, false); err != nil { return errors.Wrap(err, "failed to reset channel changed flag") } From fb6e5519f563c997919222602719a6b2cc4a89dc Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Wed, 31 Jul 2024 23:30:24 +0000 Subject: [PATCH 29/35] Update selected_channel_id on every single channel license change --- pkg/store/kotsstore/license_store.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/store/kotsstore/license_store.go b/pkg/store/kotsstore/license_store.go index 3c2477c1d1..182590663f 100644 --- a/pkg/store/kotsstore/license_store.go +++ b/pkg/store/kotsstore/license_store.go @@ -121,11 +121,23 @@ func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDi return int64(0), errors.Wrap(err, "failed to write new license") } - // app has the original license data received from the server - statements = append(statements, gorqlite.ParameterizedStatement{ - Query: `update app set license = ?, last_license_sync = ?, channel_changed = ? where id = ?`, - Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, appID}, - }) + // If the license channels array has more than one entry, then the license is a true multi-channel license, + // and we should skip updating selected_channel_id in the app table. If there's only a single entry, + // we should update the selected_channel_id in the app table to ensure it stays consistent across channel + // changes. This is a temporary solution until channel changes on true multi-channel licenses are supported. + if len(newLicense.Spec.Channels) > 1 { + // app has the original license data received from the server + statements = append(statements, gorqlite.ParameterizedStatement{ + Query: `update app set license = ?, last_license_sync = ?, channel_changed = ? where id = ?`, + Arguments: []interface{}{encodedLicense, time.Now().Unix(), channelChanged, appID}, + }) + } else { + // app has the original license data received from the server + statements = append(statements, gorqlite.ParameterizedStatement{ + Query: `update app set license = ?, last_license_sync = ?, channel_changed = ?, selected_channel_id = ? where id = ?`, + Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, appID, newLicense.Spec.ChannelID}, + }) + } appVersionStatements, newSeq, err := s.createNewVersionForLicenseChangeStatements(appID, baseSequence, archiveDir, renderer, reportingInfo) if err != nil { From 7931526f36fa7ddb54b13be6c66fa3fc36e7dfd2 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Thu, 1 Aug 2024 15:47:10 +0000 Subject: [PATCH 30/35] Finish channel_id -> selected_channel_id rename --- cmd/kots/cli/pull.go | 2 +- pkg/airgap/airgap.go | 2 +- pkg/airgap/types/types.go | 10 ++-- pkg/automation/automation.go | 12 ++--- pkg/handlers/license.go | 22 ++++---- pkg/online/online.go | 2 +- pkg/online/types/types.go | 12 ++--- pkg/pull/pull.go | 4 +- pkg/registry/registry.go | 12 ++--- pkg/render/render.go | 40 +++++++-------- pkg/rewrite/rewrite.go | 50 +++++++++---------- pkg/store/kotsstore/airgap_store.go | 4 +- pkg/store/kotsstore/app_store.go | 10 ++-- pkg/tests/pull/cases/airgap/testcase.yaml | 2 +- .../pull/cases/configcontext/testcase.yaml | 2 +- .../pull/cases/customhostnames/testcase.yaml | 2 +- pkg/tests/pull/cases/kotskinds/testcase.yaml | 2 +- pkg/tests/pull/cases/multidoc/testcase.yaml | 2 +- .../pull/cases/needsconfig/testcase.yaml | 2 +- .../pull/cases/replicatedhelm/testcase.yaml | 2 +- .../cases/required-helm-values/testcase.yaml | 2 +- .../cases/samechartvariations/testcase.yaml | 2 +- pkg/tests/pull/cases/simple/testcase.yaml | 2 +- .../pull/cases/subchart-alias/testcase.yaml | 2 +- .../pull/cases/subchart-crds/testcase.yaml | 2 +- pkg/tests/pull/cases/subcharts/testcase.yaml | 2 +- .../taganddigest-norewrite/testcase.yaml | 2 +- .../cases/taganddigest-rewrite/testcase.yaml | 2 +- .../pull/cases/v1beta2-charts/testcase.yaml | 2 +- .../cases/outdated-kotskinds/testcase.yaml | 2 +- pkg/upstream/fetch.go | 2 +- pkg/upstream/fetch_test.go | 2 +- pkg/upstream/types/types.go | 2 +- 33 files changed, 111 insertions(+), 111 deletions(-) diff --git a/cmd/kots/cli/pull.go b/cmd/kots/cli/pull.go index a6d760eb19..d4dbdd0ba5 100644 --- a/cmd/kots/cli/pull.go +++ b/cmd/kots/cli/pull.go @@ -115,7 +115,7 @@ func PullCmd() *cobra.Command { if err != nil { return errors.Wrap(err, "failed to verify and update license") } - pullOptions.AppChannelID, err = kotsutil.FindChannelIDInLicense(preferredChannelSlug, license) + pullOptions.AppSelectedChannelID, err = kotsutil.FindChannelIDInLicense(preferredChannelSlug, license) if err != nil { // should never happen since we just verified the channel return errors.Wrap(err, "failed to find channel ID in license") } diff --git a/pkg/airgap/airgap.go b/pkg/airgap/airgap.go index a8d60be407..5600e0c9e5 100644 --- a/pkg/airgap/airgap.go +++ b/pkg/airgap/airgap.go @@ -229,7 +229,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) { AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: instParams.AppVersionLabel, - AppChannelID: opts.PendingApp.ChannelID, + AppSelectedChannelID: opts.PendingApp.SelectedChannelID, SkipCompatibilityCheck: opts.SkipCompatibilityCheck, } diff --git a/pkg/airgap/types/types.go b/pkg/airgap/types/types.go index 3e15e9be16..6927d1e8ed 100644 --- a/pkg/airgap/types/types.go +++ b/pkg/airgap/types/types.go @@ -1,11 +1,11 @@ package types type PendingApp struct { - ID string - Slug string - Name string - LicenseData string - ChannelID string + ID string + Slug string + Name string + LicenseData string + SelectedChannelID string } type InstallStatus struct { diff --git a/pkg/automation/automation.go b/pkg/automation/automation.go index e38e2c16dd..04b1e9f30b 100644 --- a/pkg/automation/automation.go +++ b/pkg/automation/automation.go @@ -316,12 +316,12 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1. } else if annotations["kots.io/airgap"] != "true" { createAppOpts := online.CreateOnlineAppOpts{ PendingApp: &onlinetypes.PendingApp{ - ID: a.ID, - Slug: a.Slug, - Name: a.Name, - LicenseData: string(license), - VersionLabel: instParams.AppVersionLabel, - ChannelID: a.SelectedChannelID, + ID: a.ID, + Slug: a.Slug, + Name: a.Name, + LicenseData: string(license), + VersionLabel: instParams.AppVersionLabel, + SelectedChannelID: a.SelectedChannelID, }, UpstreamURI: upstreamURI, IsAutomated: true, diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index d903ce8a17..a928efb010 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -355,12 +355,12 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) { // complete the install online createAppOpts := online.CreateOnlineAppOpts{ PendingApp: &installationtypes.PendingApp{ - ID: a.ID, - Slug: a.Slug, - Name: a.Name, - ChannelID: a.SelectedChannelID, - LicenseData: uploadLicenseRequest.LicenseData, - VersionLabel: installationParams.AppVersionLabel, + ID: a.ID, + Slug: a.Slug, + Name: a.Name, + SelectedChannelID: a.SelectedChannelID, + LicenseData: uploadLicenseRequest.LicenseData, + VersionLabel: installationParams.AppVersionLabel, }, UpstreamURI: upstreamURI, } @@ -438,11 +438,11 @@ func (h *Handler) ResumeInstallOnline(w http.ResponseWriter, r *http.Request) { } pendingApp := installationtypes.PendingApp{ - ID: a.ID, - Slug: a.Slug, - Name: a.Name, - VersionLabel: installationParams.AppVersionLabel, - ChannelID: a.SelectedChannelID, + ID: a.ID, + Slug: a.Slug, + Name: a.Name, + VersionLabel: installationParams.AppVersionLabel, + SelectedChannelID: a.SelectedChannelID, } // the license data is left in the table diff --git a/pkg/online/online.go b/pkg/online/online.go index 3d6751526e..74a8ce6bbf 100644 --- a/pkg/online/online.go +++ b/pkg/online/online.go @@ -155,7 +155,7 @@ func CreateAppFromOnline(opts CreateOnlineAppOpts) (_ *kotsutil.KotsKinds, final AppSlug: opts.PendingApp.Slug, AppSequence: 0, AppVersionLabel: opts.PendingApp.VersionLabel, - AppChannelID: opts.PendingApp.ChannelID, + AppSelectedChannelID: opts.PendingApp.SelectedChannelID, ReportingInfo: reporting.GetReportingInfo(opts.PendingApp.ID), SkipCompatibilityCheck: opts.SkipCompatibilityCheck, } diff --git a/pkg/online/types/types.go b/pkg/online/types/types.go index cc812568b0..3858abd4dc 100644 --- a/pkg/online/types/types.go +++ b/pkg/online/types/types.go @@ -1,12 +1,12 @@ package types type PendingApp struct { - ID string - Slug string - Name string - LicenseData string - VersionLabel string - ChannelID string + ID string + Slug string + Name string + LicenseData string + VersionLabel string + SelectedChannelID string } type InstallStatus struct { diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go index 04c6c46ea2..d35133e034 100644 --- a/pkg/pull/pull.go +++ b/pkg/pull/pull.go @@ -68,7 +68,7 @@ type PullOptions struct { AppSlug string AppSequence int64 AppVersionLabel string - AppChannelID string + AppSelectedChannelID string IsGitOps bool StorageClassName string HTTPProxyEnvValue string @@ -132,7 +132,7 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) { AppSlug: pullOptions.AppSlug, AppSequence: pullOptions.AppSequence, AppVersionLabel: pullOptions.AppVersionLabel, - AppChannelID: pullOptions.AppChannelID, + AppSelectedChannelID: pullOptions.AppSelectedChannelID, LocalRegistry: pullOptions.RewriteImageOptions, ReportingInfo: pullOptions.ReportingInfo, SkipCompatibilityCheck: pullOptions.SkipCompatibilityCheck, diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 6fb28305b6..1a1f76519f 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -145,12 +145,12 @@ func RewriteImages(appID string, sequence int64, hostname string, username strin Password: password, IsReadOnly: isReadOnly, }, - AppID: a.ID, - AppSlug: a.Slug, - AppChannelID: a.SelectedChannelID, - IsGitOps: a.IsGitOps, - AppSequence: nextAppSequence, - ReportingInfo: reporting.GetReportingInfo(a.ID), + AppID: a.ID, + AppSlug: a.Slug, + AppSelectedChannelID: a.SelectedChannelID, + IsGitOps: a.IsGitOps, + AppSequence: nextAppSequence, + ReportingInfo: reporting.GetReportingInfo(a.ID), // TODO: pass in as arguments if this is ever called from CLI HTTPProxyEnvValue: os.Getenv("HTTP_PROXY"), diff --git a/pkg/render/render.go b/pkg/render/render.go index 0bc2fc8b63..e49e89bfef 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -126,26 +126,26 @@ func RenderDir(opts types.RenderDirOptions) error { appNamespace = os.Getenv("KOTSADM_TARGET_NAMESPACE") } reOptions := rewrite.RewriteOptions{ - RootDir: opts.ArchiveDir, - UpstreamURI: fmt.Sprintf("replicated://%s", license.Spec.AppSlug), - UpstreamPath: filepath.Join(opts.ArchiveDir, "upstream"), - Installation: installation, - Downstreams: downstreamNames, - Silent: true, - CreateAppDir: false, - ExcludeKotsKinds: true, - License: license, - ConfigValues: configValues, - K8sNamespace: appNamespace, - CopyImages: false, - IsAirgap: opts.App.IsAirgap, - AppID: opts.App.ID, - AppSlug: opts.App.Slug, - AppChannelID: opts.App.SelectedChannelID, - IsGitOps: opts.App.IsGitOps, - AppSequence: opts.Sequence, - ReportingInfo: opts.ReportingInfo, - RegistrySettings: opts.RegistrySettings, + RootDir: opts.ArchiveDir, + UpstreamURI: fmt.Sprintf("replicated://%s", license.Spec.AppSlug), + UpstreamPath: filepath.Join(opts.ArchiveDir, "upstream"), + Installation: installation, + Downstreams: downstreamNames, + Silent: true, + CreateAppDir: false, + ExcludeKotsKinds: true, + License: license, + ConfigValues: configValues, + K8sNamespace: appNamespace, + CopyImages: false, + IsAirgap: opts.App.IsAirgap, + AppID: opts.App.ID, + AppSlug: opts.App.Slug, + AppSelectedChannelID: opts.App.SelectedChannelID, + IsGitOps: opts.App.IsGitOps, + AppSequence: opts.Sequence, + ReportingInfo: opts.ReportingInfo, + RegistrySettings: opts.RegistrySettings, // TODO: pass in as arguments if this is ever called from CLI HTTPProxyEnvValue: os.Getenv("HTTP_PROXY"), diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go index b87cac98a2..c8390cd8e3 100644 --- a/pkg/rewrite/rewrite.go +++ b/pkg/rewrite/rewrite.go @@ -29,30 +29,30 @@ import ( ) type RewriteOptions struct { - RootDir string - UpstreamURI string - UpstreamPath string - Downstreams []string - K8sNamespace string - Silent bool - CreateAppDir bool - ExcludeKotsKinds bool - Installation *kotsv1beta1.Installation - License *kotsv1beta1.License - ConfigValues *kotsv1beta1.ConfigValues - ReportWriter io.Writer - CopyImages bool // can be false even if registry is not read-only - IsAirgap bool - RegistrySettings registrytypes.RegistrySettings - AppID string - AppSlug string - AppChannelID string - IsGitOps bool - AppSequence int64 - ReportingInfo *reportingtypes.ReportingInfo - HTTPProxyEnvValue string - HTTPSProxyEnvValue string - NoProxyEnvValue string + RootDir string + UpstreamURI string + UpstreamPath string + Downstreams []string + K8sNamespace string + Silent bool + CreateAppDir bool + ExcludeKotsKinds bool + Installation *kotsv1beta1.Installation + License *kotsv1beta1.License + ConfigValues *kotsv1beta1.ConfigValues + ReportWriter io.Writer + CopyImages bool // can be false even if registry is not read-only + IsAirgap bool + RegistrySettings registrytypes.RegistrySettings + AppID string + AppSlug string + AppSelectedChannelID string + IsGitOps bool + AppSequence int64 + ReportingInfo *reportingtypes.ReportingInfo + HTTPProxyEnvValue string + HTTPSProxyEnvValue string + NoProxyEnvValue string } func Rewrite(rewriteOptions RewriteOptions) error { @@ -82,7 +82,7 @@ func Rewrite(rewriteOptions RewriteOptions) error { License: rewriteOptions.License, AppSequence: rewriteOptions.AppSequence, AppSlug: rewriteOptions.AppSlug, - AppChannelID: rewriteOptions.AppChannelID, + AppSelectedChannelID: rewriteOptions.AppSelectedChannelID, LocalRegistry: rewriteOptions.RegistrySettings, ReportingInfo: rewriteOptions.ReportingInfo, SkipCompatibilityCheck: true, // we're rewriting an existing version, no need to check for compatibility diff --git a/pkg/store/kotsstore/airgap_store.go b/pkg/store/kotsstore/airgap_store.go index 8571759b2e..54e38c1c80 100644 --- a/pkg/store/kotsstore/airgap_store.go +++ b/pkg/store/kotsstore/airgap_store.go @@ -28,7 +28,7 @@ func (s *KOTSStore) GetPendingAirgapUploadApp() (*airgaptypes.PendingApp, error) return nil, errors.Wrap(err, "failed to scan pending app id") } - query = `select id, slug, name, license, channel_id from app where id = ?` + query = `select id, slug, name, license, selected_channel_id from app where id = ?` rows, err = db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{id}, @@ -41,7 +41,7 @@ func (s *KOTSStore) GetPendingAirgapUploadApp() (*airgaptypes.PendingApp, error) } pendingApp := airgaptypes.PendingApp{} - if err := rows.Scan(&pendingApp.ID, &pendingApp.Slug, &pendingApp.Name, &pendingApp.LicenseData, &pendingApp.ChannelID); err != nil { + if err := rows.Scan(&pendingApp.ID, &pendingApp.Slug, &pendingApp.Name, &pendingApp.LicenseData, &pendingApp.SelectedChannelID); err != nil { return nil, errors.Wrap(err, "failed to scan pending app") } diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index a388a2d866..df0395cef6 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -147,7 +147,7 @@ func (s *KOTSStore) GetAppIDFromSlug(slug string) (string, error) { func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { db := persistence.MustGetDBSession() - query := `select id, name, license, upstream_uri, icon_uri, created_at, updated_at, slug, current_sequence, last_update_check_at, last_license_sync, is_airgap, snapshot_ttl_new, snapshot_schedule, restore_in_progress_name, restore_undeploy_status, update_checker_spec, semver_auto_deploy, install_state, channel_changed, channel_id from app where id = ?` + query := `select id, name, license, upstream_uri, icon_uri, created_at, updated_at, slug, current_sequence, last_update_check_at, last_license_sync, is_airgap, snapshot_ttl_new, snapshot_schedule, restore_in_progress_name, restore_undeploy_status, update_checker_spec, semver_auto_deploy, install_state, channel_changed, selected_channel_id from app where id = ?` rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ Query: query, Arguments: []interface{}{id}, @@ -282,11 +282,11 @@ func (s *KOTSStore) GetAppFromSlug(slug string) (*apptypes.App, error) { return s.GetApp(id) } -func (s *KOTSStore) CreateApp(name string, channelID string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) { +func (s *KOTSStore) CreateApp(name string, selectedChannelID string, upstreamURI string, licenseData string, isAirgapEnabled bool, skipImagePush bool, registryIsReadOnly bool) (*apptypes.App, error) { logger.Debug("creating app", zap.String("name", name), zap.String("upstreamURI", upstreamURI), - zap.String("channelID", channelID), + zap.String("selectedChannelID", selectedChannelID), ) db := persistence.MustGetDBSession() @@ -342,10 +342,10 @@ func (s *KOTSStore) CreateApp(name string, channelID string, upstreamURI string, id := ksuid.New().String() - query := `insert into app (id, name, icon_uri, created_at, slug, upstream_uri, license, is_all_users, install_state, registry_is_readonly, channel_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + query := `insert into app (id, name, icon_uri, created_at, slug, upstream_uri, license, is_all_users, install_state, registry_is_readonly, selected_channel_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ Query: query, - Arguments: []interface{}{id, name, "", time.Now().Unix(), slugProposal, upstreamURI, licenseData, true, installState, registryIsReadOnly, channelID}, + Arguments: []interface{}{id, name, "", time.Now().Unix(), slugProposal, upstreamURI, licenseData, true, installState, registryIsReadOnly, selectedChannelID}, }) if err != nil { return nil, fmt.Errorf("failed to insert app: %v: %v", err, wr.Err) diff --git a/pkg/tests/pull/cases/airgap/testcase.yaml b/pkg/tests/pull/cases/airgap/testcase.yaml index e692e0491a..b597227ffa 100644 --- a/pkg/tests/pull/cases/airgap/testcase.yaml +++ b/pkg/tests/pull/cases/airgap/testcase.yaml @@ -16,4 +16,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/configcontext/testcase.yaml b/pkg/tests/pull/cases/configcontext/testcase.yaml index 3028be9469..3d5440c8f1 100644 --- a/pkg/tests/pull/cases/configcontext/testcase.yaml +++ b/pkg/tests/pull/cases/configcontext/testcase.yaml @@ -14,4 +14,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/customhostnames/testcase.yaml b/pkg/tests/pull/cases/customhostnames/testcase.yaml index a674006680..1db21dc494 100644 --- a/pkg/tests/pull/cases/customhostnames/testcase.yaml +++ b/pkg/tests/pull/cases/customhostnames/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/kotskinds/testcase.yaml b/pkg/tests/pull/cases/kotskinds/testcase.yaml index 74ac8eb6d2..82e589a62f 100644 --- a/pkg/tests/pull/cases/kotskinds/testcase.yaml +++ b/pkg/tests/pull/cases/kotskinds/testcase.yaml @@ -10,4 +10,4 @@ PullOptions: Downstreams: - this-cluster SkipHelmChartCheck: true - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/multidoc/testcase.yaml b/pkg/tests/pull/cases/multidoc/testcase.yaml index e079057b94..02d3eff940 100644 --- a/pkg/tests/pull/cases/multidoc/testcase.yaml +++ b/pkg/tests/pull/cases/multidoc/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/needsconfig/testcase.yaml b/pkg/tests/pull/cases/needsconfig/testcase.yaml index 14c185060f..90d8083e0a 100644 --- a/pkg/tests/pull/cases/needsconfig/testcase.yaml +++ b/pkg/tests/pull/cases/needsconfig/testcase.yaml @@ -10,4 +10,4 @@ PullOptions: Downstreams: - this-cluster SkipHelmChartCheck: true - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/replicatedhelm/testcase.yaml b/pkg/tests/pull/cases/replicatedhelm/testcase.yaml index 9847d0bf3c..bc256e88c8 100644 --- a/pkg/tests/pull/cases/replicatedhelm/testcase.yaml +++ b/pkg/tests/pull/cases/replicatedhelm/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/required-helm-values/testcase.yaml b/pkg/tests/pull/cases/required-helm-values/testcase.yaml index 68f359fade..64fb4f5238 100644 --- a/pkg/tests/pull/cases/required-helm-values/testcase.yaml +++ b/pkg/tests/pull/cases/required-helm-values/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/samechartvariations/testcase.yaml b/pkg/tests/pull/cases/samechartvariations/testcase.yaml index 5112be1431..35210f96e5 100644 --- a/pkg/tests/pull/cases/samechartvariations/testcase.yaml +++ b/pkg/tests/pull/cases/samechartvariations/testcase.yaml @@ -16,4 +16,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1YHCrcZzBxY2nJF5kcTCN9PHpk0 \ No newline at end of file + AppSelectedChannelID: 1YHCrcZzBxY2nJF5kcTCN9PHpk0 \ No newline at end of file diff --git a/pkg/tests/pull/cases/simple/testcase.yaml b/pkg/tests/pull/cases/simple/testcase.yaml index 16134bc0ed..3d4dd74c6e 100644 --- a/pkg/tests/pull/cases/simple/testcase.yaml +++ b/pkg/tests/pull/cases/simple/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subchart-alias/testcase.yaml b/pkg/tests/pull/cases/subchart-alias/testcase.yaml index 41817e2614..848952c4bb 100644 --- a/pkg/tests/pull/cases/subchart-alias/testcase.yaml +++ b/pkg/tests/pull/cases/subchart-alias/testcase.yaml @@ -14,4 +14,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subchart-crds/testcase.yaml b/pkg/tests/pull/cases/subchart-crds/testcase.yaml index cf73ee0ed0..811851da1e 100644 --- a/pkg/tests/pull/cases/subchart-crds/testcase.yaml +++ b/pkg/tests/pull/cases/subchart-crds/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/subcharts/testcase.yaml b/pkg/tests/pull/cases/subcharts/testcase.yaml index f3985532ae..e679f3b89b 100644 --- a/pkg/tests/pull/cases/subcharts/testcase.yaml +++ b/pkg/tests/pull/cases/subcharts/testcase.yaml @@ -14,4 +14,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml b/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml index fcdb47d101..e9ae529d29 100644 --- a/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml +++ b/pkg/tests/pull/cases/taganddigest-norewrite/testcase.yaml @@ -9,4 +9,4 @@ PullOptions: RewriteImages: false Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml b/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml index aa9968cf32..2fdd9059e1 100644 --- a/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml +++ b/pkg/tests/pull/cases/taganddigest-rewrite/testcase.yaml @@ -15,4 +15,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml b/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml index 8a5b7e1213..9d67049213 100644 --- a/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml +++ b/pkg/tests/pull/cases/v1beta2-charts/testcase.yaml @@ -15,4 +15,4 @@ PullOptions: IsReadOnly: true Downstreams: - this-cluster - AppChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file + AppSelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 \ No newline at end of file diff --git a/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml b/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml index f5ae296ebf..db0ea96ffd 100644 --- a/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml +++ b/pkg/tests/renderdir/cases/outdated-kotskinds/testcase.yaml @@ -4,7 +4,7 @@ RenderDirOptions: App: ID: app-id Slug: my-app - ChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 + SelectedChannelID: 1vusIYZLAVxMG6q760OJmRKj5i5 Downstreams: - Name: this-cluster Sequence: 1 \ No newline at end of file diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go index 09d96981ac..2fe0b075fb 100644 --- a/pkg/upstream/fetch.go +++ b/pkg/upstream/fetch.go @@ -60,7 +60,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *types.FetchOptions) (*ty fetchOptions.LocalRegistry, fetchOptions.ReportingInfo, fetchOptions.SkipCompatibilityCheck, - fetchOptions.AppChannelID, + fetchOptions.AppSelectedChannelID, ) } diff --git a/pkg/upstream/fetch_test.go b/pkg/upstream/fetch_test.go index a958e1e65a..9e54aa565d 100644 --- a/pkg/upstream/fetch_test.go +++ b/pkg/upstream/fetch_test.go @@ -91,7 +91,7 @@ ACgAAA==`, }, }, }, - AppChannelID: "channel-2", + AppSelectedChannelID: "channel-2", } u, err := FetchUpstream("replicated://app-slug", fetchOptions) req.NoError(err) diff --git a/pkg/upstream/types/types.go b/pkg/upstream/types/types.go index 8a647b82e1..169d10a0fc 100644 --- a/pkg/upstream/types/types.go +++ b/pkg/upstream/types/types.go @@ -115,7 +115,7 @@ type FetchOptions struct { LocalRegistry registrytypes.RegistrySettings ReportingInfo *reportingtypes.ReportingInfo SkipCompatibilityCheck bool - AppChannelID string + AppSelectedChannelID string } func (u *Upstream) GetUpstreamDir(options WriteOptions) string { From 1bebcb790c6ce79e42687564e2082db67932da3b Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 2 Aug 2024 04:04:14 +0000 Subject: [PATCH 31/35] centralized backfill and clean up - only using license chan where approporiate --- pkg/handlers/license.go | 9 ++----- pkg/handlers/update_checker_spec.go | 5 ++-- pkg/kotsutil/kots.go | 1 + pkg/store/kotsstore/app_store.go | 32 ++++++++++++------------- pkg/store/kotsstore/downstream_store.go | 16 +++---------- pkg/store/kotsstore/license_store.go | 5 ++-- pkg/store/kotsstore/version_store.go | 7 +----- pkg/store/mock/mock.go | 30 ----------------------- pkg/store/store_interface.go | 1 - pkg/update/required.go | 2 +- pkg/update/update.go | 5 ++-- pkg/update/update_test.go | 28 ++++------------------ pkg/updatechecker/updatechecker.go | 6 ++--- pkg/upstream/replicated.go | 7 +++++- 14 files changed, 43 insertions(+), 111 deletions(-) diff --git a/pkg/handlers/license.go b/pkg/handlers/license.go index a928efb010..a3658d6e44 100644 --- a/pkg/handlers/license.go +++ b/pkg/handlers/license.go @@ -680,15 +680,10 @@ func licenseResponseFromLicense(license *kotsv1beta1.License, app *apptypes.App) return entitlements[i].Title < entitlements[j].Title }) - channel, err := kotsutil.FindChannelInLicense(app.SelectedChannelID, license) - if err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } - response := LicenseResponse{ ID: license.Spec.LicenseID, Assignee: license.Spec.CustomerName, - ChannelName: channel.ChannelName, + ChannelName: license.Spec.ChannelName, LicenseSequence: license.Spec.LicenseSequence, LicenseType: license.Spec.LicenseType, Entitlements: entitlements, @@ -697,7 +692,7 @@ func licenseResponseFromLicense(license *kotsv1beta1.License, app *apptypes.App) IsGitOpsSupported: license.Spec.IsGitOpsSupported, IsIdentityServiceSupported: license.Spec.IsIdentityServiceSupported, IsGeoaxisSupported: license.Spec.IsGeoaxisSupported, - IsSemverRequired: channel.IsSemverRequired, + IsSemverRequired: license.Spec.IsSemverRequired, IsSnapshotSupported: license.Spec.IsSnapshotSupported, IsDisasterRecoverySupported: license.Spec.IsDisasterRecoverySupported, LastSyncedAt: app.LastLicenseSync, diff --git a/pkg/handlers/update_checker_spec.go b/pkg/handlers/update_checker_spec.go index 6f76581419..d281fbed7d 100644 --- a/pkg/handlers/update_checker_spec.go +++ b/pkg/handlers/update_checker_spec.go @@ -56,14 +56,13 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque return } - licenseChan, err := store.GetStore().GetOrBackfillLicenseChannel(foundApp.ID, license) + licenseChan, err := kotsutil.FindChannelInLicense(foundApp.SelectedChannelID, license) if err != nil { - updateCheckerSpecResponse.Error = "failed to backfill app channel id from license" + updateCheckerSpecResponse.Error = "failed to find app channel id from license" logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error)) JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse) return } - foundApp.SelectedChannelID = licenseChan.ChannelID // Check if the deploy update configuration is valid based on app channel if licenseChan.IsSemverRequired { diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 70f7cf223e..1252f62b57 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -1649,6 +1649,7 @@ func FindChannelInLicense(channelID string, license *kotsv1beta1.License) (*kots } } + logger.Warnf("channel id '%s' not found in multi channel license with sequence", channelID, license.Spec.LicenseSequence) return nil, errors.New("channel not found in multi channel format license") } diff --git a/pkg/store/kotsstore/app_store.go b/pkg/store/kotsstore/app_store.go index df0395cef6..e3e6dd8dd7 100644 --- a/pkg/store/kotsstore/app_store.go +++ b/pkg/store/kotsstore/app_store.go @@ -18,6 +18,7 @@ import ( "github.com/rqlite/gorqlite" "github.com/segmentio/ksuid" "go.uber.org/zap" + "k8s.io/client-go/kubernetes/scheme" ) func (s *KOTSStore) AddAppToAllDownstreams(appID string) error { @@ -270,6 +271,20 @@ func (s *KOTSStore) GetApp(id string) (*apptypes.App, error) { } app.IsGitOps = isGitOps + if app.SelectedChannelID == "" { + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode([]byte(licenseStr.String), nil, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to decode license yaml") + } + license := obj.(*kotsv1beta1.License) + licenseChan, err := s.backfillChannelIDFromLicense(app.ID, license) + if err != nil { + return nil, errors.Wrap(err, "failed to backfill channel id") + } + app.SelectedChannelID = licenseChan.ChannelID + } + return &app, nil } @@ -646,20 +661,3 @@ func (s *KOTSStore) backfillChannelIDFromLicense(appID string, license *kotsv1be } return kotsutil.FindChannelInLicense(backfillID, license) } - -func (s *KOTSStore) GetOrBackfillLicenseChannel(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) { - foundChannelID, err := s.GetAppSelectedChannelID(appID) - if err != nil { - return nil, errors.Wrap(err, "failed to get app channel id") - } - - if foundChannelID == "" { - return s.backfillChannelIDFromLicense(appID, license) - } - - licenseChan, err := kotsutil.FindChannelInLicense(foundChannelID, license) - if err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") - } - return licenseChan, nil -} diff --git a/pkg/store/kotsstore/downstream_store.go b/pkg/store/kotsstore/downstream_store.go index 1b6f298a8e..b69db3cf9a 100644 --- a/pkg/store/kotsstore/downstream_store.go +++ b/pkg/store/kotsstore/downstream_store.go @@ -410,12 +410,7 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo return nil, errors.Wrap(err, "failed to get app license") } - licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) - if err != nil { - return nil, errors.Wrap(err, "failed to get or backfill channel") - } - - downstreamtypes.SortDownstreamVersions(result.AllVersions, licenseChan.IsSemverRequired) + downstreamtypes.SortDownstreamVersions(result.AllVersions, license.Spec.IsSemverRequired) // retrieve additional details about the latest downloaded version, // since it's used for detecting things like if a certain feature is enabled or not. @@ -428,7 +423,7 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo if err := s.AddDownstreamVersionDetails(appID, clusterID, v, false); err != nil { return nil, errors.Wrap(err, "failed to add details to latest downloaded version") } - v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, result, licenseChan.IsSemverRequired) + v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, result, license.Spec.IsSemverRequired) break } @@ -680,13 +675,8 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string, return errors.Wrap(err, "failed to get app license") } - licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) - if err != nil { - return errors.Wrap(err, "failed to get or backfill channel") - } - for _, v := range versions { - v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, allVersions, licenseChan.IsSemverRequired) + v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, allVersions, license.Spec.IsSemverRequired) } } diff --git a/pkg/store/kotsstore/license_store.go b/pkg/store/kotsstore/license_store.go index 182590663f..0e0bc404d3 100644 --- a/pkg/store/kotsstore/license_store.go +++ b/pkg/store/kotsstore/license_store.go @@ -126,16 +126,17 @@ func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDi // we should update the selected_channel_id in the app table to ensure it stays consistent across channel // changes. This is a temporary solution until channel changes on true multi-channel licenses are supported. if len(newLicense.Spec.Channels) > 1 { + logger.Debug("Skipping selected_channel_id update for multi-channel license") // app has the original license data received from the server statements = append(statements, gorqlite.ParameterizedStatement{ Query: `update app set license = ?, last_license_sync = ?, channel_changed = ? where id = ?`, - Arguments: []interface{}{encodedLicense, time.Now().Unix(), channelChanged, appID}, + Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, appID}, }) } else { // app has the original license data received from the server statements = append(statements, gorqlite.ParameterizedStatement{ Query: `update app set license = ?, last_license_sync = ?, channel_changed = ?, selected_channel_id = ? where id = ?`, - Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, appID, newLicense.Spec.ChannelID}, + Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, newLicense.Spec.ChannelID, appID}, }) } diff --git a/pkg/store/kotsstore/version_store.go b/pkg/store/kotsstore/version_store.go index 9c4b20b5a1..98b4da9160 100644 --- a/pkg/store/kotsstore/version_store.go +++ b/pkg/store/kotsstore/version_store.go @@ -292,14 +292,9 @@ func (s *KOTSStore) GetAppVersionBaseSequence(appID string, versionLabel string) return -1, errors.Wrap(err, "failed to get app license") } - licenseChan, err := s.GetOrBackfillLicenseChannel(appID, license) - if err != nil { - return -1, errors.Wrap(err, "failed to get or backfill channel") - } - // add to the top of the list and sort appVersions.AllVersions = append([]*downstreamtypes.DownstreamVersion{mockVersion}, appVersions.AllVersions...) - downstreamtypes.SortDownstreamVersions(appVersions.AllVersions, licenseChan.IsSemverRequired) + downstreamtypes.SortDownstreamVersions(appVersions.AllVersions, license.Spec.IsSemverRequired) var baseVersion *downstreamtypes.DownstreamVersion for i, v := range appVersions.AllVersions { diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 9bd37e73a2..9d696b3afd 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -897,21 +897,6 @@ func (mr *MockStoreMockRecorder) GetNextAppSequence(appID interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextAppSequence", reflect.TypeOf((*MockStore)(nil).GetNextAppSequence), appID) } -// GetOrBackfillLicenseChannel mocks base method. -func (m *MockStore) GetOrBackfillLicenseChannel(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOrBackfillLicenseChannel", appID, license) - ret0, _ := ret[0].(*v1beta10.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetOrBackfillLicenseChannel indicates an expected call of GetOrBackfillLicenseChannel. -func (mr *MockStoreMockRecorder) GetOrBackfillLicenseChannel(appID, license interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrBackfillLicenseChannel", reflect.TypeOf((*MockStore)(nil).GetOrBackfillLicenseChannel), appID, license) -} - // GetParentSequenceForSequence mocks base method. func (m *MockStore) GetParentSequenceForSequence(appID, clusterID string, sequence int64) (int64, error) { m.ctrl.T.Helper() @@ -2783,21 +2768,6 @@ func (mr *MockAppStoreMockRecorder) GetDownstream(clusterID interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownstream", reflect.TypeOf((*MockAppStore)(nil).GetDownstream), clusterID) } -// GetOrBackfillLicenseChannel mocks base method. -func (m *MockAppStore) GetOrBackfillLicenseChannel(appID string, license *v1beta10.License) (*v1beta10.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOrBackfillLicenseChannel", appID, license) - ret0, _ := ret[0].(*v1beta10.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetOrBackfillLicenseChannel indicates an expected call of GetOrBackfillLicenseChannel. -func (mr *MockAppStoreMockRecorder) GetOrBackfillLicenseChannel(appID, license interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrBackfillLicenseChannel", reflect.TypeOf((*MockAppStore)(nil).GetOrBackfillLicenseChannel), appID, license) -} - // IsGitOpsEnabledForApp mocks base method. func (m *MockAppStore) IsGitOpsEnabledForApp(appID string) (bool, error) { m.ctrl.T.Helper() diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 4a07e43cb2..c487980c64 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -132,7 +132,6 @@ type AppStore interface { RemoveApp(appID string) error SetAppChannelChanged(appID string, channelChanged bool) error SetAppSelectedChannelID(appID string, channelID string) error - GetOrBackfillLicenseChannel(appID string, license *kotsv1beta1.License) (*kotsv1beta1.Channel, error) } type DownstreamStore interface { diff --git a/pkg/update/required.go b/pkg/update/required.go index 8adddad47f..36e3ef6e37 100644 --- a/pkg/update/required.go +++ b/pkg/update/required.go @@ -66,7 +66,7 @@ func getRequiredAirgapUpdates(airgap *kotsv1beta1.Airgap, license *kotsv1beta1.L licenseChan, err := kotsutil.FindChannelInLicense(selectedChannelID, license) if err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + return nil, errors.Wrap(err, "failed to find channel in license during") } // semvers can be compared across channels diff --git a/pkg/update/update.go b/pkg/update/update.go index 81b900893d..c1c1de0a6b 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -34,11 +34,10 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *k var err error var licenseChan *kotsv1beta1.Channel - licenseChan, err = kotsStore.GetOrBackfillLicenseChannel(app.ID, license) + licenseChan, err = kotsutil.FindChannelInLicense(app.SelectedChannelID, license) if err != nil { - return nil, errors.Wrap(err, "failed to backfill channel id from license") + return nil, errors.Wrap(err, "failed to find channel in license") } - app.SelectedChannelID = licenseChan.ChannelID updateCursor, err := kotsStore.GetCurrentUpdateCursor(app.ID, licenseChan.ChannelID) if err != nil { diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index ba5ad644e4..6f29cd6258 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -9,7 +9,6 @@ import ( "github.com/golang/mock/gomock" apptypes "github.com/replicatedhq/kots/pkg/app/types" - "github.com/replicatedhq/kots/pkg/kotsutil" storepkg "github.com/replicatedhq/kots/pkg/store" mock_store "github.com/replicatedhq/kots/pkg/store/mock" "github.com/replicatedhq/kots/pkg/update/types" @@ -45,7 +44,7 @@ func TestGetAvailableUpdates(t *testing.T) { kotsStore: mockStore, app: &apptypes.App{ ID: "app-id", - SelectedChannelID: "", // using legacy non-multi chan license + SelectedChannelID: "channel-id", }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -60,11 +59,6 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) - mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( - licenseChan, - nil, - ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, @@ -76,7 +70,7 @@ func TestGetAvailableUpdates(t *testing.T) { kotsStore: mockStore, app: &apptypes.App{ ID: "app-id", - SelectedChannelID: "", // using legacy non-multi chan license + SelectedChannelID: "channel-id", }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -108,11 +102,6 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) - mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( - licenseChan, - nil, - ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{ @@ -143,7 +132,8 @@ func TestGetAvailableUpdates(t *testing.T) { args: args{ kotsStore: mockStore, app: &apptypes.App{ - ID: "app-id", + ID: "app-id", + SelectedChannelID: "channel-id", }, license: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -158,11 +148,6 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - licenseChan, _ := kotsutil.FindChannelInLicense("channel-id", args.license) - mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( - licenseChan, - nil, - ) // expect a backfill mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, @@ -201,11 +186,6 @@ func TestGetAvailableUpdates(t *testing.T) { setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint - licenseChan, _ := kotsutil.FindChannelInLicense("channel-id2", args.license) - mockStore.EXPECT().GetOrBackfillLicenseChannel(args.app.ID, args.license).Return( - licenseChan, - nil, - ) mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.Channels[1].ChannelID).Return("1", nil) }, want: []types.AvailableUpdate{}, diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index 261599b11f..c0c7437545 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -13,6 +13,7 @@ import ( apptypes "github.com/replicatedhq/kots/pkg/app/types" license "github.com/replicatedhq/kots/pkg/kotsadmlicense" upstream "github.com/replicatedhq/kots/pkg/kotsadmupstream" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/preflight" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" @@ -228,11 +229,10 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- } var licenseChan *kotsv1beta1.Channel - licenseChan, err = store.GetOrBackfillLicenseChannel(a.ID, latestLicense) + licenseChan, err = kotsutil.FindChannelInLicense(a.SelectedChannelID, latestLicense) if err != nil { - return nil, errors.Wrap(err, "failed to backfill channel id from license") + return nil, errors.Wrap(err, "failed to find channel in license after sync") } - a.SelectedChannelID = licenseChan.ChannelID updateCursor, err := store.GetCurrentUpdateCursor(a.ID, licenseChan.ChannelID) if err != nil { diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 5ee1d75432..652498771b 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -207,7 +207,12 @@ func downloadReplicated( if license != nil { channel, err := kotsutil.FindChannelInLicense(appSelectedChannelID, license) if err != nil { - return nil, errors.Wrap(err, "failed to find channel in license") + // its likely the app channel has changed, if the license only has a single channel , we should use that + if len(license.Spec.Channels) == 1 { + channel = &license.Spec.Channels[0] + } else { + return nil, errors.Wrap(err, "failed to find channel in license") + } } channelID = channel.ChannelID channelName = channel.ChannelName From 4b5a4f0f2ddc96b9810cf7d5bfbac7d4ea464c22 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 2 Aug 2024 15:56:40 +0000 Subject: [PATCH 32/35] update license for pull integration test --- integration/replicated/pull_test.go | 1 + .../tests/kitchen-sink/license.yaml | 49 ++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/integration/replicated/pull_test.go b/integration/replicated/pull_test.go index 5fc1638b7e..37e94d3881 100644 --- a/integration/replicated/pull_test.go +++ b/integration/replicated/pull_test.go @@ -64,6 +64,7 @@ func Test_PullReplicated(t *testing.T) { ExcludeAdminConsole: true, ExcludeKotsKinds: true, Silent: true, + AppSelectedChannelID: "1vusIYZLAVxMG6q760OJmRKj5i5", } _, err = pull.Pull("replicated://integration", pullOptions) req.NoError(err) diff --git a/integration/replicated/tests/kitchen-sink/license.yaml b/integration/replicated/tests/kitchen-sink/license.yaml index 70b7866fd1..0d50122ad3 100644 --- a/integration/replicated/tests/kitchen-sink/license.yaml +++ b/integration/replicated/tests/kitchen-sink/license.yaml @@ -1,22 +1,49 @@ apiVersion: kots.io/v1beta1 kind: License metadata: - name: kots + creationTimestamp: null + name: testcustomer spec: - licenseID: WQ1srPj9HDmfBG-iHgwniEeUCaF2Ic-y - licenseType: dev - customerName: kots - appSlug: kitchen-sink - channelName: Unstable - licenseSequence: 1 + appSlug: my-app + channelID: 1vusIYZLAVxMG6q760OJmRKj5i5 + channelName: My Channel + customerName: Test Customer endpoint: https://replicated.app entitlements: + bool_field: + title: Bool Field + value: true + valueType: Boolean expires_at: - title: Expiration description: License Expiration - value: '' + title: Expiration + value: "2030-07-27T00:00:00Z" + valueType: String + hidden_field: + isHidden: true + title: Hidden Field + value: this is secret + valueType: String + int_field: + title: Int Field + value: 123 + valueType: Integer + string_field: + title: StringField + value: single line text valueType: String + text_field: + title: Text Field + value: |- + multi + line + text + valueType: Text isAirgapSupported: true isGitOpsSupported: true - signature: >- - eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pYTI5MGN5SjlMQ0p6Y0dWaklqcDdJbXhwWTJWdWMyVkpSQ0k2SWxkUk1YTnlVR281U0VSdFprSkhMV2xJWjNkdWFVVmxWVU5oUmpKSll5MTVJaXdpYkdsalpXNXpaVlI1Y0dVaU9pSmtaWFlpTENKamRYTjBiMjFsY2s1aGJXVWlPaUpyYjNSeklpd2lZWEJ3VTJ4MVp5STZJbXRwZEdOb1pXNHRjMmx1YXlJc0ltTm9ZVzV1Wld4T1lXMWxJam9pVlc1emRHRmliR1VpTENKc2FXTmxibk5sVTJWeGRXVnVZMlVpT2pFc0ltVnVaSEJ2YVc1MElqb2lhSFIwY0hNNkx5OXlaWEJzYVdOaGRHVmtMbUZ3Y0NJc0ltVnVkR2wwYkdWdFpXNTBjeUk2ZXlKbGVIQnBjbVZ6WDJGMElqcDdJblJwZEd4bElqb2lSWGh3YVhKaGRHbHZiaUlzSW1SbGMyTnlhWEIwYVc5dUlqb2lUR2xqWlc1elpTQkZlSEJwY21GMGFXOXVJaXdpZG1Gc2RXVWlPaUlpTENKMllXeDFaVlI1Y0dVaU9pSlRkSEpwYm1jaWZYMHNJbWx6UVdseVoyRndVM1Z3Y0c5eWRHVmtJanAwY25WbExDSnBjMGRwZEU5d2MxTjFjSEJ2Y25SbFpDSTZkSEoxWlgxOSIsImlubmVyU2lnbmF0dXJlIjoiZXlKc2FXTmxibk5sVTJsbmJtRjBkWEpsSWpvaVJFRkVVVTFXTTJkRFprdG5iMkZKYVRoSmNUVnJUV051WTJWb2FpOUJNVkIyYUZkb2QxWkhRVEpYT1ZoUVdYVjVPWE5TZHpCaU4wZ3lPRlJ5WlRadE1XUjBOazh5UlZoak1ISlBVbTFyT0dkRVZEWjJTRTF6Y2xWMFpWSXplQ3N5WWtWVU1XRnRhVWQxZDBWRFIwRnJOMnBWUkhVMFozRjNLemhvYlZOTmMxSm1iMVZLVjFGaWNVVkJOMW96UVRSVGNYVm1Va3MyZEVKUVMwcHVRek12TnpsTFJrc3JNaTlSVUVaSk0yTkRUbEZ5U1VnM04xQXhRa1ZSUVRGVU5XbFJiWEoxYURoRVVYVkRORVpCU1ZWSlpqTk9jSEZLY1dsa2IyWlpjVE52VERGaGIyNW5jWGsxUzNWQ2IwdDNhVFY2SzFkeFNDOTVLM1IwYWpOTGVYbDBPR0ZHV1ZSVWN6RnVOemRQVEZaRWVsSTBUR3R3TURGSlpUZ3dhM1JMYmtKVFoyRm5VWEJtWkhwM1VrVnpha2xKU1hGdU9IWXlXR05WU0ZaVVQxUTRha1J5UkZVd2FGQkpjaTg0WjJ4dlRrczBRMlJrY1ZoblBUMGlMQ0p3ZFdKc2FXTkxaWGtpT2lJdExTMHRMVUpGUjBsT0lGQlZRa3hKUXlCTFJWa3RMUzB0TFZ4dVRVbEpRa2xxUVU1Q1oydHhhR3RwUnpsM01FSkJVVVZHUVVGUFEwRlJPRUZOU1VsQ1EyZExRMEZSUlVGeVUzQmtkMDVpYkhSUUswODRMMFI0Y25FNFZseHVhR1pyTDNOVlZHeEJWMHhyWm5Wak1WY3pOM1p5ZFdVeGFGTlRjMHB4TldSelNIcDNiVzVtT0dORVNtdGxjSFZGWm5aUFZDOXNibTlMUjBNcmRFaFZibHh1UXpsbVRIYzViMEZPV21Wc1JIVkphemcyT1ZsSmVHdDFiVGh2Y0ZaSk0wWXdZMmh2Wm1vM0t5dENTVVIyTVhVcldITmFiR3cwVVZabmFraEZSM3BGVDF4dWQwdEtha3RYWlUxWWFYUldTSFZTZVdkNlZsSXZUemR3VUVnM09WbEVVMUJoWkV4SmFYZFFNM2gwVFhwU2QwMVJTMnRHTm1WS2RUbDJaSFExSzJSaVpWeHVVWFpUZVUxVVJHODJSV3gwYVdkMWJFTmFkemx3VldaU1MwWXlkVTUzTDJOd2MxaFhSWFpZU1ZCRU5EZzRVMlEwWWt3ellrWXZNMkY1Ym04NFlrbFlWbHh1YzB3NVV6UlBaREZSV25KNFJVUndabXhtU2pGbVN5OU9VV1JKVm1GVkwxZHRiREZyZWtGM2NESkhWMlJZUVhwS2N6VjJSVTF6YW5CcVR6VmpZVTlQTVZ4dU5uZEpSRUZSUVVKY2JpMHRMUzB0UlU1RUlGQlZRa3hKUXlCTFJWa3RMUzB0TFZ4dUlpd2lhMlY1VTJsbmJtRjBkWEpsSWpvaVpYbEtlbUZYWkhWWldGSXhZMjFWYVU5cFNsUmlia3BYWWtaT2RGZEVXbEpoVlVZMFRrVk9XVlZZV25sbGJFSkxWRVJGZGs1V1VtNWhNSGh6Vkd4cmVsZEZlR3RrTVd4WFQxWkJNMUpUZEc5V1JVcDBZVEJHYW1WWFVrZGFNSEJTVGtad2QxcFhhRlZpUkZKM1pFUlNURXN6WkZkT2VscEpZMms1ZWxaWVdsRlJNRXAwVlVjNVdHUlZWalpaTTA0MVlrWnNVRmRxWkZwaU1qVnhXVEJHZUZOcmNHcE9Semx2Wkc1YU1rMUZiRlpTYlRGaFdtcENUMVJGVG0xVWJWbzBUMFZrVTFveVpEVlNSRll3VjBSa1RWb3phRXRXZW1SM1QwaGtWV1F6VG1sbFJHUTBXVlJOTUdGR2FFSlRSbWg2WTJwT2JVOUZOV2xYUlZJeVZFUkdNMW94VWs5aGJFWjZZVlZqY2xKRE9ETk5WelZxVkRKV05sZFdiRFJVV0Vwd1QxYzRkbFV5VmpaU1JGSkdUbGhSZGxkSVpISlNNM0J4WWpCdmNrOVliM0pYYkZKVVRYcFNlbUZ0VFhkU1JGSm9VbGRXVEU5WGFFaE9WVko0WkVWNFRGbHRPSHBYYTA1WVdsY3hTR05JUm5kaWJrWjVWa2hzYjFkVVZscGtWVko0WTFSS01GZFRPVXRhTUdNelRsWkNhRTE1ZEZWak1tTXlWMWRTVjFWRll6UlNNRFI2WWxob1MwNUlhelJoTVVrMVYwWnNjV0ZGYjNkUmJVNXdZbXBTVTFJd09WbGlNakZvVVRKb2FWUnJaRFJWYkVvelQxZGFkbGRGUlRsUVUwbHpTVzFrYzJJeVNtaGlSWFJzWlZWc2EwbHFiMmxaYlZKc1dsUlZNazVVV1hkWk1scHBUa1JPYWs5WFNYbFBSMHB0VDFSb2JGbFhUbWhhYlVVeVRrUlphV1pSUFQwaWZRPT0ifQ== \ No newline at end of file + isSnapshotSupported: true + licenseID: 1vusOokxAVp1tkRGuyxnF23PJcq + licenseSequence: 7 + licenseType: prod + signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pZEdWemRHTjFjM1J2YldWeUluMHNJbk53WldNaU9uc2liR2xqWlc1elpVbEVJam9pTVhaMWMwOXZhM2hCVm5BeGRHdFNSM1Y1ZUc1R01qTlFTbU54SWl3aWJHbGpaVzV6WlZSNWNHVWlPaUp3Y205a0lpd2lZM1Z6ZEc5dFpYSk9ZVzFsSWpvaVZHVnpkQ0JEZFhOMGIyMWxjaUlzSW1Gd2NGTnNkV2NpT2lKdGVTMWhjSEFpTENKamFHRnVibVZzU1VRaU9pSXhkblZ6U1ZsYVRFRldlRTFITm5FM05qQlBTbTFTUzJvMWFUVWlMQ0pqYUdGdWJtVnNUbUZ0WlNJNklrMTVJRU5vWVc1dVpXd2lMQ0pzYVdObGJuTmxVMlZ4ZFdWdVkyVWlPamNzSW1WdVpIQnZhVzUwSWpvaWFIUjBjSE02THk5eVpYQnNhV05oZEdWa0xtRndjQ0lzSW1WdWRHbDBiR1Z0Wlc1MGN5STZleUppYjI5c1gyWnBaV3hrSWpwN0luUnBkR3hsSWpvaVFtOXZiQ0JHYVdWc1pDSXNJblpoYkhWbElqcDBjblZsTENKMllXeDFaVlI1Y0dVaU9pSkNiMjlzWldGdUluMHNJbVY0Y0dseVpYTmZZWFFpT25zaWRHbDBiR1VpT2lKRmVIQnBjbUYwYVc5dUlpd2laR1Z6WTNKcGNIUnBiMjRpT2lKTWFXTmxibk5sSUVWNGNHbHlZWFJwYjI0aUxDSjJZV3gxWlNJNklqSXdNekF0TURjdE1qZFVNREE2TURBNk1EQmFJaXdpZG1Gc2RXVlVlWEJsSWpvaVUzUnlhVzVuSW4wc0ltaHBaR1JsYmw5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtocFpHUmxiaUJHYVdWc1pDSXNJblpoYkhWbElqb2lkR2hwY3lCcGN5QnpaV055WlhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lMQ0pwYzBocFpHUmxiaUk2ZEhKMVpYMHNJbWx1ZEY5bWFXVnNaQ0k2ZXlKMGFYUnNaU0k2SWtsdWRDQkdhV1ZzWkNJc0luWmhiSFZsSWpveE1qTXNJblpoYkhWbFZIbHdaU0k2SWtsdWRHVm5aWElpZlN3aWMzUnlhVzVuWDJacFpXeGtJanA3SW5ScGRHeGxJam9pVTNSeWFXNW5SbWxsYkdRaUxDSjJZV3gxWlNJNkluTnBibWRzWlNCc2FXNWxJSFJsZUhRaUxDSjJZV3gxWlZSNWNHVWlPaUpUZEhKcGJtY2lmU3dpZEdWNGRGOW1hV1ZzWkNJNmV5SjBhWFJzWlNJNklsUmxlSFFnUm1sbGJHUWlMQ0oyWVd4MVpTSTZJbTExYkhScFhHNXNhVzVsWEc1MFpYaDBJaXdpZG1Gc2RXVlVlWEJsSWpvaVZHVjRkQ0o5ZlN3aWFYTkJhWEpuWVhCVGRYQndiM0owWldRaU9uUnlkV1VzSW1selIybDBUM0J6VTNWd2NHOXlkR1ZrSWpwMGNuVmxMQ0pwYzFOdVlYQnphRzkwVTNWd2NHOXlkR1ZrSWpwMGNuVmxmWDA9IiwiaW5uZXJTaWduYXR1cmUiOiJleUpzYVdObGJuTmxVMmxuYm1GMGRYSmxJam9pYUhneE1XTXZUR1ozUTNoVE5YRmtRWEJGU1hGdVRrMU9NMHBLYTJzNFZHZFhSVVpzVDFKVlJ6UjJjR1YzZEZoV1YzbG1lamRZY0hBd1ExazJZamRyUVRSS2N6TklhR3d3YkZJMFdUQTFMemN2UVVkQ2FEZFZNSGczUkhaTVozUXpVM00wYm5GTFZTdFhXRXBTVHpKWVFVRnZSME4xZFRWR1RGcHJRVWhYY1RSUVFtMXphSFY2Y1ZsdmNucHhlbGhGWVZWVlpFUlVkVXhDTW1nNWFIZ3dXRWhQUmxwUk16bHVkbTlPUjJaT2R5OTRTVmRaZEhSUGRYZHZhMncyTVZsb1JVeFZlRmQxU1ZSRmMwTlVhM2xtTVRNd09IazVSbFJzWlRKeVYyZEVlSEZNYTBSUFNXVXlPRWwzUzJSQkwySXdWVUl5VEZGbVRWcHdWemwyUTNCSkwybHlWek5uYmpaeU5WWjNWMjB2U1dweWJtNDNSelJrVmpadVYzcFRkMGhQUTJSdWEwMTRNRXQ1VVVOa0wxQjFaWEpUYjNSdVEwOXRTMDEzWlRSTGJqaERkMU5YVVRRNGRURkRNbTFpV1VzeGRYTlpOM1YzUFQwaUxDSndkV0pzYVdOTFpYa2lPaUl0TFMwdExVSkZSMGxPSUZCVlFreEpReUJMUlZrdExTMHRMVnh1VFVsSlFrbHFRVTVDWjJ0eGFHdHBSemwzTUVKQlVVVkdRVUZQUTBGUk9FRk5TVWxDUTJkTFEwRlJSVUZ6TkhKdlVIcDFhV1JNZVhOMmIxWTJkemxhTkZ4dVdHRmliME5tWTJNeGFHZFZhQ3N3V1VkS2NFNURSVXhyTjBaTFF5OTJhemR6ZERsR05tY3dUMjlrU0VSbGVYZFJXa2hLZFU1TVpsUnNRbEJHUTJOaU5seHVObTlzVEZOeWNGQTRjbFUzU0d4SGJsRkVSMFJNYVhkS1EyaGtSRGRVVUdSM2FXdHBkMHRGY201aldqaEdaalZsU25vd2RETmlUWFpyVDJaVVluSkJiRnh1WWtGQ1kwbzVNVmxVT1hKdVVXOXFkVWN4UldKUVRqaEZWblI2TWxZNE5IZHViR2Q0TUhCd2JEVjRPSFpOYlhwcE1ISnVibEZVV1VGamJ6WnFhMnBJTTF4dVRuTlVkWE4xUzFkdlJGUjVNWE5yZGtSUk9IbEJZV0ptWTNNME4zWnNRazAwU0RGT1JFNHZSSFJhWWxZdllubDJia0o2YkM4eFZrVnpURmRqWlZWcFRGeHVSWEYxT0VkeWF5dFFVRGQyUkdSd2JFUjNjWFpQV2t4RmRYazNkamhuUm01U09WUlVSV3ByTlVvNWRuWlVTR2RtU25VemVubEVPR2xLWTBSRE5YcHFPVnh1YjFGSlJFRlJRVUpjYmkwdExTMHRSVTVFSUZCVlFreEpReUJMUlZrdExTMHRMVnh1SWl3aWEyVjVVMmxuYm1GMGRYSmxJam9pWlhsS2VtRlhaSFZaV0ZJeFkyMVZhVTlwU2pCUldIQjJXVE5LVms1NmFGaFNSMlJzVVRKb2NtTklXa1ZVVlRsRldqQktXVTFGUmtaVFJFNUZVMGhLYkUxclRUTkxNSEJFVkROR2VGTnROVVJVVlRWVlltMDFiVnBGUm5sWldIQjZaRVJqTVZaSGFFeFBXRUpVVWtacmRrd3diek5aTUZaSlVteFdWRXd5T1VoV1JXeHNWa1ZPTUZSSE1WWlJNR04zVkd4R2JGa3pTblJUUm1zMFZVWk9hMVpWU2pCVU1WbDNZbXQwY0ZSclZuQmpia0poVFZjNWFtSldiSEZaYTNob1UyeHNWV0pGUmtWWGJVWnZWakZLVUZkcWJGSmhXRVp1V2xkb1EyRnVRak5TUjNNd1lWWkpOVTVXVmxkV1ZUVnlUMGhLYjFsVlRYbGhiVGcwVjBkYWVGbHFWbFppYlhoeFpFWkZkMDU1Y3pCaFZsSkpWRVpPTm1WRk1IcGxWWFJ2VFVaR1ZtRXdWVFJSVnpsSFVsaEtVRTFZUmxCU01WcFJVMVJDTmxsV2FIcFdWWEJ0WTBSU2JFMVVRazlPVjNSU1ZucFdUMU5XWTNaU1ZYUkZVMGhzYlU5VmJGaGtNMUl3WTFWc1lXTlhSakJTYTA1RVlVWmtjbUo2VmtSU00wSllUREkxUmsxWVl6SmxWM1JKVlZoQk1sVXhTbEppU0Zwd1VrVXdNRlpFVWt0VU1rWnNVVmQwYzFSV1VrMVVWV055V1RCYVRHSXpaRTlUVm05NVlraE9SR1JzVG5aUmFrWmFaVmRPVGxOVlNteGFiRXB1Wld0U2RVMHhSVGxRVTBselNXMWtjMkl5U21oaVJYUnNaVlZzYTBscWIybFpiVkpzV2xSVk1rNVVXWGRaTWxwcFRrUk9hazlYU1hsUFIwcHRUMVJvYkZsWFRtaGFiVVV5VGtSWmFXWlJQVDBpZlE9PSJ9 +status: {} \ No newline at end of file From 5e67d0568c3885869102b279a18b175e661b4e25 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Fri, 2 Aug 2024 18:24:45 +0000 Subject: [PATCH 33/35] Pass selected channel to pull options, and allow overriding in RenderOpts --- pkg/airgap/update.go | 1 + pkg/kotsadmupstream/upstream.go | 1 + pkg/render/render.go | 10 +++++++++- pkg/render/types/interface.go | 13 +++++++------ pkg/store/kotsstore/license_store.go | 23 +++++++++++++++-------- pkg/upstream/replicated.go | 16 ++++++++-------- 6 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pkg/airgap/update.go b/pkg/airgap/update.go index 35986a9863..4f4d208dc6 100644 --- a/pkg/airgap/update.go +++ b/pkg/airgap/update.go @@ -203,6 +203,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri AppID: a.ID, AppSlug: a.Slug, AppSequence: appSequence, + AppSelectedChannelID: a.SelectedChannelID, SkipCompatibilityCheck: skipCompatibilityCheck, KotsKinds: beforeKotsKinds, } diff --git a/pkg/kotsadmupstream/upstream.go b/pkg/kotsadmupstream/upstream.go index 525dd8f7a5..1d59d246d0 100644 --- a/pkg/kotsadmupstream/upstream.go +++ b/pkg/kotsadmupstream/upstream.go @@ -231,6 +231,7 @@ func DownloadUpdate(appID string, update types.Update, skipPreflights bool, skip RewriteImageOptions: registrySettings, SkipCompatibilityCheck: skipCompatibilityCheck, KotsKinds: beforeKotsKinds, + AppSelectedChannelID: a.SelectedChannelID, } _, err = pull.Pull(fmt.Sprintf("replicated://%s", beforeKotsKinds.License.Spec.AppSlug), pullOptions) diff --git a/pkg/render/render.go b/pkg/render/render.go index e49e89bfef..7551dbea00 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -121,6 +121,14 @@ func RenderDir(opts types.RenderDirOptions) error { downstreamNames = append(downstreamNames, d.Name) } + // typically we want the selected channel id on the App obj + // but in the case of a license sync, like when called from UpdateAppLicense + // the app object is not updated yet - but the license being passed in is the latest + selectedChannelID := opts.AppSelectedChannelID + if selectedChannelID == "" { + selectedChannelID = opts.App.SelectedChannelID + } + appNamespace := util.PodNamespace if os.Getenv("KOTSADM_TARGET_NAMESPACE") != "" { appNamespace = os.Getenv("KOTSADM_TARGET_NAMESPACE") @@ -141,7 +149,7 @@ func RenderDir(opts types.RenderDirOptions) error { IsAirgap: opts.App.IsAirgap, AppID: opts.App.ID, AppSlug: opts.App.Slug, - AppSelectedChannelID: opts.App.SelectedChannelID, + AppSelectedChannelID: selectedChannelID, IsGitOps: opts.App.IsGitOps, AppSequence: opts.Sequence, ReportingInfo: opts.ReportingInfo, diff --git a/pkg/render/types/interface.go b/pkg/render/types/interface.go index a56ce9fb99..525edf27c2 100644 --- a/pkg/render/types/interface.go +++ b/pkg/render/types/interface.go @@ -19,12 +19,13 @@ type RenderFileOptions struct { } type RenderDirOptions struct { - ArchiveDir string - App *apptypes.App - Downstreams []downstreamtypes.Downstream - RegistrySettings registrytypes.RegistrySettings - Sequence int64 - ReportingInfo *reportingtypes.ReportingInfo + ArchiveDir string + App *apptypes.App + Downstreams []downstreamtypes.Downstream + RegistrySettings registrytypes.RegistrySettings + Sequence int64 + ReportingInfo *reportingtypes.ReportingInfo + AppSelectedChannelID string } type Renderer interface { diff --git a/pkg/store/kotsstore/license_store.go b/pkg/store/kotsstore/license_store.go index 0e0bc404d3..4cc4da6e0e 100644 --- a/pkg/store/kotsstore/license_store.go +++ b/pkg/store/kotsstore/license_store.go @@ -121,6 +121,11 @@ func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDi return int64(0), errors.Wrap(err, "failed to write new license") } + selectedChannelId, err := s.GetAppSelectedChannelID(appID) + if err != nil { + return int64(0), errors.Wrap(err, "failed to get existing app selected channel id") + } + // If the license channels array has more than one entry, then the license is a true multi-channel license, // and we should skip updating selected_channel_id in the app table. If there's only a single entry, // we should update the selected_channel_id in the app table to ensure it stays consistent across channel @@ -138,9 +143,10 @@ func (s *KOTSStore) UpdateAppLicense(appID string, baseSequence int64, archiveDi Query: `update app set license = ?, last_license_sync = ?, channel_changed = ?, selected_channel_id = ? where id = ?`, Arguments: []interface{}{originalLicenseData, time.Now().Unix(), channelChanged, newLicense.Spec.ChannelID, appID}, }) + selectedChannelId = newLicense.Spec.ChannelID } - appVersionStatements, newSeq, err := s.createNewVersionForLicenseChangeStatements(appID, baseSequence, archiveDir, renderer, reportingInfo) + appVersionStatements, newSeq, err := s.createNewVersionForLicenseChangeStatements(appID, baseSequence, archiveDir, renderer, reportingInfo, selectedChannelId) if err != nil { // ignore error here to prevent a failure to render the current version // preventing the end-user from updating the application @@ -177,7 +183,7 @@ func (s *KOTSStore) UpdateAppLicenseSyncNow(appID string) error { return nil } -func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, baseSequence int64, archiveDir string, renderer rendertypes.Renderer, reportingInfo *reportingtypes.ReportingInfo) ([]gorqlite.ParameterizedStatement, int64, error) { +func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, baseSequence int64, archiveDir string, renderer rendertypes.Renderer, reportingInfo *reportingtypes.ReportingInfo, selectedChannelID string) ([]gorqlite.ParameterizedStatement, int64, error) { registrySettings, err := s.GetRegistryDetailsForApp(appID) if err != nil { return nil, int64(0), errors.Wrap(err, "failed to get registry settings for app") @@ -199,12 +205,13 @@ func (s *KOTSStore) createNewVersionForLicenseChangeStatements(appID string, bas } if err := renderer.RenderDir(rendertypes.RenderDirOptions{ - ArchiveDir: archiveDir, - App: app, - Downstreams: downstreams, - RegistrySettings: registrySettings, - Sequence: nextAppSequence, - ReportingInfo: reportingInfo, + ArchiveDir: archiveDir, + App: app, + Downstreams: downstreams, + RegistrySettings: registrySettings, + Sequence: nextAppSequence, + ReportingInfo: reportingInfo, + AppSelectedChannelID: selectedChannelID, }); err != nil { return nil, int64(0), errors.Wrap(err, "failed to render new version") } diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index 652498771b..a566342bb2 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -205,17 +205,17 @@ func downloadReplicated( // get channel name from license, if one was provided channelID, channelName := "", "" if license != nil { - channel, err := kotsutil.FindChannelInLicense(appSelectedChannelID, license) - if err != nil { - // its likely the app channel has changed, if the license only has a single channel , we should use that - if len(license.Spec.Channels) == 1 { - channel = &license.Spec.Channels[0] - } else { + if appSelectedChannelID != "" { + channel, err := kotsutil.FindChannelInLicense(appSelectedChannelID, license) + if err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } + channelID = channel.ChannelID + channelName = channel.ChannelName + } else { + channelID = license.Spec.ChannelID + channelName = license.Spec.ChannelName } - channelID = channel.ChannelID - channelName = channel.ChannelName } if existingIdentityConfig == nil { From 3cd0a798374c5801b71b33d0b34eebad2ca5a489 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 6 Aug 2024 22:18:12 +0000 Subject: [PATCH 34/35] pr feedback --- pkg/tests/renderdir/renderdir_test.go | 1 - pkg/update/required_test.go | 2 +- pkg/update/update.go | 5 +- pkg/update/update_test.go | 129 +++++++++++++++++++------- pkg/updatechecker/updatechecker.go | 4 +- 5 files changed, 97 insertions(+), 44 deletions(-) diff --git a/pkg/tests/renderdir/renderdir_test.go b/pkg/tests/renderdir/renderdir_test.go index 7d43c343e2..30f9cebed0 100644 --- a/pkg/tests/renderdir/renderdir_test.go +++ b/pkg/tests/renderdir/renderdir_test.go @@ -73,7 +73,6 @@ func TestKotsRenderDir(t *testing.T) { Name: spec.Name, RenderDirOptions: spec.RenderDirOptions, } - test.RenderDirOptions.App.SelectedChannelID = "1vusIYZLAVxMG6q760OJmRKj5i5" tests = append(tests, test) } require.NoError(t, err) diff --git a/pkg/update/required_test.go b/pkg/update/required_test.go index 0a8c834544..d53f589a2f 100644 --- a/pkg/update/required_test.go +++ b/pkg/update/required_test.go @@ -229,7 +229,7 @@ func Test_getRequiredAirgapUpdates(t *testing.T) { ChannelID: "stable-channel", ChannelName: "Stable Channel", ChannelSlug: "stable-channel", - IsDefault: true, + IsDefault: false, IsSemverRequired: true, }, { diff --git a/pkg/update/update.go b/pkg/update/update.go index c1c1de0a6b..3d79e61ffb 100644 --- a/pkg/update/update.go +++ b/pkg/update/update.go @@ -31,10 +31,7 @@ func InitAvailableUpdatesDir() error { } func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *kotsv1beta1.License) ([]types.AvailableUpdate, error) { - var err error - var licenseChan *kotsv1beta1.Channel - - licenseChan, err = kotsutil.FindChannelInLicense(app.SelectedChannelID, license) + licenseChan, err := kotsutil.FindChannelInLicense(app.SelectedChannelID, license) if err != nil { return nil, errors.Wrap(err, "failed to find channel in license") } diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 6f29cd6258..1e880fdb0f 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -31,12 +31,13 @@ func TestGetAvailableUpdates(t *testing.T) { license *kotsv1beta1.License } tests := []struct { - name string - args args - channelReleases []upstream.ChannelRelease - setup func(t *testing.T, args args, mockServerEndpoint string) - want []types.AvailableUpdate - wantErr bool + name string + args args + perChannelReleases map[string][]upstream.ChannelRelease + setup func(t *testing.T, args args, mockServerEndpoint string) + want []types.AvailableUpdate + wantErr bool + expectedSelectedChannelId string }{ { name: "no updates", @@ -55,14 +56,15 @@ func TestGetAvailableUpdates(t *testing.T) { }, }, }, - channelReleases: []upstream.ChannelRelease{}, + perChannelReleases: map[string][]upstream.ChannelRelease{}, setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, - want: []types.AvailableUpdate{}, - wantErr: false, + want: []types.AvailableUpdate{}, + wantErr: false, + expectedSelectedChannelId: "channel-id", }, { name: "has updates", @@ -81,22 +83,24 @@ func TestGetAvailableUpdates(t *testing.T) { }, }, }, - channelReleases: []upstream.ChannelRelease{ - { - ChannelSequence: 2, - ReleaseSequence: 2, - VersionLabel: "0.0.2", - IsRequired: false, - CreatedAt: testTime.Format(time.RFC3339), - ReleaseNotes: "release notes", - }, - { - ChannelSequence: 1, - ReleaseSequence: 1, - VersionLabel: "0.0.1", - IsRequired: true, - CreatedAt: testTime.Format(time.RFC3339), - ReleaseNotes: "release notes", + perChannelReleases: map[string][]upstream.ChannelRelease{ + "channel-id": { + { + ChannelSequence: 2, + ReleaseSequence: 2, + VersionLabel: "0.0.2", + IsRequired: false, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + { + ChannelSequence: 1, + ReleaseSequence: 1, + VersionLabel: "0.0.1", + IsRequired: true, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, }, }, setup: func(t *testing.T, args args, licenseEndpoint string) { @@ -125,7 +129,8 @@ func TestGetAvailableUpdates(t *testing.T) { IsDeployable: true, }, }, - wantErr: false, + wantErr: false, + expectedSelectedChannelId: "channel-id", }, { name: "fails to fetch updates", @@ -144,14 +149,15 @@ func TestGetAvailableUpdates(t *testing.T) { }, }, }, - channelReleases: []upstream.ChannelRelease{}, + perChannelReleases: map[string][]upstream.ChannelRelease{}, setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil) }, - want: []types.AvailableUpdate{}, - wantErr: true, + want: []types.AvailableUpdate{}, + wantErr: true, + expectedSelectedChannelId: "channel-id", }, { name: "uses installed channel id when multi-channel present", @@ -182,20 +188,60 @@ func TestGetAvailableUpdates(t *testing.T) { }, }, }, - channelReleases: []upstream.ChannelRelease{}, + perChannelReleases: map[string][]upstream.ChannelRelease{ + "channel-id": { + { + ChannelSequence: 2, + ReleaseSequence: 2, + VersionLabel: "0.0.2", + IsRequired: false, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + { + ChannelSequence: 1, + ReleaseSequence: 1, + VersionLabel: "0.0.1", + IsRequired: true, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + }, + "channel-id2": { + { + ChannelSequence: 3, + ReleaseSequence: 3, + VersionLabel: "3.0.0", + IsRequired: false, + CreatedAt: testTime.Format(time.RFC3339), + ReleaseNotes: "release notes", + }, + }, + }, setup: func(t *testing.T, args args, licenseEndpoint string) { t.Setenv("USE_MOCK_REPORTING", "1") args.license.Spec.Endpoint = licenseEndpoint mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.Channels[1].ChannelID).Return("1", nil) }, - want: []types.AvailableUpdate{}, - wantErr: false, + want: []types.AvailableUpdate{ + { + VersionLabel: "3.0.0", + UpdateCursor: "3", + ChannelID: "channel-id2", + IsRequired: false, + UpstreamReleasedAt: &testTime, + ReleaseNotes: "release notes", + IsDeployable: true, + }, + }, + wantErr: false, + expectedSelectedChannelId: "channel-id2", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - mockServer := newMockServerWithReleases(tt.channelReleases, tt.wantErr) + mockServer := newMockServerWithReleases(tt.perChannelReleases, tt.expectedSelectedChannelId, tt.wantErr) defer mockServer.Close() tt.setup(t, tt.args, mockServer.URL) got, err := GetAvailableUpdates(tt.args.kotsStore, tt.args.app, tt.args.license) @@ -209,16 +255,29 @@ func TestGetAvailableUpdates(t *testing.T) { } } -func newMockServerWithReleases(channelReleases []upstream.ChannelRelease, wantErr bool) *httptest.Server { +func newMockServerWithReleases(preChannelReleases map[string][]upstream.ChannelRelease, expectedSelectedChannelId string, wantErr bool) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if wantErr { http.Error(w, "error", http.StatusInternalServerError) return } + var response struct { ChannelReleases []upstream.ChannelRelease `json:"channelReleases"` } - response.ChannelReleases = channelReleases + + selectedChannelID := r.URL.Query().Get("selectedChannelId") + if selectedChannelID != expectedSelectedChannelId { + http.Error(w, "invalid selectedChannelId", http.StatusBadRequest) + return + } + + if releases, ok := preChannelReleases[selectedChannelID]; ok { + response.ChannelReleases = releases + } else { + response.ChannelReleases = []upstream.ChannelRelease{} + } + w.Header().Set("X-Replicated-UpdateCheckAt", time.Now().Format(time.RFC3339)) if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/updatechecker/updatechecker.go b/pkg/updatechecker/updatechecker.go index c0c7437545..05a87c675f 100644 --- a/pkg/updatechecker/updatechecker.go +++ b/pkg/updatechecker/updatechecker.go @@ -27,7 +27,6 @@ import ( upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types" "github.com/replicatedhq/kots/pkg/util" "github.com/replicatedhq/kots/pkg/version" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" cron "github.com/robfig/cron/v3" "go.uber.org/zap" "k8s.io/apimachinery/pkg/util/wait" @@ -228,8 +227,7 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<- return nil, errors.Wrap(err, "failed to get app") } - var licenseChan *kotsv1beta1.Channel - licenseChan, err = kotsutil.FindChannelInLicense(a.SelectedChannelID, latestLicense) + licenseChan, err := kotsutil.FindChannelInLicense(a.SelectedChannelID, latestLicense) if err != nil { return nil, errors.Wrap(err, "failed to find channel in license after sync") } From 3961a04f1e5dc4f998e25ef96acfea43983f94a2 Mon Sep 17 00:00:00 2001 From: Florian Hines Date: Tue, 6 Aug 2024 22:29:04 +0000 Subject: [PATCH 35/35] Add playwright tests for channel change --- .github/actions/kots-e2e/action.yml | 1 + .github/workflows/build-test.yaml | 35 +++++++++++ e2e/Makefile | 1 + e2e/e2e_test.go | 1 + e2e/inventory/inventory.go | 10 ++++ .../tests/change-channel/license.yaml | 29 ++++++++++ .../tests/change-channel/test.spec.ts | 58 +++++++++++++++++++ 7 files changed, 135 insertions(+) create mode 100644 e2e/playwright/tests/change-channel/license.yaml create mode 100644 e2e/playwright/tests/change-channel/test.spec.ts diff --git a/.github/actions/kots-e2e/action.yml b/.github/actions/kots-e2e/action.yml index 465fc7ee92..4dc16afeca 100644 --- a/.github/actions/kots-e2e/action.yml +++ b/.github/actions/kots-e2e/action.yml @@ -95,6 +95,7 @@ runs: - name: execute suite "${{ inputs.test-focus }}" env: TESTIM_ACCESS_TOKEN: ${{ inputs.testim-access-token }} + REPLICATED_API_TOKEN: ${{ inputs.replicated-api-token }} KOTS_NAMESPACE: ${{ inputs.kots-namespace }} run: | make -C e2e test \ diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 40469c3027..269db08ca7 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1085,6 +1085,40 @@ jobs: kots-dockerhub-username: '${{ secrets.E2E_DOCKERHUB_USERNAME }}' kots-dockerhub-password: '${{ secrets.E2E_DOCKERHUB_PASSWORD }}' + validate-change-channel: + runs-on: ubuntu-20.04 + needs: [ enable-tests, can-run-ci, build-kots, build-kotsadm, build-e2e, build-kurl-proxy, build-migrations, push-minio, push-rqlite ] + strategy: + fail-fast: false + matrix: + cluster: [ + {distribution: kind, version: v1.28.0} + ] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: download e2e deps + uses: actions/download-artifact@v4 + with: + name: e2e + path: e2e/bin/ + - run: docker load -i e2e/bin/e2e-deps.tar + - run: chmod +x e2e/bin/* + - name: download kots binary + uses: actions/download-artifact@v4 + with: + name: kots + path: bin/ + - run: chmod +x bin/* + - uses: ./.github/actions/kots-e2e + with: + test-focus: 'Change Channel' + kots-namespace: 'change-channel' + k8s-distribution: ${{ matrix.cluster.distribution }} + k8s-version: ${{ matrix.cluster.version }} + replicated-api-token: '${{ secrets.C11Y_MATRIX_TOKEN }}' + kots-dockerhub-username: '${{ secrets.E2E_DOCKERHUB_USERNAME }}' + kots-dockerhub-password: '${{ secrets.E2E_DOCKERHUB_PASSWORD }}' validate-minimal-rbac-override: runs-on: ubuntu-20.04 @@ -4130,6 +4164,7 @@ jobs: - validate-backup-and-restore - validate-no-required-config - validate-config + - validate-change-channel # non-testim tests - validate-minimal-rbac - validate-minimal-rbac-override diff --git a/e2e/Makefile b/e2e/Makefile index 846bcdb923..3fb0dae36e 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -32,6 +32,7 @@ test: docker run --rm -i --net host \ -e TESTIM_ACCESS_TOKEN \ -v $(BIN_DIR)/e2e.test:/usr/local/bin/e2e.test \ + -e REPLICATED_API_TOKEN \ -v $(KOTS_BIN_DIR)/kots:/usr/local/bin/kots \ -v $(KOTS_BIN_DIR)/kots:/usr/local/bin/kubectl-kots \ -v $(PLAYWRIGHT_DIR)/playwright-report:/playwright/playwright-report \ diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index ea47699c88..0abfca8436 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -221,6 +221,7 @@ var _ = Describe("E2E", func() { Entry(nil, inventory.MultiAppTest()), Entry(nil, inventory.NewSupportBundle()), Entry(nil, inventory.NewGitOps()), + Entry(nil, inventory.NewChangeChannel()), ) }) diff --git a/e2e/inventory/inventory.go b/e2e/inventory/inventory.go index 7b2affdff8..cb8d20f1fb 100644 --- a/e2e/inventory/inventory.go +++ b/e2e/inventory/inventory.go @@ -169,6 +169,16 @@ func NewGitOps() Test { } } +func NewChangeChannel() Test { + return Test{ + ID: "change-channel", + Name: "Change Channel", + Namespace: "change-channel", + AppSlug: "change-channel", + UpstreamURI: "change-channel/automated", + } +} + func SetupRegressionTest(kubectlCLI *kubectl.CLI) TestimParams { cmd := kubectlCLI.Command( context.Background(), diff --git a/e2e/playwright/tests/change-channel/license.yaml b/e2e/playwright/tests/change-channel/license.yaml new file mode 100644 index 0000000000..a050aa766b --- /dev/null +++ b/e2e/playwright/tests/change-channel/license.yaml @@ -0,0 +1,29 @@ +apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: github-action +spec: + appSlug: change-channel + channelID: 2k6j62KPRQLjO0tF9zZB6zJJukg + channelName: Automated + channels: + - channelID: 2k6j62KPRQLjO0tF9zZB6zJJukg + channelName: Automated + channelSlug: automated + endpoint: https://replicated.app + isDefault: true + customerName: github-action + endpoint: https://replicated.app + entitlements: + expires_at: + description: License Expiration + signature: {} + title: Expiration + value: "" + valueType: String + isKotsInstallEnabled: true + isNewKotsUiEnabled: true + licenseID: 2k6jempcB6aQyddcIEBSTj0Bvvv + licenseSequence: 16 + licenseType: trial + signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pWjJsMGFIVmlMV0ZqZEdsdmJpSjlMQ0p6Y0dWaklqcDdJbXhwWTJWdWMyVkpSQ0k2SWpKck5tcGxiWEJqUWpaaFVYbGtaR05KUlVKVFZHb3dRbloyZGlJc0lteHBZMlZ1YzJWVWVYQmxJam9pZEhKcFlXd2lMQ0pqZFhOMGIyMWxjazVoYldVaU9pSm5hWFJvZFdJdFlXTjBhVzl1SWl3aVlYQndVMngxWnlJNkltTm9ZVzVuWlMxamFHRnVibVZzSWl3aVkyaGhibTVsYkVsRUlqb2lNbXMyYWpZeVMxQlNVVXhxVHpCMFJqbDZXa0kyZWtwS2RXdG5JaXdpWTJoaGJtNWxiRTVoYldVaU9pSkJkWFJ2YldGMFpXUWlMQ0pqYUdGdWJtVnNjeUk2VzNzaVkyaGhibTVsYkVsRUlqb2lNbXMyYWpZeVMxQlNVVXhxVHpCMFJqbDZXa0kyZWtwS2RXdG5JaXdpWTJoaGJtNWxiRk5zZFdjaU9pSmhkWFJ2YldGMFpXUWlMQ0pqYUdGdWJtVnNUbUZ0WlNJNklrRjFkRzl0WVhSbFpDSXNJbWx6UkdWbVlYVnNkQ0k2ZEhKMVpTd2laVzVrY0c5cGJuUWlPaUpvZEhSd2N6b3ZMM0psY0d4cFkyRjBaV1F1WVhCd0luMWRMQ0pzYVdObGJuTmxVMlZ4ZFdWdVkyVWlPakUyTENKbGJtUndiMmx1ZENJNkltaDBkSEJ6T2k4dmNtVndiR2xqWVhSbFpDNWhjSEFpTENKbGJuUnBkR3hsYldWdWRITWlPbnNpWlhod2FYSmxjMTloZENJNmV5SjBhWFJzWlNJNklrVjRjR2x5WVhScGIyNGlMQ0prWlhOamNtbHdkR2x2YmlJNklreHBZMlZ1YzJVZ1JYaHdhWEpoZEdsdmJpSXNJblpoYkhWbElqb2lJaXdpZG1Gc2RXVlVlWEJsSWpvaVUzUnlhVzVuSWl3aWMybG5ibUYwZFhKbElqcDdmWDE5TENKcGMwNWxkMHR2ZEhOVmFVVnVZV0pzWldRaU9uUnlkV1VzSW1selMyOTBjMGx1YzNSaGJHeEZibUZpYkdWa0lqcDBjblZsZlgwPSIsImlubmVyU2lnbmF0dXJlIjoiZXlKc2FXTmxibk5sVTJsbmJtRjBkWEpsSWpvaWRsRmpjWGt6T1hRellYQnRabWhTVFdoR01HdE5jblp2YlhST1pGZDBaR3Q1UW1zNVZWbHBaM2MzT0ZKMU56Z3Jaa0V5ZW1aVVRGTXJNV1JMUkRCcVNWcG5SbkZhYkRCRlJVaHJWbmRWUld0bE5IWk9RV2QyY1VsdWJrcDJXVGxNWWpKNGVUTk1NMDFQY1VjMk1GZERZV1l3VGxkUVpWTm9jbTVHYldSSmVXcG1SRFpOTUhsWVVUWndZVkpaZVU0MWFsaEdPVUZVY0dWVU5VSXpWVnBYVkdwb1R6QTJPVk5GYVdkUGEyeExjRlYzU3pZclRGbENPV2xEVWk5bWMyNXdkbU5vV0ROYU1HSjVSSGhtZEZnMFF6Rk1XWEF4VVc5MWRITjZNMkl2TVZZMWRXOUdMemd6YlZGUVRsQXplVEZrT1hadVFYazFlV1VyWVd4cE1HTm1RalZuYmpoTFYybERkRTFsVHk5aGNHRnFZbTlVTmpoTVdGUXZRbWhTUnpaQlJVTTBaWFp4Vkd4aVNYTXhZVTFOWW0wM2VIcGFlVFo2Y2twNVFWRXJjVzA1TmxWVVpGUjRiekJ4VUdKRFJrVndWSGhyYzJKQlBUMGlMQ0p3ZFdKc2FXTkxaWGtpT2lJdExTMHRMVUpGUjBsT0lGQlZRa3hKUXlCTFJWa3RMUzB0TFZ4dVRVbEpRa2xxUVU1Q1oydHhhR3RwUnpsM01FSkJVVVZHUVVGUFEwRlJPRUZOU1VsQ1EyZExRMEZSUlVFeFFWRmFRa2xEZW5wemRXTnVXV1JNVlM5YU1GeHVNSGsxTVc5U1EzaGxNVzFRWTBvNVUxbzRORlJqZUVScFVVZFNTRzluV2pZMVQyWklWV3g1V21Sb09FeEpOSFpYZFRKRGJVVnpkbVppV1VOS05VWlRTRnh1UlhSeVkzWnhURTlUWTJVemNsaHZhR2RNVmpkNVRtaFZNa3cxUm1vMWFuRXhkelU0WjBWbGVHTXJZbTUxVVdvME16TXJOR2s0VkZWTk1tNUZVVzFKY1Z4dU1tZHNOa1JxUWk5Sk1YWnNVMjQxTmtvdk1IVmhSWHBaVjFBck4ySkhlbTEzT0dNeWQycEJhRTFyUkZJck5USlBRM2hHY1dGaFExbDBaemROTDBWbE5seHVaWFIzWVRGNFREUlNSMlJCSzNkTFYzaDRWa1VyYjAxMlluQjROMXA1WWxJdldERjBRazlvZWtaSVdISjBaa2RCYm5CWE1sQkRjbVJ4Um1oMWVsWlZMMXh1TUhWSVRuVk1URkV5ZVVaemNFVjJUa1V4YlVKRFpqZHpUR2hNY205M1RVRlBWRnAxWkZJNGFtMXJTMnhJWlROT2MzUnlUekl2ZVVadWN6bHZWRnBoTWx4dWJYZEpSRUZSUVVKY2JpMHRMUzB0UlU1RUlGQlZRa3hKUXlCTFJWa3RMUzB0TFZ4dUlpd2lhMlY1VTJsbmJtRjBkWEpsSWpvaVpYbEtlbUZYWkhWWldGSXhZMjFWYVU5cFNsQmhhbVJ4VWtaT2MwMHljSEJhVjFKMVlsZDRiV05GTVhsVFJXUnRUVzVCTldJd1VUUmFSMFY0WTBka2VrOVhUbFpoVjJSRllXdHNTbEpXYUhwbGF6VlBWakZPVkU1SVJrMWhhWFJMVlhwc2JXSXhWa1pTTUhoNFUzazROV0l6VGxKV2JtUXhVbTVXTldNeFVsSmllWFJEWWpGQk0wMUhVbHBqYkc4d1lUSTViRk13UlhoaFdHUmFZbnBhV0dGVWJFcFphMk15U3pCUmQxTkhjRWRrYlhnelVtMVZNbUpUZEV0a2JsSlBZak5yTldSR1ZuRlRXRkpUVmpGSmVtRnJNVmRWUkZwUllsVTRkMWRYVWt4UFNFMTZWMWRhVldOdVRrOWlibWgxV2pGUk1WWlZUVEZhVkVKM1UxVTVWMXBYVGpOVlZrSkhWa2hqZGxSc1dsQmlSVnBxWVRCS05rOUlRbHBsVjBaTVpESlZNbE5YZEhWT1EzUndVbXBDVTJSVVVsQk9NalZDWVRBNGNsRnNhR3BUYlZZeldYcGtRMVZVUms5bFJXUmhVV3hGTkU1VmR6VmtSM2N4WTBaa2NsTkZUbmRMTWxadFVqSkpNV1J1UmpKU1NFcHRWVlZhZEdNelJrWlhVemxIV1dwYVVWZFliSE5OYlU1RVZucHNSVmx1YUhWWFIzUnJZbFpqTVU5VVdsWlhiVkV6VDFadmNsZFlVbmRNTVdSdFpWZEtlVlpHYnpOaVIxSnpaRzVzYjJNd05XaGpTRlpYWTFST1VrNXNiSFZhTVdoMFRXNXdOVnBIWXpsUVUwbHpTVzFrYzJJeVNtaGlSWFJzWlZWc2EwbHFiMmxaYlZKc1dsUlZNazVVV1hkWk1scHBUa1JPYWs5WFNYbFBSMHB0VDFSb2JGbFhUbWhhYlVVeVRrUlphV1pSUFQwaWZRPT0ifQ== \ No newline at end of file diff --git a/e2e/playwright/tests/change-channel/test.spec.ts b/e2e/playwright/tests/change-channel/test.spec.ts new file mode 100644 index 0000000000..3ab19014aa --- /dev/null +++ b/e2e/playwright/tests/change-channel/test.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { login, uploadLicense } from '../shared'; + +const CUSTOMER_ID = '2k6jemHbYgZFqtwgyjqiVfjRQqi'; +const APP_ID = '2k6j65t0STtrZ1emyP5PUqBIQ23'; +const AUTOMATED_CHANNEL_ID = '2k6j62KPRQLjO0tF9zZB6zJJukg'; +const ALTERNATE_CHANNEL_ID = '2k6j61j49IPyDyQlbmRZJsxy3TP'; + +test('change channel', async ({ page }) => { + test.slow(); + await changeChannel(AUTOMATED_CHANNEL_ID); + await login(page); + await uploadLicense(page, expect); + await page.getByRole('button', { name: 'Deploy' }).click(); + await expect(page.locator('#app')).toContainText('Automated'); + await changeChannel(ALTERNATE_CHANNEL_ID); + + await page.getByText('Sync license').click(); + + await expect(page.getByLabel('Next step')).toContainText('License synced', { timeout: 10000 }); + await page.getByRole('button', { name: 'Ok, got it!' }).click(); + + await expect(page.locator('#app')).toContainText('Alternate'); + await expect(page.locator('#app')).toContainText('1.0.3', { timeout: 10000 }); + await expect(page.locator('#app')).toContainText('Upstream Update', { timeout: 10000 }); + + await page.getByRole('button', { name: 'Deploy', exact: true }).click(); + await page.getByRole('button', { name: 'Yes, Deploy' }).click(); + + await expect(page.locator('#app')).toContainText('Currently deployed version', { timeout: 15000 }); + await expect(page.getByText('v1.0.0')).not.toBeVisible(); + await expect(page.getByText('1.0.3')).toBeVisible(); +}); + +async function changeChannel(channelId: string) { + await fetch(`https://api.replicated.com/vendor/v3/customer/${CUSTOMER_ID}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': process.env.REPLICATED_API_TOKEN, + }, + body: JSON.stringify({ + "app_id": APP_ID, + "name": "github-action", // customer name + "channels": [ + { + "channel_id": channelId, + "pinned_channel_sequence": null, + "is_default_for_customer": true + } + ] + }) + }).then(response => { + if (!response.ok) { + throw new Error(`Unexpected status code: ${response.status}`); + } + }); +} \ No newline at end of file