Skip to content

Commit

Permalink
Support for creating multichannel licenses (#398)
Browse files Browse the repository at this point in the history
* Support for creating multichannel licenses

* Log when an explicit default channel is not provided

* Include required --name example in usage one-liner

* Actually flip found default flag, and error if default chan not in set

---------

Co-authored-by: pandemicsyn <[email protected]>
  • Loading branch information
marccampbell and pandemicsyn authored Jul 10, 2024
1 parent 2b06405 commit 889daf0
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 26 deletions.
6 changes: 3 additions & 3 deletions cli/cmd/cluster_prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ func (r *runners) InitClusterPrepare(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "prepare",
Short: "prepare cluster for testing",
Long: `The cluster prepare command will provision a cluster and install
Long: `The cluster prepare command will provision a cluster and install
a local helm chart with a custom values.yaml and custom replicated sdk entitlements.
This is a higher level CLI command that is useful in CI when you have a Helm chart and
want it running in a variety of clusters.
For more control over the workflow, consider using the cluster create command and
For more control over the workflow, consider using the cluster create command and
using kubectl and helm CLI tools to install your application.
Example:
Expand Down Expand Up @@ -235,7 +235,7 @@ func (r *runners) prepareCluster(_ *cobra.Command, args []string) error {
email := fmt.Sprintf("%[email protected]", clusterName)
customerOpts := kotsclient.CreateCustomerOpts{
Name: clusterName,
ChannelID: "",
Channels: []kotsclient.CustomerChannel{},
AppID: r.appID,
LicenseType: "test",
Email: email,
Expand Down
45 changes: 39 additions & 6 deletions cli/cmd/customer_create.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package cmd

import (
"os"
"time"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/cli/print"
"github.com/replicatedhq/replicated/client"
"github.com/replicatedhq/replicated/pkg/kotsclient"
"github.com/replicatedhq/replicated/pkg/logger"
"github.com/spf13/cobra"
)

Expand All @@ -22,7 +24,8 @@ func (r *runners) InitCustomersCreateCommand(parent *cobra.Command) *cobra.Comma
parent.AddCommand(cmd)
cmd.Flags().StringVar(&r.args.customerCreateName, "name", "", "Name of the customer")
cmd.Flags().StringVar(&r.args.customerCreateCustomID, "custom-id", "", "Set a custom customer ID to more easily tie this customer record to your external data systems")
cmd.Flags().StringVar(&r.args.customerCreateChannel, "channel", "", "Release channel to which the customer should be assigned")
cmd.Flags().StringArrayVar(&r.args.customerCreateChannel, "channel", []string{}, "Release channel to which the customer should be assigned (can be specified multiple times)")
cmd.Flags().StringVar(&r.args.customerCreateDefaultChannel, "default-channel", "", "Which of the specified channels should be the default channel. if not set, the first channel specified will be the default channel.")
cmd.Flags().DurationVar(&r.args.customerCreateExpiryDuration, "expires-in", 0, "If set, an expiration date will be set on the license. Supports Go durations like '72h' or '3600m'")
cmd.Flags().BoolVar(&r.args.customerCreateEnsureChannel, "ensure-channel", false, "If set, channel will be created if it does not exist.")
cmd.Flags().BoolVar(&r.args.customerCreateIsAirgapEnabled, "airgap", false, "If set, the license will allow airgap installs.")
Expand All @@ -38,6 +41,9 @@ func (r *runners) InitCustomersCreateCommand(parent *cobra.Command) *cobra.Comma
cmd.Flags().StringVar(&r.args.customerCreateEmail, "email", "", "Email address of the customer that is to be created.")
cmd.Flags().StringVar(&r.args.customerCreateType, "type", "dev", "The license type to create. One of: dev|trial|paid|community|test (default: dev)")
cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)")

cmd.MarkFlagRequired("channel")

return cmd
}

Expand All @@ -58,12 +64,14 @@ func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) {
r.args.customerCreateType = "prod"
}

channelID := ""
if r.args.customerCreateChannel != "" {
channels := []kotsclient.CustomerChannel{}

foundDefaultChannel := false
for _, requestedChannel := range r.args.customerCreateChannel {
getOrCreateChannelOptions := client.GetOrCreateChannelOptions{
AppID: r.appID,
AppType: r.appType,
NameOrID: r.args.customerCreateChannel,
NameOrID: requestedChannel,
Description: "",
CreateIfAbsent: r.args.customerCreateEnsureChannel,
}
Expand All @@ -73,13 +81,38 @@ func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) {
return errors.Wrap(err, "get channel")
}

channelID = channel.ID
customerChannel := kotsclient.CustomerChannel{
ID: channel.ID,
}

if r.args.customerCreateDefaultChannel == requestedChannel {
customerChannel.IsDefault = true
foundDefaultChannel = true
}

channels = append(channels, customerChannel)
}

if len(channels) == 0 {
return errors.New("no channels found")
}

if r.args.customerUpdateDefaultChannel != "" && !foundDefaultChannel {
return errors.New("default channel not found in specified channels")
}

if !foundDefaultChannel {
log := logger.NewLogger(os.Stdout)
log.Info("No default channel specified, defaulting to the first channel specified.")
firstChannel := channels[0]
firstChannel.IsDefault = true
channels[0] = firstChannel
}

opts := kotsclient.CreateCustomerOpts{
Name: r.args.customerCreateName,
CustomID: r.args.customerCreateCustomID,
ChannelID: channelID,
Channels: channels,
AppID: r.appID,
ExpiresAtDuration: r.args.customerCreateExpiryDuration,
IsAirgapEnabled: r.args.customerCreateIsAirgapEnabled,
Expand Down
52 changes: 43 additions & 9 deletions cli/cmd/customer_update.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package cmd

import (
"os"
"time"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/cli/print"
"github.com/replicatedhq/replicated/client"
"github.com/replicatedhq/replicated/pkg/kotsclient"
"github.com/replicatedhq/replicated/pkg/logger"
"github.com/spf13/cobra"
)

func (r *runners) InitCustomerUpdateCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Use: "update --customer <id> --name <name> [options]",
Short: "update a customer",
Long: `update a customer`,
RunE: r.updateCustomer,
Expand All @@ -21,10 +23,10 @@ func (r *runners) InitCustomerUpdateCommand(parent *cobra.Command) *cobra.Comman
}
parent.AddCommand(cmd)
cmd.Flags().StringVar(&r.args.customerUpdateID, "customer", "", "The ID of the customer to update")
_ = cmd.MarkFlagRequired("customer")
cmd.Flags().StringVar(&r.args.customerUpdateName, "name", "", "Name of the customer")
cmd.Flags().StringVar(&r.args.customerUpdateCustomID, "custom-id", "", "Set a custom customer ID to more easily tie this customer record to your external data systems")
cmd.Flags().StringVar(&r.args.customerUpdateChannel, "channel", "", "Release channel to which the customer should be assigned")
cmd.Flags().StringArrayVar(&r.args.customerUpdateChannel, "channel", []string{}, "Release channel to which the customer should be assigned (can be specified multiple times)")
cmd.Flags().StringVar(&r.args.customerUpdateDefaultChannel, "default-channel", "", "Which of the specified channels should be the default channel. if not set, the first channel specified will be the default channel.")
cmd.Flags().DurationVar(&r.args.customerUpdateExpiryDuration, "expires-in", 0, "If set, an expiration date will be set on the license. Supports Go durations like '72h' or '3600m'")
cmd.Flags().BoolVar(&r.args.customerUpdateEnsureChannel, "ensure-channel", false, "If set, channel will be created if it does not exist.")
cmd.Flags().BoolVar(&r.args.customerUpdateIsAirgapEnabled, "airgap", false, "If set, the license will allow airgap installs.")
Expand All @@ -39,6 +41,11 @@ func (r *runners) InitCustomerUpdateCommand(parent *cobra.Command) *cobra.Comman
cmd.Flags().StringVar(&r.args.customerUpdateEmail, "email", "", "Email address of the customer that is to be updated.")
cmd.Flags().StringVar(&r.args.customerUpdateType, "type", "dev", "The license type to update. One of: dev|trial|paid|community|test (default: dev)")
cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)")

