diff --git a/api/controllers/app/controller.go b/api/controllers/app/controller.go index e44672e9bb..09b6780c50 100644 --- a/api/controllers/app/controller.go +++ b/api/controllers/app/controller.go @@ -15,12 +15,12 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/release" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" helmcli "helm.sh/helm/v3/pkg/cli" "sigs.k8s.io/controller-runtime/pkg/client" - kyaml "sigs.k8s.io/yaml" ) type Controller interface { @@ -176,10 +176,11 @@ func NewAppController(opts ...AppControllerOption) (*AppController, error) { return nil, err } - var license *kotsv1beta1.License + var license *licensewrapper.LicenseWrapper if len(controller.license) > 0 { - license = &kotsv1beta1.License{} - if err := kyaml.Unmarshal(controller.license, license); err != nil { + var err error + license, err = helpers.ParseLicenseFromBytes(controller.license) + if err != nil { return nil, fmt.Errorf("parse license: %w", err) } } diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index e6de4ee6c0..888ab74bae 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -1169,7 +1169,7 @@ func TestSetupInfra(t *testing.T) { appcontroller.WithStateMachine(sm), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(getTestReleaseData(&appConfig)), - appcontroller.WithLicense([]byte("spec:\n licenseID: test-license\n")), + appcontroller.WithLicense([]byte("apiVersion: kots.io/v1beta1\nkind: License\nspec:\n licenseID: test-license\n")), appcontroller.WithAppConfigManager(mockAppConfigManager), appcontroller.WithAppPreflightManager(mockAppPreflightManager), appcontroller.WithAppReleaseManager(mockAppReleaseManager), @@ -1187,7 +1187,7 @@ func TestSetupInfra(t *testing.T) { WithAllowIgnoreHostPreflights(tt.serverAllowIgnoreHostPreflights), WithMetricsReporter(mockMetricsReporter), WithReleaseData(getTestReleaseData(&appConfig)), - WithLicense([]byte("spec:\n licenseID: test-license\n")), + WithLicense([]byte("apiVersion: kots.io/v1beta1\nkind: License\nspec:\n licenseID: test-license\n")), WithStore(mockStore), WithHelmClient(&helm.MockClient{}), ) diff --git a/api/integration/app/install/config_test.go b/api/integration/app/install/config_test.go index dae5d1360f..4297c6aa2a 100644 --- a/api/integration/app/install/config_test.go +++ b/api/integration/app/install/config_test.go @@ -1051,7 +1051,7 @@ func TestAppInstallSuite(t *testing.T) { linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(initialState))), linuxinstall.WithReleaseData(rd), linuxinstall.WithHelmClient(&helm.MockClient{}), - linuxinstall.WithLicense([]byte("spec:\n licenseID: test-license\n")), + linuxinstall.WithLicense([]byte("apiVersion: kots.io/v1beta1\nkind: License\nspec:\n licenseID: test-license\n")), linuxinstall.WithConfigValues(configValues), ) require.NoError(t, err) @@ -1072,7 +1072,7 @@ func TestAppInstallSuite(t *testing.T) { kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(initialState))), kubernetesinstall.WithReleaseData(rd), kubernetesinstall.WithHelmClient(&helm.MockClient{}), - kubernetesinstall.WithLicense([]byte("spec:\n licenseID: test-license\n")), + kubernetesinstall.WithLicense([]byte("apiVersion: kots.io/v1beta1\nkind: License\nspec:\n licenseID: test-license\n")), kubernetesinstall.WithConfigValues(configValues), kubernetesinstall.WithKubernetesEnvSettings(helmcli.New()), ) diff --git a/api/internal/managers/app/config/manager.go b/api/internal/managers/app/config/manager.go index 7a6d981dc4..d1505e805c 100644 --- a/api/internal/managers/app/config/manager.go +++ b/api/internal/managers/app/config/manager.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -33,7 +34,7 @@ type appConfigManager struct { rawConfig kotsv1beta1.Config appConfigStore configstore.Store releaseData *release.ReleaseData - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper isAirgap bool privateCACertConfigMapName string kcli client.Client @@ -62,7 +63,7 @@ func WithReleaseData(releaseData *release.ReleaseData) AppConfigManagerOption { } } -func WithLicense(license *kotsv1beta1.License) AppConfigManagerOption { +func WithLicense(license *licensewrapper.LicenseWrapper) AppConfigManagerOption { return func(c *appConfigManager) { c.license = license } diff --git a/api/internal/managers/app/install/install.go b/api/internal/managers/app/install/install.go index 3c0608f5cc..323fe92eb4 100644 --- a/api/internal/managers/app/install/install.go +++ b/api/internal/managers/app/install/install.go @@ -12,6 +12,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" kyaml "sigs.k8s.io/yaml" ) @@ -44,8 +45,8 @@ func (m *appInstallManager) Install(ctx context.Context, configValues kotsv1beta } func (m *appInstallManager) install(ctx context.Context, configValues kotsv1beta1.ConfigValues) error { - license := &kotsv1beta1.License{} - if err := kyaml.Unmarshal(m.license, license); err != nil { + licenseWrapper, err := licensewrapper.LoadLicenseFromBytes(m.license) + if err != nil { return fmt.Errorf("parse license: %w", err) } @@ -61,7 +62,7 @@ func (m *appInstallManager) install(ctx context.Context, configValues kotsv1beta ecDomains := utils.GetDomains(m.releaseData) installOpts := kotscli.InstallOptions{ - AppSlug: license.Spec.AppSlug, + AppSlug: licenseWrapper.GetAppSlug(), License: m.license, Namespace: kotsadmNamespace, ClusterID: m.clusterID, diff --git a/api/internal/managers/app/install/install_test.go b/api/internal/managers/app/install/install_test.go index 8b9ca333d9..059cf9f3db 100644 --- a/api/internal/managers/app/install/install_test.go +++ b/api/internal/managers/app/install/install_test.go @@ -26,14 +26,13 @@ func TestAppInstallManager_Install(t *testing.T) { // Setup environment variable for V3 t.Setenv("ENABLE_V3", "1") - // Create test license - license := &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "test-app", - }, - } - licenseBytes, err := kyaml.Marshal(license) - require.NoError(t, err) + // Create test license with proper Kubernetes resource format + licenseYAML := `apiVersion: kots.io/v1beta1 +kind: License +spec: + appSlug: test-app +` + licenseBytes := []byte(licenseYAML) // Create test release data releaseData := &release.ReleaseData{ @@ -45,7 +44,7 @@ func TestAppInstallManager_Install(t *testing.T) { } // Set up release data globally so AppSlug() returns the correct value for v3 - err = release.SetReleaseDataForTests(map[string][]byte{ + err := release.SetReleaseDataForTests(map[string][]byte{ "channelrelease.yaml": []byte("# channel release object\nappSlug: test-app"), }) require.NoError(t, err) diff --git a/api/internal/managers/app/release/manager.go b/api/internal/managers/app/release/manager.go index 7c661c8ff6..819040b21a 100644 --- a/api/internal/managers/app/release/manager.go +++ b/api/internal/managers/app/release/manager.go @@ -11,6 +11,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/sirupsen/logrus" "sigs.k8s.io/controller-runtime/pkg/client" @@ -24,7 +25,7 @@ type AppReleaseManager interface { type appReleaseManager struct { rawConfig kotsv1beta1.Config releaseData *release.ReleaseData - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper isAirgap bool privateCACertConfigMapName string kcli client.Client @@ -60,7 +61,7 @@ func WithHelmClient(hcli helm.Client) AppReleaseManagerOption { } } -func WithLicense(license *kotsv1beta1.License) AppReleaseManagerOption { +func WithLicense(license *licensewrapper.LicenseWrapper) AppReleaseManagerOption { return func(m *appReleaseManager) { m.license = license } diff --git a/api/internal/managers/kubernetes/infra/install.go b/api/internal/managers/kubernetes/infra/install.go index 6aab5c4b10..40c59f946a 100644 --- a/api/internal/managers/kubernetes/infra/install.go +++ b/api/internal/managers/kubernetes/infra/install.go @@ -12,13 +12,13 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons" addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/support" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" - kyaml "sigs.k8s.io/yaml" ) func (m *infraManager) Install(ctx context.Context, ki kubernetesinstallation.Installation) (finalErr error) { @@ -67,8 +67,8 @@ func (m *infraManager) initInstallComponentsList() error { } func (m *infraManager) install(ctx context.Context, ki kubernetesinstallation.Installation) error { - license := &kotsv1beta1.License{} - if err := kyaml.Unmarshal(m.license, license); err != nil { + license, err := helpers.ParseLicenseFromBytes(m.license) + if err != nil { return fmt.Errorf("parse license: %w", err) } @@ -76,7 +76,7 @@ func (m *infraManager) install(ctx context.Context, ki kubernetesinstallation.In return fmt.Errorf("init components: %w", err) } - _, err := m.recordInstallation(ctx, m.kcli, license, ki) + _, err = m.recordInstallation(ctx, m.kcli, license, ki) if err != nil { return fmt.Errorf("record installation: %w", err) } @@ -97,13 +97,13 @@ func (m *infraManager) install(ctx context.Context, ki kubernetesinstallation.In return nil } -func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License, ki kubernetesinstallation.Installation) (*ecv1beta1.Installation, error) { +func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *licensewrapper.LicenseWrapper, ki kubernetesinstallation.Installation) (*ecv1beta1.Installation, error) { // TODO: we may need this later return nil, nil } -func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, license *kotsv1beta1.License, ki kubernetesinstallation.Installation) error { +func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, license *licensewrapper.LicenseWrapper, ki kubernetesinstallation.Installation) error { progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -148,7 +148,7 @@ func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mc return nil } -func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *kotsv1beta1.License, ki kubernetesinstallation.Installation) (addons.KubernetesInstallOptions, error) { +func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *licensewrapper.LicenseWrapper, ki kubernetesinstallation.Installation) (addons.KubernetesInstallOptions, error) { // TODO: We should not use the runtimeconfig package for kubernetes target installs. Since runtimeconfig.KotsadmNamespace is // target agnostic, we should move it to a package that can be used by both linux/kubernetes targets. kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, m.kcli) @@ -163,7 +163,7 @@ func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *kotsv1b TLSCertBytes: m.tlsConfig.CertBytes, TLSKeyBytes: m.tlsConfig.KeyBytes, Hostname: m.tlsConfig.Hostname, - IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, + IsMultiNodeEnabled: license.IsEmbeddedClusterMultiNodeEnabled(), EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), KotsadmNamespace: kotsadmNamespace, diff --git a/api/internal/managers/linux/infra/install.go b/api/internal/managers/linux/infra/install.go index 5d7bd3aced..64969fddc0 100644 --- a/api/internal/managers/linux/infra/install.go +++ b/api/internal/managers/linux/infra/install.go @@ -15,16 +15,16 @@ import ( addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/support" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" nodeutil "k8s.io/component-helpers/node/util" "sigs.k8s.io/controller-runtime/pkg/client" - kyaml "sigs.k8s.io/yaml" ) const K0sComponentName = "Runtime" @@ -72,10 +72,14 @@ func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConf return nil } -func (m *infraManager) initInstallComponentsList(license *kotsv1beta1.License) error { +func (m *infraManager) initInstallComponentsList(license *licensewrapper.LicenseWrapper) error { + if license == nil { + return fmt.Errorf("license is required for component initialization") + } + components := []types.InfraComponent{{Name: K0sComponentName}} - addOnsNames := addons.GetAddOnsNamesForInstall(m.airgapBundle != "", license.Spec.IsDisasterRecoverySupported) + addOnsNames := addons.GetAddOnsNamesForInstall(m.airgapBundle != "", license.IsDisasterRecoverySupported()) for _, addOnName := range addOnsNames { components = append(components, types.InfraComponent{Name: addOnName}) } @@ -91,8 +95,8 @@ func (m *infraManager) initInstallComponentsList(license *kotsv1beta1.License) e } func (m *infraManager) install(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { - license := &kotsv1beta1.License{} - if err := kyaml.Unmarshal(m.license, license); err != nil { + license, err := helpers.ParseLicenseFromBytes(m.license) + if err != nil { return fmt.Errorf("parse license: %w", err) } @@ -100,7 +104,7 @@ func (m *infraManager) install(ctx context.Context, rc runtimeconfig.RuntimeConf return fmt.Errorf("init components: %w", err) } - _, err := m.installK0s(ctx, rc) + _, err = m.installK0s(ctx, rc) if err != nil { return fmt.Errorf("install k0s: %w", err) } @@ -210,7 +214,7 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC return k0sCfg, nil } -func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (*ecv1beta1.Installation, error) { +func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *licensewrapper.LicenseWrapper, rc runtimeconfig.RuntimeConfig) (*ecv1beta1.Installation, error) { logFn := m.logFn("metadata") // get the configured custom domains @@ -246,7 +250,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien return in, nil } -func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) error { +func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, license *licensewrapper.LicenseWrapper, rc runtimeconfig.RuntimeConfig) error { progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -291,7 +295,11 @@ func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mc return nil } -func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (addons.InstallOptions, error) { +func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *licensewrapper.LicenseWrapper, rc runtimeconfig.RuntimeConfig) (addons.InstallOptions, error) { + if license == nil { + return addons.InstallOptions{}, fmt.Errorf("license is required for addon installation") + } + kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, m.kcli) if err != nil { return addons.InstallOptions{}, fmt.Errorf("get kotsadm namespace: %w", err) @@ -305,8 +313,8 @@ func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *kotsv1b TLSCertBytes: m.tlsConfig.CertBytes, TLSKeyBytes: m.tlsConfig.KeyBytes, Hostname: m.tlsConfig.Hostname, - DisasterRecoveryEnabled: license.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, + DisasterRecoveryEnabled: license.IsDisasterRecoverySupported(), + IsMultiNodeEnabled: license.IsEmbeddedClusterMultiNodeEnabled(), EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), ProxySpec: rc.ProxySpec(), diff --git a/api/internal/managers/linux/infra/install_test.go b/api/internal/managers/linux/infra/install_test.go index 0da6574663..4e90cc1f20 100644 --- a/api/internal/managers/linux/infra/install_test.go +++ b/api/internal/managers/linux/infra/install_test.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" ) @@ -63,6 +64,11 @@ func TestInfraManager_getAddonInstallOpts(t *testing.T) { }, } + // Wrap the license + wrappedLicense := &licensewrapper.LicenseWrapper{ + V1: license, + } + // Create infra manager manager := NewInfraManager( WithClusterID("test-cluster"), @@ -70,7 +76,7 @@ func TestInfraManager_getAddonInstallOpts(t *testing.T) { ) // Test the getAddonInstallOpts method with configValues passed as parameter - opts, err := manager.getAddonInstallOpts(t.Context(), license, rc) + opts, err := manager.getAddonInstallOpts(t.Context(), wrappedLicense, rc) assert.NoError(t, err) // Verify the install options diff --git a/api/internal/managers/linux/infra/upgrade.go b/api/internal/managers/linux/infra/upgrade.go index b39d9ad633..15210a5587 100644 --- a/api/internal/managers/linux/infra/upgrade.go +++ b/api/internal/managers/linux/infra/upgrade.go @@ -17,10 +17,9 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kyaml "sigs.k8s.io/yaml" ) // Upgrade performs the infrastructure upgrade by orchestrating the upgrade steps @@ -113,8 +112,8 @@ func (m *infraManager) newInstallationObj(ctx context.Context, registrySettings return nil, fmt.Errorf("get current installation: %w", err) } - license := &kotsv1beta1.License{} - if err := kyaml.Unmarshal(m.license, license); err != nil { + license, err := licensewrapper.LoadLicenseFromBytes(m.license) + if err != nil { return nil, fmt.Errorf("parse license: %w", err) } @@ -139,8 +138,8 @@ func (m *infraManager) newInstallationObj(ctx context.Context, registrySettings in.Spec.Artifacts = artifacts in.Spec.Config = m.getECConfigSpec() in.Spec.LicenseInfo = &ecv1beta1.LicenseInfo{ - IsDisasterRecoverySupported: license.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, + IsDisasterRecoverySupported: license.IsDisasterRecoverySupported(), + IsMultiNodeEnabled: license.IsEmbeddedClusterMultiNodeEnabled(), } return in, nil @@ -262,8 +261,8 @@ func (m *infraManager) upgradeK0s(ctx context.Context, in *ecv1beta1.Installatio } func (m *infraManager) distributeArtifacts(ctx context.Context, in *ecv1beta1.Installation, registrySettings *types.RegistrySettings) error { - license := &kotsv1beta1.License{} - if err := kyaml.Unmarshal(m.license, license); err != nil { + license, err := licensewrapper.LoadLicenseFromBytes(m.license) + if err != nil { return fmt.Errorf("parse license: %w", err) } @@ -287,7 +286,7 @@ func (m *infraManager) distributeArtifacts(ctx context.Context, in *ecv1beta1.In localArtifactMirrorImage = destImage } - return m.upgrader.DistributeArtifacts(ctx, in, localArtifactMirrorImage, license.Spec.LicenseID, appSlug, channelID, appVersion) + return m.upgrader.DistributeArtifacts(ctx, in, localArtifactMirrorImage, license.GetLicenseID(), appSlug, channelID, appVersion) } // destECImage returns the location to an EC image in the registry diff --git a/api/pkg/template/engine.go b/api/pkg/template/engine.go index a998e00380..edc22a76c2 100644 --- a/api/pkg/template/engine.go +++ b/api/pkg/template/engine.go @@ -10,6 +10,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -26,7 +27,7 @@ const ( type Engine struct { mode Mode config *kotsv1beta1.Config - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper releaseData *release.ReleaseData privateCACertConfigMapName string // ConfigMap name for private CA certificates, empty string if not available isAirgapInstallation bool // Whether the installation is an airgap installation @@ -55,7 +56,7 @@ func WithMode(mode Mode) EngineOption { } } -func WithLicense(license *kotsv1beta1.License) EngineOption { +func WithLicense(license *licensewrapper.LicenseWrapper) EngineOption { return func(e *Engine) { e.license = license } diff --git a/api/pkg/template/execute_test.go b/api/pkg/template/execute_test.go index 50e0b7a57f..5dca65eda0 100644 --- a/api/pkg/template/execute_test.go +++ b/api/pkg/template/execute_test.go @@ -11,12 +11,20 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/helpers" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kyaml "sigs.k8s.io/yaml" ) +// Helper function to wrap old-style license in LicenseWrapper for testing +func wrapLicenseForExecuteTests(license *kotsv1beta1.License) *licensewrapper.LicenseWrapper { + return &licensewrapper.LicenseWrapper{ + V1: license, + } +} + func TestEngine_BasicTemplating(t *testing.T) { config := &kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ @@ -615,7 +623,7 @@ func TestEngine_ComplexTemplate(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license)) + engine := NewEngine(config, WithLicense(wrapLicenseForExecuteTests(license))) // Test with user values overriding config values configValues := types.AppConfigValues{ diff --git a/api/pkg/template/license.go b/api/pkg/template/license.go index 16d229fd94..817cffb201 100644 --- a/api/pkg/template/license.go +++ b/api/pkg/template/license.go @@ -10,8 +10,30 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" ) -func (e *Engine) licenseFieldValue(name string) (string, error) { +// Helper methods for direct access (used by tests and other code) +func (e *Engine) LicenseAppSlug() string { + if e.license == nil { + return "" + } + return e.license.GetAppSlug() +} + +func (e *Engine) LicenseID() string { if e.license == nil { + return "" + } + return e.license.GetLicenseID() +} + +func (e *Engine) LicenseIsEmbeddedClusterDownloadEnabled() bool { + if e.license == nil { + return false + } + return e.license.IsEmbeddedClusterDownloadEnabled() +} + +func (e *Engine) licenseFieldValue(name string) (string, error) { + if e.license == nil || (e.license.V1 == nil && e.license.V2 == nil) { return "", fmt.Errorf("license is nil") } @@ -19,39 +41,39 @@ func (e *Engine) licenseFieldValue(name string) (string, error) { // when adding new values switch name { case "isSnapshotSupported": - return fmt.Sprintf("%t", e.license.Spec.IsSnapshotSupported), nil + return fmt.Sprintf("%t", e.license.IsSnapshotSupported()), nil case "IsDisasterRecoverySupported": - return fmt.Sprintf("%t", e.license.Spec.IsDisasterRecoverySupported), nil + return fmt.Sprintf("%t", e.license.IsDisasterRecoverySupported()), nil case "isGitOpsSupported": - return fmt.Sprintf("%t", e.license.Spec.IsGitOpsSupported), nil + return fmt.Sprintf("%t", e.license.IsGitOpsSupported()), nil case "isSupportBundleUploadSupported": - return fmt.Sprintf("%t", e.license.Spec.IsSupportBundleUploadSupported), nil + return fmt.Sprintf("%t", e.license.IsSupportBundleUploadSupported()), nil case "isEmbeddedClusterMultiNodeEnabled": - return fmt.Sprintf("%t", e.license.Spec.IsEmbeddedClusterMultiNodeEnabled), nil + return fmt.Sprintf("%t", e.license.IsEmbeddedClusterMultiNodeEnabled()), nil case "isIdentityServiceSupported": - return fmt.Sprintf("%t", e.license.Spec.IsIdentityServiceSupported), nil + return fmt.Sprintf("%t", e.license.IsIdentityServiceSupported()), nil case "isGeoaxisSupported": - return fmt.Sprintf("%t", e.license.Spec.IsGeoaxisSupported), nil + return fmt.Sprintf("%t", e.license.IsGeoaxisSupported()), nil case "isAirgapSupported": - return fmt.Sprintf("%t", e.license.Spec.IsAirgapSupported), nil + return fmt.Sprintf("%t", e.license.IsAirgapSupported()), nil case "licenseType": - return e.license.Spec.LicenseType, nil + return e.license.GetLicenseType(), nil case "licenseSequence": - return fmt.Sprintf("%d", e.license.Spec.LicenseSequence), nil + return fmt.Sprintf("%d", e.license.GetLicenseSequence()), nil case "signature": - return string(e.license.Spec.Signature), nil + return string(e.license.GetSignature()), nil case "appSlug": - return e.license.Spec.AppSlug, nil + return e.license.GetAppSlug(), nil case "channelID": - return e.license.Spec.ChannelID, nil + return e.license.GetChannelID(), nil case "channelName": - return e.license.Spec.ChannelName, nil + return e.license.GetChannelName(), nil case "isSemverRequired": - return fmt.Sprintf("%t", e.license.Spec.IsSemverRequired), nil + return fmt.Sprintf("%t", e.license.IsSemverRequired()), nil case "customerName": - return e.license.Spec.CustomerName, nil + return e.license.GetCustomerName(), nil case "licenseID", "licenseId": - return e.license.Spec.LicenseID, nil + return e.license.GetLicenseID(), nil case "endpoint": if e.releaseData == nil { return "", fmt.Errorf("release data is nil") @@ -59,16 +81,18 @@ func (e *Engine) licenseFieldValue(name string) (string, error) { ecDomains := utils.GetDomains(e.releaseData) return netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), nil default: - entitlement, ok := e.license.Spec.Entitlements[name] + entitlements := e.license.GetEntitlements() + entitlement, ok := entitlements[name] if ok { - return fmt.Sprintf("%v", entitlement.Value.Value()), nil + val := entitlement.GetValue() + return fmt.Sprintf("%v", val.Value()), nil } return "", nil } } func (e *Engine) licenseDockerCfg() (string, error) { - if e.license == nil { + if e.license == nil || (e.license.V1 == nil && e.license.V2 == nil) { return "", fmt.Errorf("license is nil") } if e.releaseData == nil { @@ -78,7 +102,8 @@ func (e *Engine) licenseDockerCfg() (string, error) { return "", fmt.Errorf("channel release is nil") } - auth := fmt.Sprintf("%s:%s", e.license.Spec.LicenseID, e.license.Spec.LicenseID) + licenseID := e.license.GetLicenseID() + auth := fmt.Sprintf("%s:%s", licenseID, licenseID) encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) registryProxyInfo := getRegistryProxyInfo(e.releaseData) @@ -116,7 +141,7 @@ func getRegistryProxyInfo(releaseData *release.ReleaseData) *registryProxyInfo { } func (e *Engine) channelName() (string, error) { - if e.license == nil { + if e.license == nil || (e.license.V1 == nil && e.license.V2 == nil) { return "", fmt.Errorf("license is nil") } if e.releaseData == nil { @@ -126,13 +151,13 @@ func (e *Engine) channelName() (string, error) { return "", fmt.Errorf("channel release is nil") } - for _, channel := range e.license.Spec.Channels { + for _, channel := range e.license.GetChannels() { if channel.ChannelID == e.releaseData.ChannelRelease.ChannelID { return channel.ChannelName, nil } } - if e.license.Spec.ChannelID == e.releaseData.ChannelRelease.ChannelID { - return e.license.Spec.ChannelName, nil + if e.license.GetChannelID() == e.releaseData.ChannelRelease.ChannelID { + return e.license.GetChannelName(), nil } return "", fmt.Errorf("channel %s not found in license", e.releaseData.ChannelRelease.ChannelID) } diff --git a/api/pkg/template/license_test.go b/api/pkg/template/license_test.go index 9f7c0aa1ed..8dc381e95d 100644 --- a/api/pkg/template/license_test.go +++ b/api/pkg/template/license_test.go @@ -9,10 +9,18 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Helper function to wrap old-style license in LicenseWrapper for testing +func wrapLicense(license *kotsv1beta1.License) *licensewrapper.LicenseWrapper { + return &licensewrapper.LicenseWrapper{ + V1: license, + } +} + func TestEngine_LicenseFieldValue(t *testing.T) { license := &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ @@ -67,7 +75,7 @@ func TestEngine_LicenseFieldValue(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license)) + engine := NewEngine(config, WithLicense(wrapLicense(license))) // Test basic license fields testCases := []struct { @@ -157,7 +165,7 @@ func TestEngine_LicenseFieldValue_Endpoint(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl LicenseFieldValue \"endpoint\" }}") require.NoError(t, err) @@ -180,7 +188,7 @@ func TestEngine_LicenseFieldValue_EndpointWithoutReleaseData(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license)) + engine := NewEngine(config, WithLicense(wrapLicense(license))) err := engine.Parse("{{repl LicenseFieldValue \"endpoint\" }}") require.NoError(t, err) @@ -216,7 +224,7 @@ func TestEngine_LicenseDockerCfg(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl LicenseDockerCfg }}") require.NoError(t, err) @@ -274,7 +282,7 @@ func TestEngine_LicenseDockerCfgWithoutReleaseData(t *testing.T) { }, } - engine := NewEngine(nil, WithLicense(license)) + engine := NewEngine(nil, WithLicense(wrapLicense(license))) err := engine.Parse("{{repl LicenseDockerCfg }}") require.NoError(t, err) @@ -304,7 +312,7 @@ func TestEngine_LicenseDockerCfgStagingEndpoint(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl LicenseDockerCfg }}") require.NoError(t, err) @@ -367,7 +375,7 @@ func TestEngine_LicenseDockerCfgStagingEndpointWithReleaseData(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl LicenseDockerCfg }}") require.NoError(t, err) @@ -439,7 +447,7 @@ func TestEngine_ChannelName(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl ChannelName }}") require.NoError(t, err) @@ -479,7 +487,7 @@ func TestEngine_ChannelName_FallbackToLicenseChannel(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl ChannelName }}") require.NoError(t, err) @@ -519,7 +527,7 @@ func TestEngine_ChannelName_WithoutReleaseData(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license)) + engine := NewEngine(config, WithLicense(wrapLicense(license))) err := engine.Parse("{{repl ChannelName }}") require.NoError(t, err) @@ -550,7 +558,7 @@ func TestEngine_ChannelName_WithoutChannelRelease(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl ChannelName }}") require.NoError(t, err) @@ -590,7 +598,7 @@ func TestEngine_ChannelName_ChannelNotFound(t *testing.T) { }, } - engine := NewEngine(config, WithLicense(license), WithReleaseData(releaseData)) + engine := NewEngine(config, WithLicense(wrapLicense(license)), WithReleaseData(releaseData)) err := engine.Parse("{{repl ChannelName }}") require.NoError(t, err) @@ -598,3 +606,113 @@ func TestEngine_ChannelName_ChannelNotFound(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "channel unknown-channel-id not found in license") } + +func TestEngine_LicenseWrapper(t *testing.T) { + licenseV1Beta1 := `apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id-v1 + licenseType: dev + customerName: Test Customer V1 + customerEmail: test@example.com + endpoint: https://replicated.app + channelID: test-channel-id + channelName: Stable + licenseSequence: 1 + isAirgapSupported: true + isGitOpsSupported: false + isIdentityServiceSupported: false + isGeoaxisSupported: false + isSnapshotSupported: true + isSupportBundleUploadSupported: true + isSemverRequired: true + isDisasterRecoverySupported: true + isEmbeddedClusterDownloadEnabled: true + isEmbeddedClusterMultiNodeEnabled: true + replicatedProxyDomain: proxy.replicated.com + entitlements: + expires_at: + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: {} + channels: [] + signature: dGVzdC1saWNlbnNlLXNpZ25hdHVyZQ== +` + + licenseV1Beta2 := `apiVersion: kots.io/v1beta2 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id-v2 + licenseType: dev + customerName: Test Customer V2 + customerEmail: test@example.com + endpoint: https://replicated.app + channelID: test-channel-id + channelName: Stable + licenseSequence: 1 + isAirgapSupported: true + isGitOpsSupported: false + isIdentityServiceSupported: false + isGeoaxisSupported: false + isSnapshotSupported: true + isSupportBundleUploadSupported: true + isSemverRequired: true + isDisasterRecoverySupported: true + isEmbeddedClusterDownloadEnabled: true + isEmbeddedClusterMultiNodeEnabled: true + replicatedProxyDomain: proxy.replicated.com + entitlements: + expires_at: + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: {} + channels: [] + signature: dGVzdC1saWNlbnNlLXNpZ25hdHVyZQ== +` + + tests := []struct { + name string + licenseData string + wantAppSlug string + wantLicenseID string + wantECEnabled bool + }{ + { + name: "v1beta1 license", + licenseData: licenseV1Beta1, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v1", + wantECEnabled: true, + }, + { + name: "v1beta2 license", + licenseData: licenseV1Beta2, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v2", + wantECEnabled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wrapper, err := licensewrapper.LoadLicenseFromBytes([]byte(tt.licenseData)) + require.NoError(t, err) + + engine := NewEngine(nil, WithLicense(&wrapper)) + + assert.Equal(t, tt.wantAppSlug, engine.LicenseAppSlug()) + assert.Equal(t, tt.wantLicenseID, engine.LicenseID()) + assert.Equal(t, tt.wantECEnabled, engine.LicenseIsEmbeddedClusterDownloadEnabled()) + }) + } +} diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index fa746d9a05..7658c79c8a 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -43,6 +43,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/versions" "github.com/replicatedhq/embedded-cluster/web" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -91,7 +92,7 @@ type installConfig struct { isAirgap bool enableManagerExperience bool licenseBytes []byte - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper airgapMetadata *airgap.AirgapMetadata embeddedAssetsSize int64 endUserConfig *ecv1beta1.Config @@ -136,9 +137,14 @@ func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { return err } + // Verify license is available for metrics reporting + if installCfg.license == nil { + return fmt.Errorf("license is required for installation") + } + metricsReporter := newInstallReporter( replicatedAppURL(), cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), - installCfg.license.Spec.LicenseID, installCfg.clusterID, installCfg.license.Spec.AppSlug, + installCfg.license.GetLicenseID(), installCfg.clusterID, installCfg.license.GetAppSlug(), ) metricsReporter.ReportInstallationStarted(ctx) @@ -379,7 +385,7 @@ func preRunInstall(cmd *cobra.Command, flags *installFlags, rc runtimeconfig.Run // sync the license if we are in the manager experience and a license is provided and we are // not in airgap mode - if installCfg.enableManagerExperience && installCfg.license != nil && !installCfg.isAirgap { + if installCfg.enableManagerExperience && installCfg.license.GetLicenseID() != "" && !installCfg.isAirgap { replicatedAPI, err := newReplicatedAPIClient(installCfg.license, installCfg.clusterID) if err != nil { return nil, fmt.Errorf("failed to create replicated API client: %w", err) @@ -705,6 +711,11 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags installF return nil, fmt.Errorf("get kotsadm namespace: %w", err) } + // Verify license is available before configuring install options + if installCfg.license == nil { + return nil, fmt.Errorf("license is required for installation") + } + opts := &addons.InstallOptions{ ClusterID: installCfg.clusterID, AdminConsolePwd: flags.adminConsolePassword, @@ -714,8 +725,8 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags installF TLSCertBytes: installCfg.tlsCertBytes, TLSKeyBytes: installCfg.tlsKeyBytes, Hostname: flags.hostname, - DisasterRecoveryEnabled: installCfg.license.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: installCfg.license.Spec.IsEmbeddedClusterMultiNodeEnabled, + DisasterRecoveryEnabled: installCfg.license.IsDisasterRecoverySupported(), + IsMultiNodeEnabled: installCfg.license.IsEmbeddedClusterMultiNodeEnabled(), EmbeddedConfigSpec: embCfgSpec, EndUserConfigSpec: euCfgSpec, ProxySpec: rc.ProxySpec(), @@ -726,8 +737,12 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags installF OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), ServiceCIDR: rc.ServiceCIDR(), KotsInstaller: func() error { + // License already validated above, but check defensively + if installCfg.license == nil { + return fmt.Errorf("license is required for KOTS installation") + } opts := kotscli.InstallOptions{ - AppSlug: installCfg.license.Spec.AppSlug, + AppSlug: installCfg.license.GetAppSlug(), License: installCfg.licenseBytes, Namespace: kotsadmNamespace, ClusterID: installCfg.clusterID, @@ -835,28 +850,28 @@ func ensureAdminConsolePassword(flags *installFlags) error { return nil } -func verifyLicense(license *kotsv1beta1.License) error { +func verifyLicense(license *licensewrapper.LicenseWrapper) error { rel := release.GetChannelRelease() // handle the three cases that do not require parsing the license file // 1. no release and no license, which is OK // 2. no license and a release, which is not OK // 3. a license and no release, which is not OK - if rel == nil && license == nil { + if rel == nil && (license == nil || license.GetLicenseID() == "") { // no license and no release, this is OK return nil } else if rel == nil && license != nil { // license is present but no release, this means we would install without vendor charts and k0s overrides return fmt.Errorf("a license was provided but no release was found in binary, please rerun without the license flag") - } else if rel != nil && license == nil { + } else if rel != nil && (license == nil || license.GetLicenseID() == "") { // release is present but no license, this is not OK return fmt.Errorf("no license was provided for %s and one is required, please rerun with '--license '", rel.AppSlug) } // Check if the license matches the application version data - if rel.AppSlug != license.Spec.AppSlug { + if rel.AppSlug != license.GetAppSlug() { // if the app is different, we will not be able to provide the correct vendor supplied charts and k0s overrides - return fmt.Errorf("license app %s does not match binary app %s, please provide the correct license", license.Spec.AppSlug, rel.AppSlug) + return fmt.Errorf("license app %s does not match binary app %s, please provide the correct license", license.GetAppSlug(), rel.AppSlug) } // Ensure the binary channel actually is present in the supplied license @@ -864,18 +879,23 @@ func verifyLicense(license *kotsv1beta1.License) error { return err } - if license.Spec.Entitlements["expires_at"].Value.StrVal != "" { - // read the expiration date, and check it against the current date - expiration, err := time.Parse(time.RFC3339, license.Spec.Entitlements["expires_at"].Value.StrVal) - if err != nil { - return fmt.Errorf("parse expiration date: %w", err) - } - if time.Now().After(expiration) { - return fmt.Errorf("license expired on %s, please provide a valid license", expiration) + entitlements := license.GetEntitlements() + if expiresAtField, ok := entitlements["expires_at"]; ok { + entValue := expiresAtField.GetValue() + expiresAtValue := entValue.Value() + if expiresAtStr, ok := expiresAtValue.(string); ok && expiresAtStr != "" { + // read the expiration date, and check it against the current date + expiration, err := time.Parse(time.RFC3339, expiresAtStr) + if err != nil { + return fmt.Errorf("parse expiration date: %w", err) + } + if time.Now().After(expiration) { + return fmt.Errorf("license expired on %s, please provide a valid license", expiration) + } } } - if !license.Spec.IsEmbeddedClusterDownloadEnabled { + if !license.IsEmbeddedClusterDownloadEnabled() { return fmt.Errorf("license does not have embedded cluster enabled, please provide a valid license") } @@ -884,15 +904,19 @@ func verifyLicense(license *kotsv1beta1.License) error { // checkChannelExistence verifies that a channel exists in a supplied license, returning a user-friendly // error message actually listing available channels, if it does not. -func checkChannelExistence(license *kotsv1beta1.License, rel *release.ChannelRelease) error { +func checkChannelExistence(license *licensewrapper.LicenseWrapper, rel *release.ChannelRelease) error { + if license == nil { + return fmt.Errorf("license is nil") + } var allowedChannels []string channelExists := false - if len(license.Spec.Channels) == 0 { // support pre-multichannel licenses - allowedChannels = append(allowedChannels, fmt.Sprintf("%s (%s)", license.Spec.ChannelName, license.Spec.ChannelID)) - channelExists = license.Spec.ChannelID == rel.ChannelID + channels := license.GetChannels() + if len(channels) == 0 { // support pre-multichannel licenses + allowedChannels = append(allowedChannels, fmt.Sprintf("%s (%s)", license.GetChannelName(), license.GetChannelID())) + channelExists = license.GetChannelID() == rel.ChannelID } else { - for _, channel := range license.Spec.Channels { + for _, channel := range channels { allowedChannels = append(allowedChannels, fmt.Sprintf("%s (%s)", channel.ChannelSlug, channel.ChannelID)) if channel.ChannelID == rel.ChannelID { channelExists = true @@ -1117,7 +1141,7 @@ func checkAirgapMatches(airgapInfo *kotsv1beta1.Airgap) error { // maybePromptForAppUpdate warns the user if the embedded release is not the latest for the current // channel. If stdout is a terminal, it will prompt the user to continue installing the out-of-date // release and return an error if the user chooses not to continue. -func maybePromptForAppUpdate(ctx context.Context, prompt prompts.Prompt, license *kotsv1beta1.License, assumeYes bool) error { +func maybePromptForAppUpdate(ctx context.Context, prompt prompts.Prompt, license *licensewrapper.LicenseWrapper, assumeYes bool) error { channelRelease := release.GetChannelRelease() if channelRelease == nil { // It is possible to install without embedding the release data. In this case, we cannot @@ -1125,7 +1149,7 @@ func maybePromptForAppUpdate(ctx context.Context, prompt prompts.Prompt, license return nil } - if license == nil { + if license == nil || license.GetLicenseID() == "" { return errors.New("license required") } @@ -1149,7 +1173,7 @@ func maybePromptForAppUpdate(ctx context.Context, prompt prompts.Prompt, license logrus.Infof( "To download it, run:\n curl -fL \"%s\" \\\n -H \"Authorization: %s\" \\\n -o %s-%s.tgz\n", releaseURL, - license.Spec.LicenseID, + license.GetLicenseID(), channelRelease.AppSlug, channelRelease.ChannelSlug, ) @@ -1263,15 +1287,15 @@ func normalizeNoPromptToYes(f *pflag.FlagSet, name string) pflag.NormalizedName return pflag.NormalizedName(name) } -func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkInterface string, rc runtimeconfig.RuntimeConfig, isHeadlessInstall bool) { +func printSuccessMessage(license *licensewrapper.LicenseWrapper, hostname string, networkInterface string, rc runtimeconfig.RuntimeConfig, isHeadlessInstall bool) { adminConsoleURL := getAdminConsoleURL(hostname, networkInterface, rc.AdminConsolePort()) // Create the message content var message string if isHeadlessInstall { - message = fmt.Sprintf("The Admin Console for %s is available at:", license.Spec.AppSlug) + message = fmt.Sprintf("The Admin Console for %s is available at:", license.GetAppSlug()) } else { - message = fmt.Sprintf("Visit the Admin Console to configure and install %s:", license.Spec.AppSlug) + message = fmt.Sprintf("Visit the Admin Console to configure and install %s:", license.GetAppSlug()) } // Determine the length of the longest line diff --git a/cmd/installer/cli/install_config_test.go b/cmd/installer/cli/install_config_test.go index 13d846b84f..c780ff7feb 100644 --- a/cmd/installer/cli/install_config_test.go +++ b/cmd/installer/cli/install_config_test.go @@ -588,7 +588,7 @@ spec: os.WriteFile(invalidPath, []byte("this is not a valid license file"), 0644) return invalidPath }(), - wantErr: "failed to parse the license file", + wantErr: "failed to parse license file", expectLicense: false, }, { @@ -602,7 +602,7 @@ metadata: os.WriteFile(wrongKindPath, []byte(wrongKindData), 0644) return wrongKindPath }(), - wantErr: "failed to parse the license file", + wantErr: "failed to parse license file", expectLicense: false, }, { @@ -618,7 +618,7 @@ spec: os.WriteFile(corruptPath, []byte(corruptData), 0644) return corruptPath }(), - wantErr: "failed to parse the license file", + wantErr: "failed to parse license file", expectLicense: false, }, { @@ -646,8 +646,8 @@ spec: if tt.expectLicense { assert.NotEmpty(t, installCfg.licenseBytes, "License bytes should be populated") assert.NotNil(t, installCfg.license, "License should be parsed") - assert.Equal(t, "test-license-id", installCfg.license.Spec.LicenseID) - assert.Equal(t, "test-app", installCfg.license.Spec.AppSlug) + assert.Equal(t, "test-license-id", installCfg.license.GetLicenseID()) + assert.Equal(t, "test-app", installCfg.license.GetAppSlug()) } else { assert.Empty(t, installCfg.licenseBytes, "License bytes should be empty") assert.Nil(t, installCfg.license, "License should be nil") diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index f8c1ecd6c8..3e7a3d3b65 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -7,14 +7,17 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/prompts/plain" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -320,7 +323,10 @@ func Test_maybePromptForAppUpdate(t *testing.T) { prompts.SetTerminal(true) t.Cleanup(func() { prompts.SetTerminal(false) }) - err = maybePromptForAppUpdate(context.Background(), prompt, license, tt.assumeYes) + // Wrap the license for the new API + wrappedLicense := &licensewrapper.LicenseWrapper{V1: license} + err = maybePromptForAppUpdate(context.Background(), prompt, wrappedLicense, tt.assumeYes) + if tt.wantErr { require.Error(t, err) } else { @@ -365,10 +371,10 @@ func getReleasesHandler(t *testing.T, channelID string, apiHandler http.HandlerF func Test_verifyLicense(t *testing.T) { tests := []struct { - name string - license *kotsv1beta1.License - wantErr string - useRelease bool + name string + licenseContents string + wantErr string + useRelease bool }{ { name: "no license, no release", @@ -381,160 +387,181 @@ func Test_verifyLicense(t *testing.T) { }, { name: "valid license, no release", - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: true, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-no-release + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + `, wantErr: "a license was provided but no release was found in binary, please rerun without the license flag", }, { name: "valid license, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: true, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-valid + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + `, }, { name: "valid multi-channel license, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "OtherChannelID", - IsEmbeddedClusterDownloadEnabled: true, - Channels: []kotsv1beta1.Channel{ - { - ChannelID: "OtherChannelID", - ChannelName: "OtherChannel", - ChannelSlug: "other-channel", - IsDefault: true, - }, - { - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - ChannelName: "ExpectedChannel", - ChannelSlug: "expected-channel", - IsDefault: false, - }, - }, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-multichannel + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "OtherChannelID" + isEmbeddedClusterDownloadEnabled: true + channels: + - channelID: OtherChannelID + channelName: OtherChannel + channelSlug: other-channel + isDefault: true + - channelID: 2cHXb1RCttzpR0xvnNWyaZCgDBP + channelName: ExpectedChannel + channelSlug: expected-channel + isDefault: false + `, }, { name: "expired license, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: true, - Entitlements: map[string]kotsv1beta1.EntitlementField{ - "expires_at": { - Value: kotsv1beta1.EntitlementValue{ - Type: kotsv1beta1.String, - StrVal: "2024-06-03T00:00:00Z", - }, - }, - }, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-expired + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + entitlements: + expires_at: + description: License Expiration + signature: {} + title: Expiration + value: "2024-06-03T00:00:00Z" + valueType: String + `, wantErr: "license expired on 2024-06-03 00:00:00 +0000 UTC, please provide a valid license", }, { name: "license with no expiration, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: true, - Entitlements: map[string]kotsv1beta1.EntitlementField{ - "expires_at": { - Value: kotsv1beta1.EntitlementValue{ - Type: kotsv1beta1.String, - StrVal: "", - }, - }, - }, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-no-expiration + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + entitlements: + expires_at: + description: License Expiration + signature: {} + title: Expiration + value: "" + valueType: String + `, }, { name: "license with 100 year expiration, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: true, - Entitlements: map[string]kotsv1beta1.EntitlementField{ - "expires_at": { - Value: kotsv1beta1.EntitlementValue{ - Type: kotsv1beta1.String, - StrVal: "2124-06-03T00:00:00Z", - }, - }, - }, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-100year + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + entitlements: + expires_at: + description: License Expiration + signature: {} + title: Expiration + value: "2124-06-03T00:00:00Z" + valueType: String + `, }, { name: "embedded cluster not enabled, with release", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", - IsEmbeddedClusterDownloadEnabled: false, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-no-ec + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: false + `, wantErr: "license does not have embedded cluster enabled, please provide a valid license", }, { name: "incorrect license (multichan license)", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", - IsEmbeddedClusterDownloadEnabled: false, - Channels: []kotsv1beta1.Channel{ - { - ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", - ChannelName: "Stable", - ChannelSlug: "stable", - IsDefault: true, - }, - { - ChannelID: "4l9fCbxTNIhuAOaC6MoKMVeV3K", - ChannelName: "Alternate", - ChannelSlug: "alternate", - IsDefault: false, - }, - }, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-multichan + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK" + isEmbeddedClusterDownloadEnabled: false + channels: + - channelID: 2i9fCbxTNIhuAOaC6MoKMVeGzuK + channelName: Stable + channelSlug: stable + isDefault: true + - channelID: 4l9fCbxTNIhuAOaC6MoKMVeV3K + channelName: Alternate + channelSlug: alternate + isDefault: false + `, wantErr: "binary channel 2cHXb1RCttzpR0xvnNWyaZCgDBP (CI) not present in license, channels allowed by license are: stable (2i9fCbxTNIhuAOaC6MoKMVeGzuK), alternate (4l9fCbxTNIhuAOaC6MoKMVeV3K)", }, { name: "incorrect license (pre-multichan license)", useRelease: true, - license: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "embedded-cluster-smoke-test-staging-app", - ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", - ChannelName: "Stable", - IsEmbeddedClusterDownloadEnabled: false, - }, - }, + licenseContents: `apiVersion: kots.io/v1beta1 +kind: License +spec: + licenseID: test-license-premultichan + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK" + channelName: "Stable" + isEmbeddedClusterDownloadEnabled: false + `, wantErr: "binary channel 2cHXb1RCttzpR0xvnNWyaZCgDBP (CI) not present in license, channels allowed by license are: Stable (2i9fCbxTNIhuAOaC6MoKMVeGzuK)", }, + { + name: "v1beta2 license, with release", + useRelease: true, + licenseContents: `apiVersion: kots.io/v1beta2 +kind: License +spec: + licenseID: test-license-v1beta2 + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: true + `, + }, + { + name: "v1beta2 license without EC enabled, with release", + useRelease: true, + licenseContents: `apiVersion: kots.io/v1beta2 +kind: License +spec: + licenseID: test-license-v1beta2-no-ec + appSlug: embedded-cluster-smoke-test-staging-app + channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" + isEmbeddedClusterDownloadEnabled: false + `, + wantErr: "license does not have embedded cluster enabled, please provide a valid license", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -557,7 +584,20 @@ versionLabel: testversion release.SetReleaseDataForTests(nil) }) - err = verifyLicense(tt.license) + // Parse license contents into wrapper + var license *licensewrapper.LicenseWrapper + if tt.licenseContents != "" { + tmpdir := t.TempDir() + licensePath := filepath.Join(tmpdir, "license.yaml") + err := os.WriteFile(licensePath, []byte(tt.licenseContents), 0644) + req.NoError(err) + + license, err = helpers.ParseLicense(licensePath) + req.NoError(err) + } + + err = verifyLicense(license) + if tt.wantErr != "" { req.EqualError(err, tt.wantErr) } else { diff --git a/cmd/installer/cli/release.go b/cmd/installer/cli/release.go index 92cb8ae308..5250bf93db 100644 --- a/cmd/installer/cli/release.go +++ b/cmd/installer/cli/release.go @@ -10,7 +10,7 @@ import ( "net/url" "time" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" ) type apiChannelRelease struct { @@ -26,20 +26,25 @@ type apiChannelRelease struct { ReplicatedProxyDomain string `json:"replicatedProxyDomain"` } -func getCurrentAppChannelRelease(ctx context.Context, license *kotsv1beta1.License, channelID string) (*apiChannelRelease, error) { +func getCurrentAppChannelRelease(ctx context.Context, license *licensewrapper.LicenseWrapper, channelID string) (*apiChannelRelease, error) { + if license == nil { + return nil, fmt.Errorf("license is required") + } + query := url.Values{} query.Set("selectedChannelId", channelID) query.Set("channelSequence", "") // sending an empty string will return the latest channel release query.Set("isSemverSupported", "true") apiURL := replicatedAppURL() - url := fmt.Sprintf("%s/release/%s/pending?%s", apiURL, license.Spec.AppSlug, query.Encode()) + url := fmt.Sprintf("%s/release/%s/pending?%s", apiURL, license.GetAppSlug(), query.Encode()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } - auth := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", license.Spec.LicenseID, license.Spec.LicenseID)))) + licenseID := license.GetLicenseID() + auth := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", licenseID, licenseID)))) req.Header.Set("Authorization", auth) // This will use the proxy from the environment if set by the cli command. diff --git a/cmd/installer/cli/release_test.go b/cmd/installer/cli/release_test.go index 758fe410d3..3e5d602ab9 100644 --- a/cmd/installer/cli/release_test.go +++ b/cmd/installer/cli/release_test.go @@ -9,6 +9,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -91,7 +92,10 @@ func Test_getCurrentAppChannelRelease(t *testing.T) { }, } - got, err := getCurrentAppChannelRelease(context.Background(), license, tt.args.channelID) + // Wrap the license for the new API + wrappedLicense := &licensewrapper.LicenseWrapper{V1: license} + + got, err := getCurrentAppChannelRelease(context.Background(), wrappedLicense, tt.args.channelID) if tt.wantErr { require.Error(t, err) } else { diff --git a/cmd/installer/cli/replicatedapi.go b/cmd/installer/cli/replicatedapi.go index 097a333e7f..a4c8191fb2 100644 --- a/cmd/installer/cli/replicatedapi.go +++ b/cmd/installer/cli/replicatedapi.go @@ -7,7 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/replicatedapi" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" ) @@ -21,7 +21,8 @@ func proxyRegistryURL() string { return netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain) } -func newReplicatedAPIClient(license *kotsv1beta1.License, clusterID string) (replicatedapi.Client, error) { +func newReplicatedAPIClient(license *licensewrapper.LicenseWrapper, clusterID string) (replicatedapi.Client, error) { + // Pass the wrapper directly - the API client now handles both v1beta1 and v1beta2 return replicatedapi.NewClient( replicatedAppURL(), license, @@ -30,7 +31,7 @@ func newReplicatedAPIClient(license *kotsv1beta1.License, clusterID string) (rep ) } -func syncLicense(ctx context.Context, client replicatedapi.Client, license *kotsv1beta1.License) (*kotsv1beta1.License, []byte, error) { +func syncLicense(ctx context.Context, client replicatedapi.Client, license *licensewrapper.LicenseWrapper) (*licensewrapper.LicenseWrapper, []byte, error) { logrus.Debug("Syncing license") updatedLicense, licenseBytes, err := client.SyncLicense(ctx) @@ -38,13 +39,16 @@ func syncLicense(ctx context.Context, client replicatedapi.Client, license *kots return nil, nil, fmt.Errorf("get latest license: %w", err) } - if updatedLicense.Spec.LicenseSequence != license.Spec.LicenseSequence { - logrus.Debugf("License synced successfully (sequence %d -> %d)", - license.Spec.LicenseSequence, - updatedLicense.Spec.LicenseSequence) - } else { - logrus.Debug("License is already up to date") + if license != nil { + oldSeq := license.GetLicenseSequence() + newSeq := updatedLicense.GetLicenseSequence() + if newSeq != oldSeq { + logrus.Debugf("License synced successfully (sequence %d -> %d)", oldSeq, newSeq) + } else { + logrus.Debug("License is already up to date") + } } + // Return wrapper directly - already wrapped by SyncLicense return updatedLicense, licenseBytes, nil } diff --git a/cmd/installer/cli/upgrade.go b/cmd/installer/cli/upgrade.go index 23afe25b35..90630e16d6 100644 --- a/cmd/installer/cli/upgrade.go +++ b/cmd/installer/cli/upgrade.go @@ -26,7 +26,7 @@ import ( rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/replicatedhq/embedded-cluster/pkg/versions" "github.com/replicatedhq/embedded-cluster/web" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -49,7 +49,7 @@ type upgradeConfig struct { passwordHash []byte tlsConfig apitypes.TLSConfig tlsCert tls.Certificate - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper licenseBytes []byte airgapMetadata *airgap.AirgapMetadata embeddedAssetsSize int64 @@ -124,9 +124,14 @@ func UpgradeCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { initialVersion = currentInstallation.Spec.Config.Version } + // Verify license is available for metrics reporting + if upgradeConfig.license == nil { + return fmt.Errorf("license is required for upgrade") + } + metricsReporter := newUpgradeReporter( replicatedAppURL(), cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), - upgradeConfig.license.Spec.LicenseID, upgradeConfig.clusterID, upgradeConfig.license.Spec.AppSlug, + upgradeConfig.license.GetLicenseID(), upgradeConfig.clusterID, upgradeConfig.license.GetAppSlug(), targetVersion, initialVersion, ) metricsReporter.ReportUpgradeStarted(ctx) @@ -255,7 +260,7 @@ func preRunUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig *up upgradeConfig.license = l // sync the license if a license is provided and we are not in airgap mode - if upgradeConfig.license != nil && flags.airgapBundle == "" { + if upgradeConfig.license != nil && upgradeConfig.license.GetLicenseID() != "" && flags.airgapBundle == "" { replicatedAPI, err := newReplicatedAPIClient(upgradeConfig.license, upgradeConfig.clusterID) if err != nil { return fmt.Errorf("failed to create replicated API client: %w", err) diff --git a/e2e/licenses/snapshot-license.yaml b/e2e/licenses/snapshot-license.yaml index 7c252e8ba6..d6769214da 100644 --- a/e2e/licenses/snapshot-license.yaml +++ b/e2e/licenses/snapshot-license.yaml @@ -1,24 +1,26 @@ -apiVersion: kots.io/v1beta1 +apiVersion: kots.io/v1beta2 kind: License metadata: name: githubsecretsnapshotcitestcustomer spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: 2cHXb1RCttzpR0xvnNWyaZCgDBP + appSlug: embedded-cluster-smoke-test-staging-app-mallard + channelID: 34qXUVJbXBQynDdvf9R7gLRP8K0 channelName: CI channels: - - channelID: 2cHXb1RCttzpR0xvnNWyaZCgDBP + - channelID: 34qXUVJbXBQynDdvf9R7gLRP8K0 channelName: CI channelSlug: ci endpoint: https://staging.replicated.app isDefault: true replicatedProxyDomain: proxy.staging.replicated.com + customerEmail: noreply@staging.replicated.com customerName: GitHub Secret Snapshot CI Test Customer endpoint: https://staging.replicated.app entitlements: expires_at: description: License Expiration - signature: {} + signature: + v2: gej19vkWRp+Ko8Ass2XmZkg7AIOVsCCwbWtwXT5cFnZ4z+SDggxCK1qx8xZ+a9pZVV42ez/F/rX8T6h7S/gymXiHJXAWMMHWsdAsvElv0iaLEmShwNoZs1z321vPfNnW05Fx82SlAycIPVU23NbBbAQxziGa4dcLkb5ao+gPokPKjB3XfHmLs9Bi+m4tr1kmQ2V9ZyhIMLaAaF/A5WcZTf54JZjruBd0lhM1P1r1vQAg62/YIdkcg1JcSkhluDvKFjVc1inHrnRvtTflq6uIyxylL2CprAytaEY0jAFFlhKuqlIX7fA3AubkPNsfenqkcnSUs7r3i24Z3iDbhQemYw== title: Expiration value: "" valueType: String @@ -27,8 +29,8 @@ spec: isEmbeddedClusterMultiNodeEnabled: true isKotsInstallEnabled: true isNewKotsUiEnabled: true - licenseID: 2fSe1CXtMOX9jNgHTe00mvqO502 - licenseSequence: 5 + licenseID: 34qXJJcnq3lsmapwAYM4xGyGwUR + licenseSequence: 7 licenseType: prod replicatedProxyDomain: proxy.staging.replicated.com - signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXhJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pWjJsMGFIVmljMlZqY21WMGMyNWhjSE5vYjNSamFYUmxjM1JqZFhOMGIyMWxjaUo5TENKemNHVmpJanA3SW14cFkyVnVjMlZKUkNJNklqSm1VMlV4UTFoMFRVOVlPV3BPWjBoVVpUQXdiWFp4VHpVd01pSXNJbXhwWTJWdWMyVlVlWEJsSWpvaWNISnZaQ0lzSW1OMWMzUnZiV1Z5VG1GdFpTSTZJa2RwZEVoMVlpQlRaV055WlhRZ1UyNWhjSE5vYjNRZ1Ewa2dWR1Z6ZENCRGRYTjBiMjFsY2lJc0ltRndjRk5zZFdjaU9pSmxiV0psWkdSbFpDMWpiSFZ6ZEdWeUxYTnRiMnRsTFhSbGMzUXRjM1JoWjJsdVp5MWhjSEFpTENKamFHRnVibVZzU1VRaU9pSXlZMGhZWWpGU1EzUjBlbkJTTUhoMmJrNVhlV0ZhUTJkRVFsQWlMQ0pqYUdGdWJtVnNUbUZ0WlNJNklrTkpJaXdpWTJoaGJtNWxiSE1pT2x0N0ltTm9ZVzV1Wld4SlJDSTZJakpqU0ZoaU1WSkRkSFI2Y0ZJd2VIWnVUbGQ1WVZwRFowUkNVQ0lzSW1Ob1lXNXVaV3hUYkhWbklqb2lZMmtpTENKamFHRnVibVZzVG1GdFpTSTZJa05KSWl3aWFYTkVaV1poZFd4MElqcDBjblZsTENKbGJtUndiMmx1ZENJNkltaDBkSEJ6T2k4dmMzUmhaMmx1Wnk1eVpYQnNhV05oZEdWa0xtRndjQ0lzSW5KbGNHeHBZMkYwWldSUWNtOTRlVVJ2YldGcGJpSTZJbkJ5YjNoNUxuTjBZV2RwYm1jdWNtVndiR2xqWVhSbFpDNWpiMjBpZlYwc0lteHBZMlZ1YzJWVFpYRjFaVzVqWlNJNk5Td2laVzVrY0c5cGJuUWlPaUpvZEhSd2N6b3ZMM04wWVdkcGJtY3VjbVZ3YkdsallYUmxaQzVoY0hBaUxDSnlaWEJzYVdOaGRHVmtVSEp2ZUhsRWIyMWhhVzRpT2lKd2NtOTRlUzV6ZEdGbmFXNW5MbkpsY0d4cFkyRjBaV1F1WTI5dElpd2laVzUwYVhSc1pXMWxiblJ6SWpwN0ltVjRjR2x5WlhOZllYUWlPbnNpZEdsMGJHVWlPaUpGZUhCcGNtRjBhVzl1SWl3aVpHVnpZM0pwY0hScGIyNGlPaUpNYVdObGJuTmxJRVY0Y0dseVlYUnBiMjRpTENKMllXeDFaU0k2SWlJc0luWmhiSFZsVkhsd1pTSTZJbE4wY21sdVp5SXNJbk5wWjI1aGRIVnlaU0k2ZTMxOWZTd2lhWE5FYVhOaGMzUmxjbEpsWTI5MlpYSjVVM1Z3Y0c5eWRHVmtJanAwY25WbExDSnBjMDVsZDB0dmRITlZhVVZ1WVdKc1pXUWlPblJ5ZFdVc0ltbHpSVzFpWldSa1pXUkRiSFZ6ZEdWeVJHOTNibXh2WVdSRmJtRmliR1ZrSWpwMGNuVmxMQ0pwYzBWdFltVmtaR1ZrUTJ4MWMzUmxjazExYkhScGJtOWtaVVZ1WVdKc1pXUWlPblJ5ZFdVc0ltbHpTMjkwYzBsdWMzUmhiR3hGYm1GaWJHVmtJanAwY25WbGZYMD0iLCJpbm5lclNpZ25hdHVyZSI6ImV5SnNhV05sYm5ObFUybG5ibUYwZFhKbElqb2lSM05DWmxOTWRFOVplRXhLY21zeE5IVkZMMVowTVRCT1FsZDNVRUZRVERCdWFWbGxaR3B5ZVZaR2VWVmFiVzU1ZVN0UGNEUmpWa2RSU1ZSVE1IbEVjMnAyVFN0MVozTlNlRzB5TjI1TFVVcGhlWGg0VjFSUFJGTnNjV2xwTVRWUFFXcE9jMFZ5V1VZeWFtaHBkV00xT0VaTVoxZzNNVU5ZY0dkRlNHdGljME5tYVV4WFFqUjNZVzEzVEZkRWRHZ3JXR2xzVjBoS1pIUXhTRmhzWVZOcU1VVk1Oa0p3TDJwNFYwVlFTVVV3WXpSemEwTk5hemhHZEVsNVVWZE1TV3BhTnpWRE5WVkVOM293YjNKNFpEUjJSRzh2ZVRWNk9FUlRTMEZRZWtVdlNXZzVSMlpMVm1WSVRITXhVekpJTlZoRFMzTk1aVU5CWWpSVUwxUkNiMEp6V2tONlpXeGxOMGRvTTNkRWMxUlZWRlUyUVZKTVVIQlVXV1ZZY2tWcWJVTjBaREp0VEhOR1ExcDNOekExWkZwWFZrbHhNRXRITnlzek5FMDVPRzlyY0RsV01EWjRaVEk0VDFocGFFbFhVM00xWW1kNU1tSjNQVDBpTENKd2RXSnNhV05MWlhraU9pSXRMUzB0TFVKRlIwbE9JRkJWUWt4SlF5QkxSVmt0TFMwdExWeHVUVWxKUWtscVFVNUNaMnR4YUd0cFJ6bDNNRUpCVVVWR1FVRlBRMEZST0VGTlNVbENRMmRMUTBGUlJVRTBWSFZGVTBaMVZXSkxXVTh4V0hGVWRWVTNVMXh1YkUwMVZXRXZkV0ZITTNjeGJHRjJXVW94Vms5cE0yWkNaMUpTUm1Fd04wazFUMGxxZGtwU1NWRlVkMlExUnk5Vk5tOXJhbEpoVFZneE5tWTRZemgwYUZ4dWRXUlJhVEU0YWtZNFRtWmxkVXhFY0c1bE5WSmhTa05zTlc5WmEwOURRVmhuZEdKSmRVaHdSa1I0UzBjM1FUVmtNWFpXUkcxUWFubGtaVUp6U0haMlExeHVaWGgzY1VGS2VFZGtNamxOU0hOQllVTldUWHBsVW5SV09VVktkMFZ1TDJSdE9VVnpZMGhpUnpnMllscGpZWGxFWmpOblRYSXhNamN3T1RGUFRrOVdSVnh1T1ZWR1pVWldZV3hoTW5GbE5URmlNRGszTmtka1kzUnNSWEoyZG1OUVRWcHdTbVFyZWpKVGRtTnhVMlJLY0U5WlFqRXphbVJXYW5OTmRtRkJTVzh4VDF4dU4xcEZVMFZwVWtGNk5VaGFjSHAyV1c0emNpOXhhMHg2U0dScFRVTk9SeXRtZW1SbGR6ZGxaVUp0WTNrcmExQlZXRmd6WkdGeU0yRmFWeXR0WmtOU09WeHViRkZKUkVGUlFVSmNiaTB0TFMwdFJVNUVJRkJWUWt4SlF5QkxSVmt0TFMwdExWeHVJaXdpYTJWNVUybG5ibUYwZFhKbElqb2laWGxLZW1GWFpIVlpXRkl4WTIxVmFVOXBTbEpqUkdNelkwUkNVMVY2YUVkVFNHeEVVekZzYUdSRVJsUmhiVnBHVWxaS1dWTjZUakppTUVZelVrWk9iMkl3VlhwaWEwMTNWMFZTTWxveWRFVmxhMVo2V2xkT1YxRnFWa1JsU0ZVMFZtMVdTbFJ1Y0ZwbFIzUTBVMGhPVUdWdVkzbFRWMGx5WkZoR1ZFNVZjSGRSYkVKNFZqRmtkVnB1UmtoVk1tYzFZa2hhYWxReWQzZFNSMDVUWWpKc1ZtTkhPWGRoVkdjeVRrWmpkbVZ1YURCVlZuQmFWRmRyZDFSSVJqUlZiVWt3VTBSb1NGWlhkRkpoTWxGM1lsaEtTVlJFYkcxU01ITXdUREpHWVZSdFZsZFJWbWhHVkZob2EyUkZOVkprU0U1NlZGVmtlVlpVVmpCVVNGcFlZbXhCZW1OVldrSk9VM1JIVkRGb00xWnRjREpqUlhReVZWZEtNbGRHY0VsaFJscHVUbGhLUjFRd05XdE5la3BoVDBWMGJWVnFWa05VUjFwUldqSnZORlpyV1haaGEyaGFaREprZEU5WFNuSmtWVEI1VDFoc1dsb3dTa1ZXVkZwQ1YyMVdhVmxVYkVwU2FrSnJUakowZW1JelpHcGlWWFJFVTFoS2MxSXpXbGxVTWxadlZIazVXR05WVW5wWFYxWndWREJrZVZkSGJ6RmxiR3hJVW01V1JWSXpaRWxYVmtZMlkzcHNTV1JxVG0xYVZVWjZWREF4VEZwWVdraGhWMDVZWTFoYWJsb3hXbE5UUjBsNFRWZG9NbU5XYkZGVlJsSTJZbE01VUdKSVl6bFFVMGx6U1cxa2MySXlTbWhpUlhSc1pWVnNhMGxxYjJsYVIxVjVXWHBKTTA1VVdURk9iVkYzVGtkSmVGbHRTWGRhYWtVeFdUSlpNMDFIV1hkYVYwVjVXVlJKYVdaUlBUMGlmUT09In0= + signature: eyJsaWNlbnNlRGF0YSI6ImV5SmhjR2xXWlhKemFXOXVJam9pYTI5MGN5NXBieTkyTVdKbGRHRXlJaXdpYTJsdVpDSTZJa3hwWTJWdWMyVWlMQ0p0WlhSaFpHRjBZU0k2ZXlKdVlXMWxJam9pWjJsMGFIVmljMlZqY21WMGMyNWhjSE5vYjNSamFYUmxjM1JqZFhOMGIyMWxjaUo5TENKemNHVmpJanA3SW14cFkyVnVjMlZKUkNJNklqTTBjVmhLU21OdWNUTnNjMjFoY0hkQldVMDBlRWQ1UjNkVlVpSXNJbXhwWTJWdWMyVlVlWEJsSWpvaWNISnZaQ0lzSW1OMWMzUnZiV1Z5VG1GdFpTSTZJa2RwZEVoMVlpQlRaV055WlhRZ1UyNWhjSE5vYjNRZ1Ewa2dWR1Z6ZENCRGRYTjBiMjFsY2lJc0ltRndjRk5zZFdjaU9pSmxiV0psWkdSbFpDMWpiSFZ6ZEdWeUxYTnRiMnRsTFhSbGMzUXRjM1JoWjJsdVp5MWhjSEF0YldGc2JHRnlaQ0lzSW1Ob1lXNXVaV3hKUkNJNklqTTBjVmhWVmtwaVdFSlJlVzVFWkhabU9WSTNaMHhTVURoTE1DSXNJbU5vWVc1dVpXeE9ZVzFsSWpvaVEwa2lMQ0pqZFhOMGIyMWxja1Z0WVdsc0lqb2libTl5WlhCc2VVQnpkR0ZuYVc1bkxuSmxjR3hwWTJGMFpXUXVZMjl0SWl3aVkyaGhibTVsYkhNaU9sdDdJbU5vWVc1dVpXeEpSQ0k2SWpNMGNWaFZWa3BpV0VKUmVXNUVaSFptT1ZJM1oweFNVRGhMTUNJc0ltTm9ZVzV1Wld4VGJIVm5Jam9pWTJraUxDSmphR0Z1Ym1Wc1RtRnRaU0k2SWtOSklpd2lhWE5FWldaaGRXeDBJanAwY25WbExDSmxibVJ3YjJsdWRDSTZJbWgwZEhCek9pOHZjM1JoWjJsdVp5NXlaWEJzYVdOaGRHVmtMbUZ3Y0NJc0luSmxjR3hwWTJGMFpXUlFjbTk0ZVVSdmJXRnBiaUk2SW5CeWIzaDVMbk4wWVdkcGJtY3VjbVZ3YkdsallYUmxaQzVqYjIwaWZWMHNJbXhwWTJWdWMyVlRaWEYxWlc1alpTSTZOeXdpWlc1a2NHOXBiblFpT2lKb2RIUndjem92TDNOMFlXZHBibWN1Y21Wd2JHbGpZWFJsWkM1aGNIQWlMQ0p5WlhCc2FXTmhkR1ZrVUhKdmVIbEViMjFoYVc0aU9pSndjbTk0ZVM1emRHRm5hVzVuTG5KbGNHeHBZMkYwWldRdVkyOXRJaXdpWlc1MGFYUnNaVzFsYm5SeklqcDdJbVY0Y0dseVpYTmZZWFFpT25zaWRHbDBiR1VpT2lKRmVIQnBjbUYwYVc5dUlpd2laR1Z6WTNKcGNIUnBiMjRpT2lKTWFXTmxibk5sSUVWNGNHbHlZWFJwYjI0aUxDSjJZV3gxWlNJNklpSXNJblpoYkhWbFZIbHdaU0k2SWxOMGNtbHVaeUlzSW5OcFoyNWhkSFZ5WlNJNmV5SjJNaUk2SW1kbGFqRTVkbXRYVW5BclMyODRRWE56TWxodFdtdG5OMEZKVDFaelEwTjNZbGQwZDFoVU5XTkdibG8wZWl0VFJHZG5lRU5MTVhGNE9IaGFLMkU1Y0ZwV1ZqUXlaWG92Umk5eVdEaFVObWczVXk5bmVXMVlhVWhLV0VGWFRVMUlWM05rUVhOMlJXeDJNR2xoVEVWdFUyaDNUbTlhY3pGNk16SXhkbEJtVG01WE1EVkdlRGd5VTJ4QmVXTkpVRlpWTWpOT1lrSmlRVkY0ZW1sSFlUUmtZMHhyWWpWaGJ5dG5VRzlyVUV0cVFqTllaa2h0VEhNNVFta3JiVFIwY2pGcmJWRXlWamxhZVdoSlRVeGhRV0ZHTDBFMVYyTmFWR1kxTkVwYWFuSjFRbVF3YkdoTk1WQXhjakYyVVVGbk5qSXZXVWxrYTJObk1VcGpVMnRvYkhWRWRrdEdhbFpqTVdsdVNISnVVblowVkdac2NUWjFTWGw0ZVd4TU1rTndja0Y1ZEdGRldUQnFRVVpHYkdoTGRYRnNTVmczWmtFelFYVmlhMUJPYzJabGJuRnJZMjVUVlhNM2NqTnBNalJhTTJsRVltaFJaVzFaZHowOUluMTlmU3dpYVhORWFYTmhjM1JsY2xKbFkyOTJaWEo1VTNWd2NHOXlkR1ZrSWpwMGNuVmxMQ0pwYzA1bGQwdHZkSE5WYVVWdVlXSnNaV1FpT25SeWRXVXNJbWx6UlcxaVpXUmtaV1JEYkhWemRHVnlSRzkzYm14dllXUkZibUZpYkdWa0lqcDBjblZsTENKcGMwVnRZbVZrWkdWa1EyeDFjM1JsY2sxMWJIUnBUbTlrWlVWdVlXSnNaV1FpT25SeWRXVXNJbWx6UzI5MGMwbHVjM1JoYkd4RmJtRmliR1ZrSWpwMGNuVmxmWDA9IiwiaW5uZXJTaWduYXR1cmUiOiJleUoyTWt4cFkyVnVjMlZUYVdkdVlYUjFjbVVpT2lKdVRtWnNVMHQ2ZGxWdVkxaEhkRWRYZUdRNFIyUjNkWEo0V2xRNGNYVXhVeXR0VFhkNVZYaFJjRVpIZFRCa05sTTRXamgwZGxCNFN6azVRVEF4UWtGSVRqazFNMjVvVlhSU1FVTjVXbVJQV0VsWFkzSnJiVkV5Y0VneVVuTnBWVXN3VldWNk9HMTNSRTkxT0V4bk5HNUxlVzF5YTJOblQwODRTemhGWWl0bmIzTlRaamxSYlV0bE5qRlFXRE12TDFFMWRITXlhSFZGY204eFlsRTFZblZMWWpKMmRUZ3JkVWN6WWxkd2FFaG9XakpGVlcwck1YSkhPREl5TDJGTU9HRTRkVFJyWjFSdGEzTkhlVWRKTTJReVJsbFhVVWRpUzA5d2FuUmhaRzk1Um5kTlVXeGxjbXB6U1dsQ2RVdERUakF3YzJwcU0yeE5XRlJVVDI1aVprMTBiR2MyTlcxVE1EQjBMMEoxUTBFNGFHVnhTMkZ1ZFRoRWMweEJha1pZUVRkaGMzRjJNRlpXTm1zcmNFWmhSRnBhV21rd2VGQkRWMjVrTVhWaU5IZEtaamRYTURsemNVNWpRamN3V0ZNNGFUbExUbmRNUVdjOVBTSXNJbkIxWW14cFkwdGxlU0k2SWkwdExTMHRRa1ZIU1U0Z1VGVkNURWxESUV0RldTMHRMUzB0WEc1TlNVbENTV3BCVGtKbmEzRm9hMmxIT1hjd1FrRlJSVVpCUVU5RFFWRTRRVTFKU1VKRFowdERRVkZGUVhNeEwzVkpWR1o2TkhOQ1RXbHVibU5oVVZnMlhHNVZlVEZ6V1RGTFJXRldaek4xVVZCR2NsRnFhakEyV1N0cVVIZE1SVFZGTmxaMGMxWmtUMlV3Vms5aWF5dFdSR1J2VVhkNGVYWnhTVTkyVURCWWJDOUZYRzVWU3pocVNFbDNhbTF5ZWl0RU5sQkxWWEUyV0ZkUFEybEdaVEp6TVdFM1dUQnBUbTVOVkVGYVIzQjNWamRDZGpKS2FrNVdLMWx0YlV4aWQyWlZkMDQ1WEc1eGNqRndlVE5WTWxWa1ptNUhiMlp4T1RZMVpIaDNaMk5IT1ZGelFtZEtaamhVUVhBd1prVjFaM1ZMTUd3M1VUZ3lZWE5TYzFwVVJIaDNSakYzVlVaR1hHNTFWa2RKYUU5c2FFNTRSRXh4UVhvNGQyWjNRMnBIYlRCQ2JHZE1RVXRsWVhSVk9XeHdXbXR0V1ZoNlpuVkNTa3BJTUd0RWVYUkdVRTlXTlZwUFFqWXhYRzR3WVdoT05WSkhVVEIzYzBObE5YUkhibXc0VEVadU5IUkdOemRIZVhkUFRHMW1aVnBQWVVsWlVXZDROelUyY1ZkeVdsUXhOWGg1VlRCTVVWVlJiMGxZWEc0emQwbEVRVkZCUWx4dUxTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0WEc0aUxDSjJNa3RsZVZOcFoyNWhkSFZ5WlNJNkltVjVTbnBoVjJSMVdWaFNNV050VldsUGFVcHZZMVV4U1dSSFRqTmxWR3hFWTBaamRscHRVVE5WTW5ocVVtdDRNVTFHV2xsa1JGSnFVV3BTTW1Fd2VHcFNSM1JKVFc1bmQySXpTbTlaYTNSM1RteFNjVk15WkhWa2FrcFhUV3RXYldOcmNFNU9lbWN4Vmtka2NsRnNVVEZQVlZVelRsZHdlbGRxYkZkVU1qbEtWVlZLV0ZwVmNFcE5WVGgzVkVSYU1sTXlOV2xaTW5oVFZGWlNZVkZYZEVsUmVUaHlWVEo0YW1GclNsbFpNRTVHVlVoS2IxWklTbWxNTTFwVVZGVlNVMVZyTkRKVlJWSjBXbFpWZWsxRlZreFdiVFIyVWpCV2MwNUZaRzFUYkZKV1dsWnNTMlF3VGxKUFJYaFlWVE5DUlZwV1JreGtSemg2VFVVeGFVOUlVa0poUjNCT1ZraFNZVTlJY0hwVk1sRjNZV3haTWxwRVJrZGthMlJEV1d4YWMxRXhjRTlrZWtKV1QwTjBkV0ZVVm5SaE1IY3hWMGRTYTJFeldtbGpVemt3WWtkMGFtTklRbEpWUkVKSVZVZFNURTFyYXpKUFZrRjVWVWRXUzFac1FuaGtNamx2VlVaT1VWbHJiR0ZsYkdzMFl6RkNRMDFVUVRCVE1tUjVVbTVhYmxkVVJsUmFSbEpHWlVkb1dWa3lkR3hhVlhoRllWZFdRbVZHWnpSYWJWcHZXVlZvUjJSdFZYZFViRlp4U3pBMVIyRkZSbGRqZW1jeVdURlNjMDB6V2xOV00yaEdWVE5DU1dGWFJuaGlhM0I2VVZoS1VXTnFUa2RYUjJNNVVGTkpjMGx0WkhOaU1rcG9Za1YwYkdWVmJHdEphbTlwV2tkVmVWbDZTVE5PVkZreFRtMVJkMDVIU1hoWmJVbDNXbXBGTVZreVdUTk5SMWwzV2xkRmVWbFVTV2xtVVQwOUluMD0ifQ== diff --git a/go.mod b/go.mod index 6c388f9494..c8ef6566fc 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/replicatedhq/embedded-cluster/kinds v0.0.0 github.com/replicatedhq/embedded-cluster/utils v0.0.0 - github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d + github.com/replicatedhq/kotskinds v0.0.0-20251029124314-174e89c93554 github.com/replicatedhq/troubleshoot v0.123.12 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 diff --git a/go.sum b/go.sum index c1ebe8e39a..90a46c129d 100644 --- a/go.sum +++ b/go.sum @@ -693,8 +693,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d h1:N8t9W5SYs1MKPsuAp4PA5Haje4cOyCyubAq65qB1wzE= -github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d/go.mod h1:+k4PHo2wukoU9kdiKrqqgi89Wmj+9AiwppYGVK11zig= +github.com/replicatedhq/kotskinds v0.0.0-20251029124314-174e89c93554 h1:a9vLewcXgVC/vclEak7CV0gsSYhYinjnWDoUkzrqN4w= +github.com/replicatedhq/kotskinds v0.0.0-20251029124314-174e89c93554/go.mod h1:+k4PHo2wukoU9kdiKrqqgi89Wmj+9AiwppYGVK11zig= github.com/replicatedhq/troubleshoot v0.123.12 h1:XbgZJMSwIHyf1lvxIRNwI9AVsRzcA7N3AWLPLSkrr+w= github.com/replicatedhq/troubleshoot v0.123.12/go.mod h1:CKPCj8si77XuSL6sIAFdqtO23/eha159eEBlQF8HpVw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/pkg-new/replicatedapi/client.go b/pkg-new/replicatedapi/client.go index 758d068b5d..d144dc5a87 100644 --- a/pkg-new/replicatedapi/client.go +++ b/pkg-new/replicatedapi/client.go @@ -12,13 +12,13 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/versions" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kyaml "sigs.k8s.io/yaml" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" ) var defaultHTTPClient = newRetryableHTTPClient() // ClientFactory is a function type for creating replicatedapi clients -type ClientFactory func(replicatedAppURL string, license *kotsv1beta1.License, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) +type ClientFactory func(replicatedAppURL string, license *licensewrapper.LicenseWrapper, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) var clientFactory ClientFactory = defaultNewClient @@ -28,12 +28,12 @@ func SetClientFactory(factory ClientFactory) { } type Client interface { - SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error) + SyncLicense(ctx context.Context) (*licensewrapper.LicenseWrapper, []byte, error) } type client struct { replicatedAppURL string - license *kotsv1beta1.License + license *licensewrapper.LicenseWrapper releaseData *release.ReleaseData clusterID string httpClient *retryablehttp.Client @@ -53,13 +53,13 @@ func WithHTTPClient(httpClient *retryablehttp.Client) ClientOption { } } -// NewClient creates a new replicatedapi client using the configured factory -func NewClient(replicatedAppURL string, license *kotsv1beta1.License, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) { +func NewClient(replicatedAppURL string, license *licensewrapper.LicenseWrapper, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) { + // NewClient creates a new replicatedapi client using the configured factory return clientFactory(replicatedAppURL, license, releaseData, opts...) } // defaultNewClient is the default implementation of NewClient -func defaultNewClient(replicatedAppURL string, license *kotsv1beta1.License, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) { +func defaultNewClient(replicatedAppURL string, license *licensewrapper.LicenseWrapper, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) { c := &client{ replicatedAppURL: replicatedAppURL, license: license, @@ -76,11 +76,15 @@ func defaultNewClient(replicatedAppURL string, license *kotsv1beta1.License, rel } // SyncLicense fetches the latest license from the Replicated API -func (c *client) SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error) { - u := fmt.Sprintf("%s/license/%s", c.replicatedAppURL, c.license.Spec.AppSlug) +func (c *client) SyncLicense(ctx context.Context) (*licensewrapper.LicenseWrapper, []byte, error) { + if c.license == nil { + return nil, nil, fmt.Errorf("no license configured") + } + + u := fmt.Sprintf("%s/license/%s", c.replicatedAppURL, c.license.GetAppSlug()) params := url.Values{} - params.Set("licenseSequence", fmt.Sprintf("%d", c.license.Spec.LicenseSequence)) + params.Set("licenseSequence", fmt.Sprintf("%d", c.license.GetLicenseSequence())) if c.releaseData != nil && c.releaseData.ChannelRelease != nil { params.Set("selectedChannelId", c.releaseData.ChannelRelease.ChannelID) } @@ -109,22 +113,23 @@ func (c *client) SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, return nil, nil, fmt.Errorf("read response body: %w", err) } - var licenseResp kotsv1beta1.License - if err := kyaml.Unmarshal(body, &licenseResp); err != nil { - return nil, nil, fmt.Errorf("unmarshal license response: %w", err) + // Parse response into wrapper (handles both v1beta1 and v1beta2 responses) + licenseWrapper, err := licensewrapper.LoadLicenseFromBytes(body) + if err != nil { + return nil, nil, fmt.Errorf("parse license response: %w", err) } - if licenseResp.Spec.LicenseID == "" { + if licenseWrapper.GetLicenseID() == "" { return nil, nil, fmt.Errorf("license is empty") } - c.license = &licenseResp + c.license = &licenseWrapper if _, err := c.getChannelFromLicense(); err != nil { return nil, nil, fmt.Errorf("get channel from license: %w", err) } - return &licenseResp, body, nil + return &licenseWrapper, body, nil } // newRetryableRequest returns a retryablehttp.Request object with kots defaults set, including a User-Agent header. @@ -141,9 +146,18 @@ func (c *client) newRetryableRequest(ctx context.Context, method string, url str // injectHeaders injects the basic auth header, user agent header, and reporting info headers into the http.Header. func (c *client) injectHeaders(header http.Header) { - header.Set("Authorization", "Basic "+basicAuth(c.license.Spec.LicenseID, c.license.Spec.LicenseID)) + if c.license == nil { + return + } + licenseID := c.license.GetLicenseID() + header.Set("Authorization", "Basic "+basicAuth(licenseID, licenseID)) header.Set("User-Agent", fmt.Sprintf("Embedded-Cluster/%s", versions.Version)) + // Add license version header for v1beta2 licenses + if c.license.IsV2() { + header.Set("X-Replicated-License-Version", "v1beta2") + } + c.injectReportingInfoHeaders(header) } @@ -151,20 +165,26 @@ func (c *client) getChannelFromLicense() (*kotsv1beta1.Channel, error) { if c.releaseData == nil || c.releaseData.ChannelRelease == nil || c.releaseData.ChannelRelease.ChannelID == "" { return nil, fmt.Errorf("channel release is empty") } - if c.license == nil || c.license.Spec.LicenseID == "" { + if c.license == nil || c.license.GetLicenseID() == "" { return nil, fmt.Errorf("license is empty") } - for _, channel := range c.license.Spec.Channels { + + // Check multi-channel licenses first + channels := c.license.GetChannels() + for _, channel := range channels { if channel.ChannelID == c.releaseData.ChannelRelease.ChannelID { return &channel, nil } } - if c.license.Spec.ChannelID == c.releaseData.ChannelRelease.ChannelID { + + // Fallback to legacy single-channel license + if c.license.GetChannelID() == c.releaseData.ChannelRelease.ChannelID { return &kotsv1beta1.Channel{ - ChannelID: c.license.Spec.ChannelID, - ChannelName: c.license.Spec.ChannelName, + ChannelID: c.license.GetChannelID(), + ChannelName: c.license.GetChannelName(), }, nil } + return nil, fmt.Errorf("channel %s not found in license", c.releaseData.ChannelRelease.ChannelID) } diff --git a/pkg-new/replicatedapi/client_test.go b/pkg-new/replicatedapi/client_test.go index 3b8e97a8df..3c35e1ec13 100644 --- a/pkg-new/replicatedapi/client_test.go +++ b/pkg-new/replicatedapi/client_test.go @@ -9,20 +9,27 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/versions" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.yaml.in/yaml/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kyaml "sigs.k8s.io/yaml" ) func TestSyncLicense(t *testing.T) { tests := []struct { - name string - license kotsv1beta1.License - releaseData *release.ReleaseData - serverHandler func(t *testing.T) http.HandlerFunc - expectedLicense *kotsv1beta1.License - wantErr string + name string + license kotsv1beta1.License + licenseV2 *kotsv1beta2.License + releaseData *release.ReleaseData + serverHandler func(t *testing.T) http.HandlerFunc + wantLicenseSequence int64 + wantAppSlug string + wantLicenseID string + wantIsV1 bool + wantIsV2 bool + wantErr string }{ { name: "successful license sync", @@ -60,6 +67,9 @@ func TestSyncLicense(t *testing.T) { assert.NotEmpty(t, authHeader) assert.Contains(t, authHeader, "Basic ") + // Validate license version header is NOT present for v1beta1 + assert.Empty(t, r.Header.Get("X-Replicated-License-Version")) + // Return response as YAML resp := kotsv1beta1.License{ TypeMeta: metav1.TypeMeta{ @@ -83,23 +93,141 @@ func TestSyncLicense(t *testing.T) { } w.WriteHeader(http.StatusOK) - yaml.NewEncoder(w).Encode(resp) + respBytes, err := kyaml.Marshal(resp) + if err != nil { + t.Fatalf("failed to marshal license: %v", err) + } + w.Write(respBytes) } }, - expectedLicense: &kotsv1beta1.License{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "kots.io/v1beta1", - Kind: "License", - }, + wantLicenseSequence: 6, + wantAppSlug: "test-app", + wantLicenseID: "test-license-id", + wantIsV1: true, + }, + { + name: "successful license sync with v1beta2 response (from v1beta1)", + license: kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ AppSlug: "test-app", LicenseID: "test-license-id", - LicenseSequence: 6, - CustomerName: "Test Customer", + LicenseSequence: 5, ChannelID: "test-channel-123", ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", }, }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Validate request + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/license/test-app", r.URL.Path) + assert.Equal(t, "5", r.URL.Query().Get("licenseSequence")) + assert.Equal(t, "test-channel-123", r.URL.Query().Get("selectedChannelId")) + assert.Equal(t, "application/yaml", r.Header.Get("Accept")) + + // Validate auth header + authHeader := r.Header.Get("Authorization") + assert.NotEmpty(t, authHeader) + assert.Contains(t, authHeader, "Basic ") + + // Validate license version header is NOT present for v1beta1 (request uses v1beta1 license) + assert.Empty(t, r.Header.Get("X-Replicated-License-Version")) + + // Return v1beta2 license response + resp := `apiVersion: kots.io/v1beta2 +kind: License +spec: + licenseID: test-license-id-v2 + appSlug: test-app + licenseSequence: 6 + customerName: Test Customer + channelID: test-channel-123 + channelName: Stable + channels: + - channelID: test-channel-123 + channelName: Stable` + + w.WriteHeader(http.StatusOK) + w.Write([]byte(resp)) + } + }, + wantLicenseSequence: 6, + wantAppSlug: "test-app", + wantLicenseID: "test-license-id-v2", + wantIsV2: true, + }, + { + name: "successful license sync with v1beta2 request", + licenseV2: &kotsv1beta2.License{ + Spec: kotsv1beta2.LicenseSpec{ + AppSlug: "test-app-v2", + LicenseID: "test-license-id-v2", + LicenseSequence: 7, + ChannelID: "test-channel-456", + ChannelName: "Beta", + Channels: []kotsv1beta2.Channel{ + { + ChannelID: "test-channel-456", + ChannelName: "Beta", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-456", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Validate request + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/license/test-app-v2", r.URL.Path) + assert.Equal(t, "7", r.URL.Query().Get("licenseSequence")) + assert.Equal(t, "test-channel-456", r.URL.Query().Get("selectedChannelId")) + assert.Equal(t, "application/yaml", r.Header.Get("Accept")) + + // Validate auth header + authHeader := r.Header.Get("Authorization") + assert.NotEmpty(t, authHeader) + assert.Contains(t, authHeader, "Basic ") + + // Validate license version header IS present for v1beta2 + assert.Equal(t, "v1beta2", r.Header.Get("X-Replicated-License-Version")) + + // Return v1beta2 license response + resp := `apiVersion: kots.io/v1beta2 +kind: License +spec: + licenseID: test-license-id-v2 + appSlug: test-app-v2 + licenseSequence: 8 + customerName: Test Customer V2 + channelID: test-channel-456 + channelName: Beta + channels: + - channelID: test-channel-456 + channelName: Beta` + + w.WriteHeader(http.StatusOK) + w.Write([]byte(resp)) + } + }, + wantLicenseSequence: 8, + wantAppSlug: "test-app-v2", + wantLicenseID: "test-license-id-v2", + wantIsV2: true, }, { name: "returns error on 401 unauthorized", @@ -215,7 +343,7 @@ func TestSyncLicense(t *testing.T) { w.Write([]byte("invalid yaml")) } }, - wantErr: "unmarshal license response", + wantErr: "parse license response", }, } @@ -227,8 +355,16 @@ func TestSyncLicense(t *testing.T) { server := httptest.NewServer(tt.serverHandler(t)) defer server.Close() - // Create client - c, err := NewClient(server.URL, &tt.license, tt.releaseData) + // Wrap the license (v1beta1 or v1beta2) + var wrapper *licensewrapper.LicenseWrapper + if tt.licenseV2 != nil { + wrapper = &licensewrapper.LicenseWrapper{V2: tt.licenseV2} + } else { + wrapper = &licensewrapper.LicenseWrapper{V1: &tt.license} + } + + // Create client with wrapper + c, err := NewClient(server.URL, wrapper, tt.releaseData) req.NoError(err) // Execute test @@ -242,15 +378,26 @@ func TestSyncLicense(t *testing.T) { req.Nil(rawLicense) } else { req.NoError(err) - req.NotNil(license) req.NotNil(rawLicense) - assert.Equal(t, tt.expectedLicense.Spec.AppSlug, license.Spec.AppSlug) - assert.Equal(t, tt.expectedLicense.Spec.LicenseID, license.Spec.LicenseID) - assert.Equal(t, tt.expectedLicense.Spec.LicenseSequence, license.Spec.LicenseSequence) + + // Assert using wrapper methods (works for both v1beta1 and v1beta2) + assert.Equal(t, tt.wantLicenseSequence, license.GetLicenseSequence()) + assert.Equal(t, tt.wantAppSlug, license.GetAppSlug()) + assert.Equal(t, tt.wantLicenseID, license.GetLicenseID()) + + // Assert version + if tt.wantIsV1 { + assert.True(t, license.IsV1()) + assert.False(t, license.IsV2()) + } + if tt.wantIsV2 { + assert.False(t, license.IsV1()) + assert.True(t, license.IsV2()) + } // Validate raw license is valid YAML var parsedLicense kotsv1beta1.License - err = yaml.Unmarshal(rawLicense, &parsedLicense) + err = kyaml.Unmarshal(rawLicense, &parsedLicense) req.NoError(err, "rawLicense should be valid YAML") } }) @@ -259,14 +406,32 @@ func TestSyncLicense(t *testing.T) { func TestGetReportingInfoHeaders(t *testing.T) { tests := []struct { - name string - clusterID string - expectedCount int - checkHeaders map[string]string + name string + clusterID string + licenseWrapper *licensewrapper.LicenseWrapper + expectedCount int + checkHeaders map[string]string }{ { - name: "with cluster ID", - clusterID: "cluster-123", + name: "with cluster ID and v1beta1 license", + clusterID: "cluster-123", + licenseWrapper: &licensewrapper.LicenseWrapper{ + V1: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + }, expectedCount: 7, // EmbeddedClusterID, ChannelID, ChannelName, K8sVersion, K8sDistribution, EmbeddedClusterVersion, IsKurl checkHeaders: map[string]string{ "X-Replicated-EmbeddedClusterID": "cluster-123", @@ -279,8 +444,56 @@ func TestGetReportingInfoHeaders(t *testing.T) { }, }, { - name: "zero values should be skipped", - clusterID: "", + name: "with cluster ID and v1beta2 license", + clusterID: "cluster-456", + licenseWrapper: &licensewrapper.LicenseWrapper{ + V2: &kotsv1beta2.License{ + Spec: kotsv1beta2.LicenseSpec{ + AppSlug: "test-app-v2", + LicenseID: "test-license-id-v2", + LicenseSequence: 2, + ChannelID: "test-channel-456", + ChannelName: "Beta", + Channels: []kotsv1beta2.Channel{ + { + ChannelID: "test-channel-456", + ChannelName: "Beta", + }, + }, + }, + }, + }, + expectedCount: 7, // EmbeddedClusterID, ChannelID, ChannelName, K8sVersion, K8sDistribution, EmbeddedClusterVersion, IsKurl + checkHeaders: map[string]string{ + "X-Replicated-EmbeddedClusterID": "cluster-456", + "X-Replicated-DownstreamChannelID": "test-channel-456", + "X-Replicated-DownstreamChannelName": "Beta", + "X-Replicated-K8sVersion": versions.K0sVersion, + "X-Replicated-K8sDistribution": DistributionEmbeddedCluster, + "X-Replicated-EmbeddedClusterVersion": versions.Version, + "X-Replicated-IsKurl": "false", + }, + }, + { + name: "zero values should be skipped", + clusterID: "", + licenseWrapper: &licensewrapper.LicenseWrapper{ + V1: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + }, expectedCount: 6, // ChannelID, ChannelName, K8sVersion, K8sDistribution, EmbeddedClusterVersion, IsKurl checkHeaders: map[string]string{ "X-Replicated-IsKurl": "false", @@ -292,30 +505,19 @@ func TestGetReportingInfoHeaders(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - license := kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - AppSlug: "test-app", - LicenseID: "test-license-id", - LicenseSequence: 1, - ChannelID: "test-channel-123", - ChannelName: "Stable", - Channels: []kotsv1beta1.Channel{ - { - ChannelID: "test-channel-123", - ChannelName: "Stable", - }, - }, - }, + channelID := "test-channel-123" + if tt.licenseWrapper != nil && tt.licenseWrapper.GetChannelID() != "" { + channelID = tt.licenseWrapper.GetChannelID() } releaseData := &release.ReleaseData{ ChannelRelease: &release.ChannelRelease{ - ChannelID: "test-channel-123", + ChannelID: channelID, }, } c := &client{ - license: &license, + license: tt.licenseWrapper, releaseData: releaseData, clusterID: tt.clusterID, } @@ -360,7 +562,7 @@ func TestInjectHeaders(t *testing.T) { } c := &client{ - license: &license, + license: &licensewrapper.LicenseWrapper{V1: &license}, releaseData: releaseData, clusterID: "test-cluster-id", } @@ -388,4 +590,7 @@ func TestInjectHeaders(t *testing.T) { req.Equal(DistributionEmbeddedCluster, header.Get("X-Replicated-K8sDistribution")) req.Equal(versions.Version, header.Get("X-Replicated-EmbeddedClusterVersion")) req.Equal("false", header.Get("X-Replicated-IsKurl")) + + // Validate license version header is NOT present for v1beta1 + req.Empty(header.Get("X-Replicated-License-Version")) } diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 4f363f76ac..6a6dd17aa0 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -12,13 +12,13 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" ) type InstallOptions struct { AdminConsolePwd string AdminConsolePort int - License *kotsv1beta1.License + License *licensewrapper.LicenseWrapper IsAirgap bool TLSCertBytes []byte TLSKeyBytes []byte @@ -43,7 +43,7 @@ type InstallOptions struct { type KubernetesInstallOptions struct { AdminConsolePwd string AdminConsolePort int - License *kotsv1beta1.License + License *licensewrapper.LicenseWrapper IsAirgap bool TLSCertBytes []byte TLSKeyBytes []byte diff --git a/pkg/dryrun/dryrun.go b/pkg/dryrun/dryrun.go index 85890f0028..9d44bf2bd6 100644 --- a/pkg/dryrun/dryrun.go +++ b/pkg/dryrun/dryrun.go @@ -20,7 +20,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/sirupsen/logrus" "github.com/spf13/pflag" @@ -83,7 +83,7 @@ func Init(outputFile string, client *Client) { }) } if client.ReplicatedAPIClient != nil { - replicatedapi.SetClientFactory(func(replicatedAppURL string, license *kotsv1beta1.License, releaseData *release.ReleaseData, opts ...replicatedapi.ClientOption) (replicatedapi.Client, error) { + replicatedapi.SetClientFactory(func(replicatedAppURL string, license *licensewrapper.LicenseWrapper, releaseData *release.ReleaseData, opts ...replicatedapi.ClientOption) (replicatedapi.Client, error) { return client.ReplicatedAPIClient, nil }) } diff --git a/pkg/dryrun/replicatedapi.go b/pkg/dryrun/replicatedapi.go index 0d03340912..ff6a866454 100644 --- a/pkg/dryrun/replicatedapi.go +++ b/pkg/dryrun/replicatedapi.go @@ -5,24 +5,23 @@ import ( "fmt" "github.com/replicatedhq/embedded-cluster/pkg-new/replicatedapi" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - "sigs.k8s.io/yaml" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" ) var _ replicatedapi.Client = (*ReplicatedAPIClient)(nil) // ReplicatedAPIClient is a mockable implementation of the replicatedapi.Client interface. type ReplicatedAPIClient struct { - License *kotsv1beta1.License + License *licensewrapper.LicenseWrapper LicenseBytes []byte } // SyncLicense returns the mocked license data. -func (c *ReplicatedAPIClient) SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error) { +func (c *ReplicatedAPIClient) SyncLicense(ctx context.Context) (*licensewrapper.LicenseWrapper, []byte, error) { // If License is not set but LicenseBytes is, parse the license from bytes if c.License == nil && len(c.LicenseBytes) > 0 { - var license kotsv1beta1.License - if err := yaml.Unmarshal(c.LicenseBytes, &license); err != nil { + license, err := licensewrapper.LoadLicenseFromBytes(c.LicenseBytes) + if err != nil { return nil, nil, fmt.Errorf("failed to parse license from bytes: %w", err) } c.License = &license diff --git a/pkg/helpers/parse.go b/pkg/helpers/parse.go index a846c2fa18..225b3e7242 100644 --- a/pkg/helpers/parse.go +++ b/pkg/helpers/parse.go @@ -6,6 +6,7 @@ import ( embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" kyaml "sigs.k8s.io/yaml" ) @@ -33,25 +34,24 @@ func ParseEndUserConfig(fpath string) (*embeddedclusterv1beta1.Config, error) { return &cfg, nil } -// ParseLicense parses the license from the given file. -func ParseLicense(fpath string) (*kotsv1beta1.License, error) { +// ParseLicense parses the license from the given file and returns a LicenseWrapper +// that provides version-agnostic access to both v1beta1 and v1beta2 licenses. +func ParseLicense(fpath string) (*licensewrapper.LicenseWrapper, error) { data, err := os.ReadFile(fpath) if err != nil { - return nil, fmt.Errorf("failed to read license file: %w", err) + return nil, fmt.Errorf("unable to read license file: %w", err) } return ParseLicenseFromBytes(data) } -// ParseLicenseFromBytes parses the license from a byte slice -func ParseLicenseFromBytes(data []byte) (*kotsv1beta1.License, error) { - var license kotsv1beta1.License - if err := kyaml.Unmarshal(data, &license); err != nil { - return nil, ErrNotALicenseFile{Err: err} - } - if license.Spec.LicenseID == "" { - return nil, ErrNotALicenseFile{Err: fmt.Errorf("license id is empty")} +// ParseLicenseFromBytes parses license data from bytes and returns a LicenseWrapper +// that provides version-agnostic access to both v1beta1 and v1beta2 licenses. +func ParseLicenseFromBytes(data []byte) (*licensewrapper.LicenseWrapper, error) { + wrapper, err := licensewrapper.LoadLicenseFromBytes(data) + if err != nil { + return nil, fmt.Errorf("failed to load license: %w", err) } - return &license, nil + return &wrapper, nil } func ParseConfigValues(fpath string) (*kotsv1beta1.ConfigValues, error) { diff --git a/pkg/helpers/parse_test.go b/pkg/helpers/parse_test.go index 18a123ffe2..df48533a21 100644 --- a/pkg/helpers/parse_test.go +++ b/pkg/helpers/parse_test.go @@ -1,17 +1,21 @@ package helpers import ( - "errors" + "embed" "os" "path/filepath" "testing" embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +//go:embed testdata/* +var testdata embed.FS + func TestParseEndUserConfig(t *testing.T) { tests := []struct { name string @@ -113,108 +117,160 @@ kind: Config`, func TestParseLicense(t *testing.T) { tests := []struct { - name string - fpath string - fileContent string - expected *kotsv1beta1.License - wantErr error + name string + licenseFile string + wantErr bool + wantIsV1 bool + wantIsV2 bool + wantAppSlug string + wantLicenseID string + wantECEnabled bool + wantCustomer string }{ { - name: "file does not exist", - fpath: "nonexistent.yaml", - wantErr: os.ErrNotExist, + name: "v1beta1 license", + licenseFile: "testdata/license-v1beta1.yaml", + wantErr: false, + wantIsV1: true, + wantIsV2: false, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v1", + wantECEnabled: true, + wantCustomer: "Test Customer V1", }, { - name: "invalid YAML returns ErrNotALicenseFile", - fpath: "invalid.yaml", - fileContent: `invalid: yaml: content: [ - unclosed bracket`, - wantErr: ErrNotALicenseFile{}, + name: "v1beta2 license", + licenseFile: "testdata/license-v1beta2.yaml", + wantErr: false, + wantIsV1: false, + wantIsV2: true, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v2", + wantECEnabled: true, + wantCustomer: "Test Customer V2", }, { - name: "valid YAML but not a license returns ErrNotALicenseFile", - fpath: "not-license.yaml", - fileContent: `apiVersion: v1 -kind: ConfigMap -metadata: - name: test`, - wantErr: ErrNotALicenseFile{}, + name: "invalid version (v1beta3)", + licenseFile: "testdata/license-invalid-version.yaml", + wantErr: true, }, { - name: "valid license", - fpath: "license.yaml", - fileContent: `apiVersion: kots.io/v1beta1 -kind: License -metadata: - name: test-license -spec: - licenseID: "test-license-id" - appSlug: "test-app" - endpoint: "https://replicated.app"`, - expected: &kotsv1beta1.License{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "kots.io/v1beta1", - Kind: "License", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-license", - }, - Spec: kotsv1beta1.LicenseSpec{ - LicenseID: "test-license-id", - AppSlug: "test-app", - Endpoint: "https://replicated.app", - }, - }, - }, - { - name: "minimal valid license", - fpath: "minimal-license.yaml", - fileContent: `apiVersion: kots.io/v1beta1 -kind: License -spec: - licenseID: "test-license-id"`, - expected: &kotsv1beta1.License{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "kots.io/v1beta1", - Kind: "License", - }, - Spec: kotsv1beta1.LicenseSpec{ - LicenseID: "test-license-id", - }, - }, + name: "file not found", + licenseFile: "testdata/nonexistent.yaml", + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req := require.New(t) - var testFile string - if tt.fileContent != "" { - // Create temporary file + if tt.licenseFile != "testdata/nonexistent.yaml" { + // Read from embedded filesystem and write to temp file + data, err := testdata.ReadFile(tt.licenseFile) + require.NoError(t, err) tmpDir := t.TempDir() - testFile = filepath.Join(tmpDir, tt.fpath) - err := os.WriteFile(testFile, []byte(tt.fileContent), 0644) - req.NoError(err) + testFile = filepath.Join(tmpDir, filepath.Base(tt.licenseFile)) + err = os.WriteFile(testFile, data, 0644) + require.NoError(t, err) } else { - // Use the fpath as-is for non-existent file tests - testFile = tt.fpath + // Use non-existent path for the error case + testFile = tt.licenseFile } - result, err := ParseLicense(testFile) + wrapper, err := ParseLicense(testFile) - if tt.wantErr != nil { - req.Error(err) - if errors.Is(tt.wantErr, ErrNotALicenseFile{}) { - req.ErrorAs(err, &tt.wantErr) - } else { - req.ErrorIs(err, tt.wantErr) - } - req.Nil(result) - } else { - req.NoError(err) - req.Equal(tt.expected, result) + if tt.wantErr { + require.Error(t, err) + require.Nil(t, wrapper) + return + } + + require.NoError(t, err) + require.NotNil(t, wrapper) + assert.Equal(t, tt.wantIsV1, wrapper.IsV1()) + assert.Equal(t, tt.wantIsV2, wrapper.IsV2()) + assert.Equal(t, tt.wantAppSlug, wrapper.GetAppSlug()) + assert.Equal(t, tt.wantLicenseID, wrapper.GetLicenseID()) + assert.Equal(t, tt.wantECEnabled, wrapper.IsEmbeddedClusterDownloadEnabled()) + assert.Equal(t, tt.wantCustomer, wrapper.GetCustomerName()) + }) + } +} + +func TestParseLicenseFromBytes(t *testing.T) { + tests := []struct { + name string + setupData func(t *testing.T) []byte + wantErr bool + wantIsV1 bool + wantIsV2 bool + wantAppSlug string + wantLicenseID string + wantECEnabled bool + }{ + { + name: "v1beta1 license", + setupData: func(t *testing.T) []byte { + data, err := testdata.ReadFile("testdata/license-v1beta1.yaml") + require.NoError(t, err) + return data + }, + wantErr: false, + wantIsV1: true, + wantIsV2: false, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v1", + wantECEnabled: true, + }, + { + name: "v1beta2 license", + setupData: func(t *testing.T) []byte { + data, err := testdata.ReadFile("testdata/license-v1beta2.yaml") + require.NoError(t, err) + return data + }, + wantErr: false, + wantIsV1: false, + wantIsV2: true, + wantAppSlug: "embedded-cluster-test", + wantLicenseID: "test-license-id-v2", + wantECEnabled: true, + }, + { + name: "invalid version (v1beta3)", + setupData: func(t *testing.T) []byte { + return []byte(`apiVersion: kots.io/v1beta3 +kind: License`) + }, + wantErr: true, + }, + { + name: "invalid YAML", + setupData: func(t *testing.T) []byte { + return []byte(`this is not valid yaml: [[[`) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := tt.setupData(t) + wrapper, err := ParseLicenseFromBytes(data) + + if tt.wantErr { + require.Error(t, err) + require.Nil(t, wrapper) + return } + + require.NoError(t, err) + require.NotNil(t, wrapper) + assert.Equal(t, tt.wantIsV1, wrapper.IsV1()) + assert.Equal(t, tt.wantIsV2, wrapper.IsV2()) + assert.Equal(t, tt.wantAppSlug, wrapper.GetAppSlug()) + assert.Equal(t, tt.wantLicenseID, wrapper.GetLicenseID()) + assert.Equal(t, tt.wantECEnabled, wrapper.IsEmbeddedClusterDownloadEnabled()) }) } } diff --git a/pkg/helpers/testdata/license-invalid-version.yaml b/pkg/helpers/testdata/license-invalid-version.yaml new file mode 100644 index 0000000000..4721c535aa --- /dev/null +++ b/pkg/helpers/testdata/license-invalid-version.yaml @@ -0,0 +1,13 @@ +apiVersion: kots.io/v1beta3 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id + licenseType: dev + customerName: Test Customer + customerEmail: test@example.com + endpoint: https://replicated.app + licenseSequence: 1 + isEmbeddedClusterDownloadEnabled: true diff --git a/pkg/helpers/testdata/license-v1beta1.yaml b/pkg/helpers/testdata/license-v1beta1.yaml new file mode 100644 index 0000000000..626cc45daa --- /dev/null +++ b/pkg/helpers/testdata/license-v1beta1.yaml @@ -0,0 +1,34 @@ +apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id-v1 + licenseType: dev + customerName: Test Customer V1 + customerEmail: test@example.com + endpoint: https://replicated.app + channelID: test-channel-id + channelName: Stable + licenseSequence: 1 + isAirgapSupported: true + isGitOpsSupported: false + isIdentityServiceSupported: false + isGeoaxisSupported: false + isSnapshotSupported: true + isSupportBundleUploadSupported: true + isSemverRequired: true + isDisasterRecoverySupported: true + isEmbeddedClusterDownloadEnabled: true + isEmbeddedClusterMultiNodeEnabled: true + replicatedProxyDomain: proxy.replicated.com + entitlements: + expires_at: + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: {} + channels: [] + signature: dGVzdC1saWNlbnNlLXNpZ25hdHVyZQ== diff --git a/pkg/helpers/testdata/license-v1beta2-missing-appslug.yaml b/pkg/helpers/testdata/license-v1beta2-missing-appslug.yaml new file mode 100644 index 0000000000..2aa3419563 --- /dev/null +++ b/pkg/helpers/testdata/license-v1beta2-missing-appslug.yaml @@ -0,0 +1,13 @@ +apiVersion: kots.io/v1beta2 +kind: License +metadata: + name: test-license +spec: + # appSlug is intentionally missing for testing validation + licenseID: test-license-id-v2 + licenseType: dev + customerName: Test Customer V2 + customerEmail: test@example.com + endpoint: https://replicated.app + licenseSequence: 1 + isEmbeddedClusterDownloadEnabled: true diff --git a/pkg/helpers/testdata/license-v1beta2-no-ec-enabled.yaml b/pkg/helpers/testdata/license-v1beta2-no-ec-enabled.yaml new file mode 100644 index 0000000000..7fadd08795 --- /dev/null +++ b/pkg/helpers/testdata/license-v1beta2-no-ec-enabled.yaml @@ -0,0 +1,14 @@ +apiVersion: kots.io/v1beta2 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id-v2 + licenseType: dev + customerName: Test Customer V2 + customerEmail: test@example.com + endpoint: https://replicated.app + licenseSequence: 1 + # isEmbeddedClusterDownloadEnabled is intentionally false for testing validation + isEmbeddedClusterDownloadEnabled: false diff --git a/pkg/helpers/testdata/license-v1beta2.yaml b/pkg/helpers/testdata/license-v1beta2.yaml new file mode 100644 index 0000000000..6ea0b13e64 --- /dev/null +++ b/pkg/helpers/testdata/license-v1beta2.yaml @@ -0,0 +1,34 @@ +apiVersion: kots.io/v1beta2 +kind: License +metadata: + name: test-license +spec: + appSlug: embedded-cluster-test + licenseID: test-license-id-v2 + licenseType: dev + customerName: Test Customer V2 + customerEmail: test@example.com + endpoint: https://replicated.app + channelID: test-channel-id + channelName: Stable + licenseSequence: 1 + isAirgapSupported: true + isGitOpsSupported: false + isIdentityServiceSupported: false + isGeoaxisSupported: false + isSnapshotSupported: true + isSupportBundleUploadSupported: true + isSemverRequired: true + isDisasterRecoverySupported: true + isEmbeddedClusterDownloadEnabled: true + isEmbeddedClusterMultiNodeEnabled: true + replicatedProxyDomain: proxy.replicated.com + entitlements: + expires_at: + title: Expiration + description: License Expiration + value: "" + valueType: String + signature: {} + channels: [] + signature: dGVzdC1saWNlbnNlLXNpZ25hdHVyZQ== diff --git a/pkg/kubeutils/installation.go b/pkg/kubeutils/installation.go index 9498bff5c1..760dbfa45e 100644 --- a/pkg/kubeutils/installation.go +++ b/pkg/kubeutils/installation.go @@ -15,7 +15,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/crds" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -123,7 +123,7 @@ func writeInstallationStatusMessage(writer *spinner.MessageWriter, install *ecv1 type RecordInstallationOptions struct { ClusterID string IsAirgap bool - License *kotsv1beta1.License + License *licensewrapper.LicenseWrapper ConfigSpec *ecv1beta1.ConfigSpec MetricsBaseURL string RuntimeConfig *ecv1beta1.RuntimeConfigSpec @@ -133,6 +133,11 @@ type RecordInstallationOptions struct { } func RecordInstallation(ctx context.Context, kcli client.Client, opts RecordInstallationOptions) (*ecv1beta1.Installation, error) { + // Verify license is available before recording installation + if opts.License == nil { + return nil, fmt.Errorf("license is required for recording installation") + } + // ensure that the embedded-cluster namespace exists ns := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -172,8 +177,8 @@ func RecordInstallation(ctx context.Context, kcli client.Client, opts RecordInst EndUserK0sConfigOverrides: euOverrides, BinaryName: runtimeconfig.AppSlug(), LicenseInfo: &ecv1beta1.LicenseInfo{ - IsDisasterRecoverySupported: opts.License.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: opts.License.Spec.IsEmbeddedClusterMultiNodeEnabled, + IsDisasterRecoverySupported: opts.License.IsDisasterRecoverySupported(), + IsMultiNodeEnabled: opts.License.IsEmbeddedClusterMultiNodeEnabled(), }, }, } diff --git a/pkg/kubeutils/installation_test.go b/pkg/kubeutils/installation_test.go index 0cabcfc0dc..d4f4a5d556 100644 --- a/pkg/kubeutils/installation_test.go +++ b/pkg/kubeutils/installation_test.go @@ -14,6 +14,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/crds" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -208,10 +209,12 @@ func TestRecordInstallation(t *testing.T) { opts: RecordInstallationOptions{ ClusterID: uuid.New().String(), IsAirgap: false, - License: &kotsv1beta1.License{ - Spec: kotsv1beta1.LicenseSpec{ - IsDisasterRecoverySupported: true, - IsEmbeddedClusterMultiNodeEnabled: false, + License: &licensewrapper.LicenseWrapper{ + V1: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + IsDisasterRecoverySupported: true, + IsEmbeddedClusterMultiNodeEnabled: false, + }, }, }, ConfigSpec: &ecv1beta1.ConfigSpec{ @@ -248,12 +251,12 @@ func TestRecordInstallation(t *testing.T) { opts: RecordInstallationOptions{ ClusterID: uuid.New().String(), IsAirgap: true, - License: &kotsv1beta1.License{ + License: &licensewrapper.LicenseWrapper{V1: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ IsDisasterRecoverySupported: false, IsEmbeddedClusterMultiNodeEnabled: true, }, - }, + }}, ConfigSpec: &ecv1beta1.ConfigSpec{ Version: "1.16.0+k8s-1.31", }, @@ -283,12 +286,12 @@ func TestRecordInstallation(t *testing.T) { opts: RecordInstallationOptions{ ClusterID: uuid.New().String(), IsAirgap: true, - License: &kotsv1beta1.License{ + License: &licensewrapper.LicenseWrapper{V1: &kotsv1beta1.License{ Spec: kotsv1beta1.LicenseSpec{ IsDisasterRecoverySupported: false, IsEmbeddedClusterMultiNodeEnabled: false, }, - }, + }}, ConfigSpec: &ecv1beta1.ConfigSpec{ Version: "1.18.0+k8s-1.33", }, diff --git a/pkg/metrics/reporter.go b/pkg/metrics/reporter.go index 53cd0acaa7..fdffbf2388 100644 --- a/pkg/metrics/reporter.go +++ b/pkg/metrics/reporter.go @@ -12,7 +12,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/metrics/types" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/versions" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/pkg/licensewrapper" "github.com/sirupsen/logrus" nodeutil "k8s.io/component-helpers/node/util" ) @@ -32,17 +32,24 @@ func (e ErrorNoFail) Error() string { return e.Err.Error() } -// LicenseID returns the license id. If the license is nil, it returns an empty string. -func LicenseID(license *kotsv1beta1.License) string { - if license != nil { - return license.Spec.LicenseID +// LicenseID returns the license id from a LicenseWrapper. +func LicenseID(license *licensewrapper.LicenseWrapper) string { + if license == nil { + return "" } - return "" + return license.GetLicenseID() } -// License returns the parsed license. If something goes wrong, it returns nil. -func License(licenseFlag string) *kotsv1beta1.License { - license, _ := helpers.ParseLicense(licenseFlag) +// License returns the parsed license as a LicenseWrapper. If something goes wrong, it returns nil. +func License(licenseFlag string) *licensewrapper.LicenseWrapper { + if licenseFlag == "" { + return nil + } + license, err := helpers.ParseLicense(licenseFlag) + if err != nil { + logrus.WithError(err).Warn("failed to parse license") + return nil + } return license }