Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for multi channel licenses #4767

Merged
merged 42 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
984a1bd
migration to app.channel_id column
Jul 17, 2024
8b2c5e3
Store (and return) channel_id used at install time
Jul 17, 2024
ddfbc72
Merge branch 'main' into florian/feat/multichan-licenses
Jul 18, 2024
d1ebe36
Bump kotskinds package
Jul 18, 2024
e28678b
Install/Update use stored app.channel_id
Jul 18, 2024
53fac7f
cleanup
Jul 19, 2024
b635366
Merge branch 'main' into florian/feat/multichan-licenses
Jul 19, 2024
1355d88
Start supporting IsSemversion checks using the license channel entry
Jul 19, 2024
b5ae7c4
Backfill app.channel_id when required
Jul 19, 2024
f2dec75
kots pull checks if channel slug in license
Jul 19, 2024
cf7faa8
Airgap license channel id checks use the new channels obj
Jul 21, 2024
c5b647a
Merge branch 'main' into florian/feat/multichan-licenses
Jul 21, 2024
ba4eed9
pr feedback - logging
Jul 22, 2024
bd38373
pr feedback - fetch latest license at install check
Jul 22, 2024
c8cab80
clean up
Jul 22, 2024
fb0e0f0
Update tests
Jul 23, 2024
f0c5f74
Wordsmith error message for product
Jul 23, 2024
7ac8b94
fix - kots pull/install only prevalidate multichan licenses
Jul 24, 2024
8d78738
Update cmd/kots/cli/install.go
pandemicsyn Jul 25, 2024
8fb86a2
pr feedback
Jul 25, 2024
a5e4bd9
Actually use/set AppChannelID's in Upstream/FetchOptions
Jul 26, 2024
100b3c4
Merge branch 'main' into florian/feat/multichan-licenses
Jul 26, 2024
5ca3f8f
Update pkg/multichannel/multichannel.go
pandemicsyn Jul 29, 2024
4ac83cb
Consolidate backfilling to GetOrBackfillLicenseChannel
Jul 29, 2024
2b16bf1
Update migrations/tables/app.yaml
pandemicsyn Jul 30, 2024
c39c455
pr feedback - rename channel_id -> selected_channel_id
Jul 30, 2024
8e0c2df
Update pkg/kotsutil/kots.go
pandemicsyn Jul 30, 2024
61ea5d5
move multichannel.go to license package
Jul 30, 2024
83e7f4f
Merge branch 'florian/feat/multichan-licenses' of github.com:replicat…
Jul 30, 2024
f2624c8
general pr feedback
Jul 30, 2024
45ababa
getRequiredAirgapUpdates uses selectedChannelID to select license chan
Jul 30, 2024
f38716c
Replicate app api calls pass selectedChannelId
Jul 31, 2024
4c47efd
Merge branch 'main' into florian/feat/multichan-licenses
Jul 31, 2024
ab0f4d7
remove selectedAppChannel update from operator
Jul 31, 2024
fb6e551
Update selected_channel_id on every single channel license change
Jul 31, 2024
7931526
Finish channel_id -> selected_channel_id rename
Aug 1, 2024
1bebcb7
centralized backfill and clean up - only using license chan where app…
Aug 2, 2024
4b5a4f0
update license for pull integration test
Aug 2, 2024
5e67d05
Pass selected channel to pull options, and allow overriding in Render…
Aug 2, 2024
3cd0a79
pr feedback
Aug 6, 2024
a19443b
Merge branch 'main' into florian/feat/multichan-licenses
Aug 6, 2024
3961a04
Add playwright tests for channel change
Aug 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cmd/kots/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ func InstallCmd() *cobra.Command {
}()

upstream := pull.RewriteUpstream(args[0])
preferredChannelSlug, err := extractPreferredChannelSlug(upstream)
if err != nil {
return errors.Wrap(err, "failed to extract preferred channel slug")
}

// If we are passed a multi-channel license, verify that the requested channel is in the license
// so that we can warn the user immediately if it is not.
if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) {
pandemicsyn marked this conversation as resolved.
Show resolved Hide resolved
return errors.New("requested channel not found in license")
}

namespace := v.GetString("namespace")