cmd.MarkFlagRequired("customer")
cmd.MarkFlagRequired("channel")
cmd.MarkFlagRequired("name") // until the API supports better patching, this is actually a required field

return cmd
}

Expand All @@ -62,28 +69,55 @@ func (r *runners) updateCustomer(cmd *cobra.Command, _ []string) (err error) {
r.args.customerUpdateType = "prod"
}

channelID := ""
if r.args.customerUpdateChannel != "" {
channels := []kotsclient.CustomerChannel{}

foundDefaultChannel := false
for _, requestedChannel := range r.args.customerUpdateChannel {
getOrCreateChannelOptions := client.GetOrCreateChannelOptions{
AppID: r.appID,
AppType: r.appType,
NameOrID: r.args.customerUpdateChannel,
NameOrID: requestedChannel,
Description: "",
CreateIfAbsent: r.args.customerUpdateEnsureChannel,
CreateIfAbsent: r.args.customerCreateEnsureChannel,
}

channel, err := r.api.GetOrCreateChannelByName(getOrCreateChannelOptions)
if err != nil {
return errors.Wrap(err, "get channel")
}

channelID = channel.ID
customerChannel := kotsclient.CustomerChannel{
ID: channel.ID,
}

if r.args.customerUpdateDefaultChannel == requestedChannel {
customerChannel.IsDefault = true
foundDefaultChannel = true
}

channels = append(channels, customerChannel)
}

if len(channels) == 0 {
return errors.New("no channels found")
}

if r.args.customerUpdateDefaultChannel != "" && !foundDefaultChannel {
return errors.New("default channel not found in specified channels")
}

if !foundDefaultChannel {
log := logger.NewLogger(os.Stdout)
log.Info("No default channel specified, defaulting to the first channel specified.")
firstChannel := channels[0]
firstChannel.IsDefault = true
channels[0] = firstChannel
}

opts := kotsclient.UpdateCustomerOpts{
Name: r.args.customerUpdateName,
CustomID: r.args.customerUpdateCustomID,
ChannelID: channelID,
Channels: channels,
AppID: r.appID,
ExpiresAtDuration: r.args.customerUpdateExpiryDuration,
IsAirgapEnabled: r.args.customerUpdateIsAirgapEnabled,
Expand Down
6 changes: 4 additions & 2 deletions cli/cmd/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ type runnerArgs struct {
customerArchiveNameOrId string
customerCreateName string
customerCreateCustomID string
customerCreateChannel string
customerCreateChannel []string
customerCreateDefaultChannel string
customerCreateEnsureChannel bool
customerCreateExpiryDuration time.Duration
customerCreateIsAirgapEnabled bool
Expand All @@ -97,7 +98,8 @@ type runnerArgs struct {
customerUpdateID string
customerUpdateName string
customerUpdateCustomID string
customerUpdateChannel string
customerUpdateChannel []string
customerUpdateDefaultChannel string
customerUpdateEnsureChannel bool
customerUpdateExpiryDuration time.Duration
customerUpdateIsAirgapEnabled bool
Expand Down
12 changes: 9 additions & 3 deletions pkg/kotsclient/customer_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ type EntitlementValue struct {
Value string `json:"value"`
}

type CustomerChannel struct {
ID string `json:"channel_id"`
PinnedChannelSequence *int64 `json:"pinned_channel_sequence"`
IsDefault bool `json:"is_default_for_customer"`
}

type CreateCustomerRequest struct {
Name string `json:"name"`
ChannelID string `json:"channel_id"`
Channels []CustomerChannel `json:"channels"`
CustomID string `json:"custom_id"`
AppID string `json:"app_id"`
Type string `json:"type"`
Expand All @@ -41,7 +47,7 @@ type CreateCustomerResponse struct {
type CreateCustomerOpts struct {
Name string
CustomID string
ChannelID string
Channels []CustomerChannel
AppID string
ExpiresAt string
ExpiresAtDuration time.Duration
Expand All @@ -64,7 +70,7 @@ func (c *VendorV3Client) CreateCustomer(opts CreateCustomerOpts) (*types.Custome
request := &CreateCustomerRequest{
Name: opts.Name,
CustomID: opts.CustomID,
ChannelID: opts.ChannelID,
Channels: opts.Channels,
AppID: opts.AppID,
Type: opts.LicenseType,
IsAirgapEnabled: opts.IsAirgapEnabled,
Expand Down
6 changes: 3 additions & 3 deletions pkg/kotsclient/customer_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

type UpdateCustomerRequest struct {
Name string `json:"name"`
ChannelID string `json:"channel_id"`
Channels []CustomerChannel `json:"channels"`
CustomID string `json:"custom_id"`
AppID string `json:"app_id"`
Type string `json:"type"`
Expand All @@ -36,7 +36,7 @@ type UpdateCustomerResponse struct {
type UpdateCustomerOpts struct {
Name string
CustomID string
ChannelID string
Channels []CustomerChannel
AppID string
ExpiresAt string
ExpiresAtDuration time.Duration
Expand All @@ -58,7 +58,7 @@ func (c *VendorV3Client) UpdateCustomer(customerID string, opts UpdateCustomerOp
request := &UpdateCustomerRequest{
Name: opts.Name,
CustomID: opts.CustomID,
ChannelID: opts.ChannelID,
Channels: opts.Channels,
AppID: opts.AppID,
Type: opts.LicenseType,
IsAirgapEnabled: opts.IsAirgapEnabled,
Expand Down
18 changes: 18 additions & 0 deletions pkg/platformclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ func (c *HTTPClient) DoJSON(method string, path string, successStatus int, reqBo
}
if resp.StatusCode != successStatus {
if resp.StatusCode == http.StatusForbidden {
// look for a response message in the body
body, err := io.ReadAll(resp.Body)
if err != nil {
return ErrForbidden
}

// some of the methods in the api have a standardized response for 403
type forbiddenResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
var fr forbiddenResponse
if err := json.Unmarshal(body, &fr); err == nil {
return errors.New(fr.Error.Message)
}

return ErrForbidden
}
body, _ := io.ReadAll(resp.Body)
Expand Down

0 comments on commit 889daf0

Please sign in to comment.