Expand Down Expand Up @@ -278,6 +288,7 @@ func InstallCmd() *cobra.Command {
IncludeMinio: v.GetBool("with-minio"),
IncludeMinioSnapshots: v.GetBool("with-minio"),
StrictSecurityContext: v.GetBool("strict-security-context"),
RequestedChannelSlug: preferredChannelSlug,

RegistryConfig: *registryConfig,

Expand Down Expand Up @@ -1076,3 +1087,12 @@ func checkPreflightResults(response *handlers.GetPreflightResultResponse, skipPr

return true, nil
}

func slugInLicenseChannels(slug string, license *kotsv1beta1.License) bool {
for _, channel := range license.Spec.Channels {
if channel.ChannelSlug == slug {
return true
}
}
return false
}
44 changes: 19 additions & 25 deletions cmd/kots/cli/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/replicatedhq/kots/pkg/pull"
registrytypes "github.com/replicatedhq/kots/pkg/registry/types"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/replicatedhq/kotskinds/client/kotsclientset/scheme"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -43,11 +42,13 @@ func PullCmd() *cobra.Command {
}
}

appSlug, err := getAppSlugForPull(args[0], v.GetString("license-file"))
license, err := getLicense(v)
if err != nil {
return errors.Wrap(err, "failed to determine app slug")
return errors.Wrap(err, "failed to get license")
}

appSlug := getAppSlugForPull(args[0], license)

namespace, err := getNamespaceOrDefault(v.GetString("namespace"))
if err != nil {
return errors.Wrap(err, "failed to get namespace")
Expand Down Expand Up @@ -98,6 +99,17 @@ func PullCmd() *cobra.Command {
}

upstream := pull.RewriteUpstream(args[0])
preferredChannelSlug, err := extractPreferredChannelSlug(upstream)
if err != nil {
return errors.Wrap(err, "failed to extract preferred channel slug")
}

// If we are passed a multi-channel license, verify that the requested channel is in the license
// so that we can warn the user immediately if it is not.
if license != nil && !slugInLicenseChannels(preferredChannelSlug, license) {
return errors.New("requested channel not found in license")
}

renderDir, err := pull.Pull(upstream, pullOptions)
if err != nil {
return errors.Wrap(err, "failed to pull")
Expand Down Expand Up @@ -146,28 +158,10 @@ func PullCmd() *cobra.Command {
return cmd
}

func getAppSlugForPull(uri string, licenseFile string) (string, error) {
func getAppSlugForPull(uri string, license *kotsv1beta1.License) string {
appSlug := strings.Split(uri, "/")[0]
if licenseFile == "" {
return appSlug, nil
}

licenseData, err := os.ReadFile(licenseFile)
if err != nil {
return "", errors.Wrap(err, "failed to read license file")
}

decode := scheme.Codecs.UniversalDeserializer().Decode
decoded, gvk, err := decode(licenseData, nil, nil)
if err != nil {
return "", errors.Wrap(err, "unable to decode license file")
if license == nil {
return appSlug
}

if gvk.Group != "kots.io" || gvk.Version != "v1beta1" || gvk.Kind != "License" {
return "", errors.New("not an application license")
}

license := decoded.(*kotsv1beta1.License)

return license.Spec.AppSlug, nil
return license.Spec.AppSlug
}
18 changes: 18 additions & 0 deletions cmd/kots/cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/pkg/errors"
"github.com/replicatedhq/kots/pkg/replicatedapp"
"github.com/replicatedhq/kots/pkg/util"
)

Expand Down Expand Up @@ -51,3 +52,20 @@ func splitEndpointAndNamespace(endpoint string) (string, string) {
}
return registryEndpoint, registryNamespace
}

func extractPreferredChannelSlug(upstreamURI string) (string, error) {
u, err := url.ParseRequestURI(upstreamURI)
if err != nil {
return "", errors.Wrap(err, "failed to parse uri")
}

replicatedUpstream, err := replicatedapp.ParseReplicatedURL(u)
if err != nil {
return "", errors.Wrap(err, "failed to parse replicated url")
}

if replicatedUpstream.Channel != nil {
return *replicatedUpstream.Channel, nil
}
return "stable", nil
}
49 changes: 49 additions & 0 deletions cmd/kots/cli/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,52 @@ func Test_getHostFromEndpoint(t *testing.T) {
})
}
}

func Test_extractPreferredChannelSlug(t *testing.T) {
type args struct {
upstreamURI string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
"no channel",
args{
upstreamURI: "replicated://app-slug",
},
"stable", // default channel
false,
},
{
"with channel",
args{
upstreamURI: "replicated://app-slug/channel",
},
"channel",
false,
},
{
"invalid uri",
args{
upstreamURI: "junk",
},
"",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractPreferredChannelSlug(tt.args.upstreamURI)
if (err != nil) != tt.wantErr {
t.Errorf("extractPreferredChannelSlug() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractPreferredChannelSlug() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/replicatedhq/embedded-cluster-kinds v1.4.6
github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2
github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95
github.com/replicatedhq/kurlkinds v1.5.0
github.com/replicatedhq/troubleshoot v0.95.1
github.com/replicatedhq/yaml/v3 v3.0.0-beta5-replicatedhq
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,10 @@ github.com/replicatedhq/embedded-cluster-kinds v1.4.6 h1:nP2/ANhUW1omiPp3WFm6tQW
github.com/replicatedhq/embedded-cluster-kinds v1.4.6/go.mod h1:AwopUvvGcaWO4mn9DkbPj5RnLuOy756CNLrcaAlmjMo=
github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2 h1:xL4u2RHhMaGDgz7Lol5MhVYLnWahV3sCJZbfebpPao0=
github.com/replicatedhq/kotskinds v0.0.0-20240621084729-1eb1e3eac6f2/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41 h1:xvvRq5EZ7wBlsrDZIAUpJ318cEMzTpt8zVAO2atFQlM=
github.com/replicatedhq/kotskinds v0.0.0-20240717145110-8eb39e8b3a41/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95 h1:JhwPz4Bgbz5iYl3UV2EB+HnF9oW/eCRi+hASAz+J6XI=
github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I=
github.com/replicatedhq/kurlkinds v1.5.0 h1:zZ0PKNeh4kXvSzVGkn62DKTo314GxhXg1TSB3azURMc=
github.com/replicatedhq/kurlkinds v1.5.0/go.mod h1:rUpBMdC81IhmJNCWMU/uRsMETv9P0xFoMvdSP/TAr5A=
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 h1:eRlNDHxGfVkPCRXbA4BfQJvt5DHjFiTtWy3R/t4djyY=
Expand Down
2 changes: 2 additions & 0 deletions migrations/tables/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ spec:
default: 0
constraints:
notNull: true
- name: channel_id
pandemicsyn marked this conversation as resolved.
Show resolved Hide resolved
type: text
5 changes: 5 additions & 0 deletions pkg/app/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type App struct {
InstallState string `json:"installState"`
LastLicenseSync string `json:"lastLicenseSync"`
ChannelChanged bool `json:"channelChanged"`
ChannelID string `json:"channel_id"`
}

func (a *App) GetID() string {
Expand All @@ -44,6 +45,10 @@ func (a *App) GetCurrentSequence() int64 {
return a.CurrentSequence
}

func (a *App) GetChannelID() string {
return a.ChannelID
}

func (a *App) GetIsAirgap() bool {
return a.IsAirgap
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/automation/automation.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,12 @@ func installLicenseSecret(clientset *kubernetes.Clientset, licenseSecret corev1.
desiredAppName := strings.Replace(appSlug, "-", " ", 0)
upstreamURI := fmt.Sprintf("replicated://%s", appSlug)

a, err := store.GetStore().CreateApp(desiredAppName, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly)
matchedChannelID, err := kotsutil.FindChannelIDInLicense(instParams.RequestedChannelSlug, verifiedLicense)
if err != nil {
return errors.Wrap(err, "failed to find requested channel in license")
}

a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, string(license), verifiedLicense.Spec.IsAirgapSupported, instParams.SkipImagePush, instParams.RegistryIsReadOnly)
if err != nil {
return errors.Wrap(err, "failed to create app record")
}
Expand Down
11 changes: 10 additions & 1 deletion pkg/handlers/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,16 @@ func (h *Handler) UploadNewLicense(w http.ResponseWriter, r *http.Request) {
desiredAppName := strings.Replace(verifiedLicense.Spec.AppSlug, "-", " ", 0)
upstreamURI := fmt.Sprintf("replicated://%s", verifiedLicense.Spec.AppSlug)

a, err := store.GetStore().CreateApp(desiredAppName, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly)
// verify that requested channel slug exists in the license
matchedChannelID, err := kotsutil.FindChannelIDInLicense(installationParams.RequestedChannelSlug, verifiedLicense)
if err != nil {
logger.Error(err)
uploadLicenseResponse.Error = "Requested install channel not found in license"
JSON(w, http.StatusBadRequest, uploadLicenseResponse)
return
}

a, err := store.GetStore().CreateApp(desiredAppName, matchedChannelID, upstreamURI, licenseString, verifiedLicense.Spec.IsAirgapSupported, installationParams.SkipImagePush, installationParams.RegistryIsReadOnly)
if err != nil {
logger.Error(err)
uploadLicenseResponse.Error = err.Error()
Expand Down
22 changes: 21 additions & 1 deletion pkg/handlers/update_checker_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/store"
"github.com/replicatedhq/kots/pkg/updatechecker"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
cron "github.com/robfig/cron/v3"
)

Expand Down Expand Up @@ -56,8 +57,27 @@ func (h *Handler) SetAutomaticUpdatesConfig(w http.ResponseWriter, r *http.Reque
return
}

var licenseChan *kotsv1beta1.Channel
if foundApp.ChannelID == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto: wouldn't using GetOrBackfillLicenseChannel suffice here?

backfillID := kotsutil.GetBackfillChannelIDFromLicense(license)
if err := store.GetStore().SetAppChannelID(foundApp.ID, backfillID); err != nil {
updateCheckerSpecResponse.Error = "failed to backfill app channel id from license"
logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error))
JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse)
return
}
foundApp.ChannelID = backfillID
}

if licenseChan, err = kotsutil.FindChannelInLicense(foundApp.ChannelID, license); err != nil {
updateCheckerSpecResponse.Error = "failed to find channel in license"
logger.Error(errors.Wrap(err, updateCheckerSpecResponse.Error))
JSON(w, http.StatusInternalServerError, updateCheckerSpecResponse)
return
}

// Check if the deploy update configuration is valid based on app channel
if license.Spec.IsSemverRequired {
if licenseChan.IsSemverRequired {
if configureAutomaticUpdatesRequest.AutoDeploy == apptypes.AutoDeploySequence {
updateCheckerSpecResponse.Error = "automatic updates based on sequence type are not supported for semantic versioning apps"
JSON(w, http.StatusUnprocessableEntity, updateCheckerSpecResponse)
Expand Down
5 changes: 4 additions & 1 deletion pkg/handlers/upgrade_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ func canStartUpgradeService(a *apptypes.App, r StartUpgradeServiceRequest) (bool
if err != nil {
return false, "", errors.Wrap(err, "failed to find airgap metadata")
}
if currLicense.Spec.ChannelID != airgap.Spec.ChannelID || r.ChannelID != airgap.Spec.ChannelID {
if _, err := kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, currLicense); err != nil {
return false, "channel mismatch, channel not in license", nil
}
if r.ChannelID != airgap.Spec.ChannelID {
return false, "channel mismatch", nil
}
isDeployable, nonDeployableCause, err := update.IsAirgapUpdateDeployable(a, airgap)
Expand Down
2 changes: 2 additions & 0 deletions pkg/kotsadm/objects/configmaps_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ func KotsadmConfigMap(deployOptions types.DeployOptions) *corev1.ConfigMap {
"wait-duration": fmt.Sprintf("%v", deployOptions.Timeout),
"with-minio": fmt.Sprintf("%v", deployOptions.IncludeMinio),
"app-version-label": deployOptions.AppVersionLabel,
"requested-channel-slug": deployOptions.RequestedChannelSlug,
}

if kotsadmversion.KotsadmPullSecret(deployOptions.Namespace, deployOptions.RegistryConfig) != nil {
data["kotsadm-registry"] = kotsadmversion.KotsadmRegistry(deployOptions.RegistryConfig)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/kotsadm/types/deployoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type DeployOptions struct {
IsMinimalRBAC bool
AdditionalNamespaces []string
IsGKEAutopilot bool
RequestedChannelSlug string

IdentityConfig kotsv1beta1.IdentityConfig
IngressConfig kotsv1beta1.IngressConfig
Expand Down
Loading
Loading