diff --git a/cli/cmd/customer_archive.go b/cli/cmd/customer_archive.go index b5e56ac8c..dad39cf6f 100644 --- a/cli/cmd/customer_archive.go +++ b/cli/cmd/customer_archive.go @@ -7,30 +7,49 @@ import ( func (r *runners) InitCustomersArchiveCommand(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ - Use: "archive", - Short: "archive a customer", - Long: `archive a customer`, + Use: "archive [flags]", + Short: "Archive a customer", + Long: `The archive command allows you to archive a specific customer. + +This command will mark a customer as archived in the system. Archived customers +are typically those who are no longer active or have terminated their service. +Archiving a customer does not delete their data but changes their status in the system. + +You must specify the customer using either their name or ID with the --customer flag.`, + Example: ` # Archive a customer by ID + replicated customer archive --customer cus_abcdef123456 + + # Archive a customer by name + replicated customer archive --customer "Acme Inc" + + # Archive a customer for a specific app (if you have multiple apps) + replicated customer archive --app myapp --customer "Acme Inc"`, RunE: r.archiveCustomer, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVar(&r.args.customerArchiveNameOrId, "customer", "", "The Customer Name or ID") + cmd.Flags().String("customer", "", "The Customer Name or ID") + cmd.MarkFlagRequired("customer") return cmd } -func (r *runners) archiveCustomer(_ *cobra.Command, _ []string) error { - if r.args.customerArchiveNameOrId == "" { - return errors.Errorf("missing or invalid parameters: customer") +func (r *runners) archiveCustomer(cmd *cobra.Command, _ []string) error { + customerNameOrId, err := cmd.Flags().GetString("customer") + if err != nil { + return errors.Wrap(err, "get customer flag") + } + if customerNameOrId == "" { + return errors.New("missing or invalid parameters: customer") } - customer, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, r.args.customerArchiveNameOrId) + customer, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, customerNameOrId) if err != nil { - return errors.Wrapf(err, "find customer %q", r.args.customerArchiveNameOrId) + return errors.Wrapf(err, "find customer %q", customerNameOrId) } if err := r.api.ArchiveCustomer(r.appType, customer.ID); err != nil { - return errors.Wrapf(err, "archive customer %q", r.args.customerArchiveNameOrId) + return errors.Wrapf(err, "archive customer %q", customerNameOrId) } return nil diff --git a/cli/cmd/customer_create.go b/cli/cmd/customer_create.go index 4614c74ec..b72189bd7 100644 --- a/cli/cmd/customer_create.go +++ b/cli/cmd/customer_create.go @@ -12,68 +12,118 @@ import ( "github.com/spf13/cobra" ) +type createCustomerOpts struct { + Name string + CustomID string + ChannelNames []string + DefaultChannel string + ExpiryDuration time.Duration + EnsureChannel bool + IsAirgapEnabled bool + IsGitopsSupported bool + IsSnapshotSupported bool + IsKotsInstallEnabled bool + IsEmbeddedClusterEnabled bool + IsGeoaxisSupported bool + IsHelmVMDownloadEnabled bool + IsIdentityServiceSupported bool + IsInstallerSupportEnabled bool + IsSupportBundleUploadEnabled bool + Email string + CustomerType string +} + func (r *runners) InitCustomersCreateCommand(parent *cobra.Command) *cobra.Command { + opts := createCustomerOpts{} + var outputFormat string + cmd := &cobra.Command{ - Use: "create", - Short: "create a customer", - Long: `create a customer`, - RunE: r.createCustomer, + Use: "create", + Short: "Create a new customer for the current application", + Long: `Create a new customer for the current application with specified attributes. + +This command allows you to create a customer record with various properties such as name, +custom ID, channels, license type, and feature flags. You can set expiration dates, +enable or disable specific features, and assign the customer to one or more channels. + +The --app flag must be set to specify the target application.`, + Example: ` # Create a basic customer with a name and assigned to a channel + replicated customer create --app myapp --name "Acme Inc" --channel stable + + # Create a customer with multiple channels and a custom ID + replicated customer create --app myapp --name "Beta Corp" --custom-id "BETA123" --channel beta --channel stable + + # Create a paid customer with specific features enabled + replicated customer create --app myapp --name "Enterprise Ltd" --type paid --channel enterprise --airgap --snapshot + + # Create a trial customer with an expiration date + replicated customer create --app myapp --name "Trial User" --type trial --channel stable --expires-in 720h + + # Create a customer with all available options + replicated customer create --app myapp --name "Full Options Inc" --custom-id "FULL001" \ + --channel stable --channel beta --default-channel stable --type paid \ + --email "contact@fulloptions.com" --expires-in 8760h \ + --airgap --snapshot --kots-install --embedded-cluster-download \ + --support-bundle-upload --ensure-channel`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.createCustomer(cmd, opts, outputFormat) + }, SilenceUsage: false, - SilenceErrors: true, // this command uses custom error printing + SilenceErrors: true, } + 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().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.") - cmd.Flags().BoolVar(&r.args.customerCreateIsGitopsSupported, "gitops", false, "If set, the license will allow the GitOps usage.") - cmd.Flags().BoolVar(&r.args.customerCreateIsSnapshotSupported, "snapshot", false, "If set, the license will allow Snapshots.") - cmd.Flags().BoolVar(&r.args.customerCreateIsKotsInstallEnabled, "kots-install", true, "If set, the license will allow KOTS install. Otherwise license will allow Helm CLI installs only.") - cmd.Flags().BoolVar(&r.args.customerCreateIsEmbeddedClusterDownloadEnabled, "embedded-cluster-download", false, "If set, the license will allow embedded cluster downloads.") - cmd.Flags().BoolVar(&r.args.customerCreateIsGeoaxisSupported, "geo-axis", false, "If set, the license will allow Geo Axis usage.") - cmd.Flags().BoolVar(&r.args.customerCreateIsHelmVMDownloadEnabled, "helmvm-cluster-download", false, "If set, the license will allow helmvm cluster downloads.") - cmd.Flags().BoolVar(&r.args.customerCreateIsIdentityServiceSupported, "identity-service", false, "If set, the license will allow Identity Service usage.") - cmd.Flags().BoolVar(&r.args.customerCreateIsInstallerSupportEnabled, "installer-support", false, "If set, the license will allow installer support.") - cmd.Flags().BoolVar(&r.args.customerCreateIsSupportBundleUploadEnabled, "support-bundle-upload", false, "If set, the license will allow uploading support bundles.") - 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.Flags().StringVar(&opts.Name, "name", "", "Name of the customer") + cmd.Flags().StringVar(&opts.CustomID, "custom-id", "", "Set a custom customer ID to more easily tie this customer record to your external data systems") + cmd.Flags().StringArrayVar(&opts.ChannelNames, "channel", []string{}, "Release channel to which the customer should be assigned (can be specified multiple times)") + cmd.Flags().StringVar(&opts.DefaultChannel, "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(&opts.ExpiryDuration, "expires-in", 0, "If set, an expiration date will be set on the license. Supports Go durations like '72h' or '3600m'") + cmd.Flags().BoolVar(&opts.EnsureChannel, "ensure-channel", false, "If set, channel will be created if it does not exist.") + cmd.Flags().BoolVar(&opts.IsAirgapEnabled, "airgap", false, "If set, the license will allow airgap installs.") + cmd.Flags().BoolVar(&opts.IsGitopsSupported, "gitops", false, "If set, the license will allow the GitOps usage.") + cmd.Flags().BoolVar(&opts.IsSnapshotSupported, "snapshot", false, "If set, the license will allow Snapshots.") + cmd.Flags().BoolVar(&opts.IsKotsInstallEnabled, "kots-install", true, "If set, the license will allow KOTS install. Otherwise license will allow Helm CLI installs only.") + cmd.Flags().BoolVar(&opts.IsEmbeddedClusterEnabled, "embedded-cluster-download", false, "If set, the license will allow embedded cluster downloads.") + cmd.Flags().BoolVar(&opts.IsGeoaxisSupported, "geo-axis", false, "If set, the license will allow Geo Axis usage.") + cmd.Flags().BoolVar(&opts.IsHelmVMDownloadEnabled, "helmvm-cluster-download", false, "If set, the license will allow helmvm cluster downloads.") + cmd.Flags().BoolVar(&opts.IsIdentityServiceSupported, "identity-service", false, "If set, the license will allow Identity Service usage.") + cmd.Flags().BoolVar(&opts.IsInstallerSupportEnabled, "installer-support", false, "If set, the license will allow installer support.") + cmd.Flags().BoolVar(&opts.IsSupportBundleUploadEnabled, "support-bundle-upload", false, "If set, the license will allow uploading support bundles.") + cmd.Flags().StringVar(&opts.Email, "email", "", "Email address of the customer that is to be created.") + cmd.Flags().StringVar(&opts.CustomerType, "type", "dev", "The license type to create. One of: dev|trial|paid|community|test (default: dev)") + cmd.Flags().StringVar(&outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)") cmd.MarkFlagRequired("channel") return cmd } -func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) { +func (r *runners) createCustomer(cmd *cobra.Command, opts createCustomerOpts, outputFormat string) (err error) { defer func() { printIfError(cmd, err) }() - // all of the following validation occurs in the API also, but - // we want to fail fast if the user has provided invalid input - if err := validateCustomerType(r.args.customerCreateType); err != nil { + // Validation + if err := validateCustomerType(opts.CustomerType); err != nil { return errors.Wrap(err, "validate customer type") } - if r.args.customerCreateType == "test" && r.args.customerCreateExpiryDuration > time.Hour*48 { + if opts.CustomerType == "test" && opts.ExpiryDuration > time.Hour*48 { return errors.New("test licenses cannot be created with an expiration date greater than 48 hours") } - if r.args.customerCreateType == "paid" { - r.args.customerCreateType = "prod" + if opts.CustomerType == "paid" { + opts.CustomerType = "prod" } channels := []kotsclient.CustomerChannel{} foundDefaultChannel := false - for _, requestedChannel := range r.args.customerCreateChannel { + for _, requestedChannel := range opts.ChannelNames { getOrCreateChannelOptions := client.GetOrCreateChannelOptions{ AppID: r.appID, AppType: r.appType, NameOrID: requestedChannel, Description: "", - CreateIfAbsent: r.args.customerCreateEnsureChannel, + CreateIfAbsent: opts.EnsureChannel, } channel, err := r.api.GetOrCreateChannelByName(getOrCreateChannelOptions) @@ -85,7 +135,7 @@ func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) { ID: channel.ID, } - if r.args.customerCreateDefaultChannel == requestedChannel { + if opts.DefaultChannel == requestedChannel { customerChannel.IsDefault = true foundDefaultChannel = true } @@ -97,7 +147,7 @@ func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) { return errors.New("no channels found") } - if r.args.customerUpdateDefaultChannel != "" && !foundDefaultChannel { + if opts.DefaultChannel != "" && !foundDefaultChannel { return errors.New("default channel not found in specified channels") } @@ -110,32 +160,32 @@ func (r *runners) createCustomer(cmd *cobra.Command, _ []string) (err error) { channels[0] = firstChannel } - opts := kotsclient.CreateCustomerOpts{ - Name: r.args.customerCreateName, - CustomID: r.args.customerCreateCustomID, + createOpts := kotsclient.CreateCustomerOpts{ + Name: opts.Name, + CustomID: opts.CustomID, Channels: channels, AppID: r.appID, - ExpiresAtDuration: r.args.customerCreateExpiryDuration, - IsAirgapEnabled: r.args.customerCreateIsAirgapEnabled, - IsGitopsSupported: r.args.customerCreateIsGitopsSupported, - IsSnapshotSupported: r.args.customerCreateIsSnapshotSupported, - IsKotsInstallEnabled: r.args.customerCreateIsKotsInstallEnabled, - IsEmbeddedClusterDownloadEnabled: r.args.customerCreateIsEmbeddedClusterDownloadEnabled, - IsGeoaxisSupported: r.args.customerCreateIsGeoaxisSupported, - IsHelmVMDownloadEnabled: r.args.customerCreateIsHelmVMDownloadEnabled, - IsIdentityServiceSupported: r.args.customerCreateIsIdentityServiceSupported, - IsInstallerSupportEnabled: r.args.customerCreateIsInstallerSupportEnabled, - IsSupportBundleUploadEnabled: r.args.customerCreateIsSupportBundleUploadEnabled, - LicenseType: r.args.customerCreateType, - Email: r.args.customerCreateEmail, + ExpiresAtDuration: opts.ExpiryDuration, + IsAirgapEnabled: opts.IsAirgapEnabled, + IsGitopsSupported: opts.IsGitopsSupported, + IsSnapshotSupported: opts.IsSnapshotSupported, + IsKotsInstallEnabled: opts.IsKotsInstallEnabled, + IsEmbeddedClusterDownloadEnabled: opts.IsEmbeddedClusterEnabled, + IsGeoaxisSupported: opts.IsGeoaxisSupported, + IsHelmVMDownloadEnabled: opts.IsHelmVMDownloadEnabled, + IsIdentityServiceSupported: opts.IsIdentityServiceSupported, + IsInstallerSupportEnabled: opts.IsInstallerSupportEnabled, + IsSupportBundleUploadEnabled: opts.IsSupportBundleUploadEnabled, + LicenseType: opts.CustomerType, + Email: opts.Email, } - customer, err := r.api.CreateCustomer(r.appType, opts) + customer, err := r.api.CreateCustomer(r.appType, createOpts) if err != nil { return errors.Wrap(err, "create customer") } - err = print.Customer(r.outputFormat, r.w, customer) + err = print.Customer(outputFormat, r.w, customer) if err != nil { return errors.Wrap(err, "print customer") } diff --git a/cli/cmd/customer_download_license.go b/cli/cmd/customer_download_license.go index 4adaaa110..38a9d23c6 100644 --- a/cli/cmd/customer_download_license.go +++ b/cli/cmd/customer_download_license.go @@ -9,43 +9,66 @@ import ( ) func (r *runners) InitCustomersDownloadLicenseCommand(parent *cobra.Command) *cobra.Command { + var ( + customer string + output string + ) + cmd := &cobra.Command{ - Use: "download-license", - Short: "download a customer license", - Long: `download a customer license`, - RunE: r.downloadCustomerLicense, + Use: "download-license [flags]", + Short: "Download a customer's license", + Long: `The download-license command allows you to retrieve and save a customer's license. + +This command fetches the license for a specified customer and either outputs it +to stdout or saves it to a file. The license contains crucial information about +the customer's subscription and usage rights. + +You must specify the customer using either their name or ID with the --customer flag.`, + Example: ` # Download license for a customer by ID and output to stdout + replicated customer download-license --customer cus_abcdef123456 + + # Download license for a customer by name and save to a file + replicated customer download-license --customer "Acme Inc" --output license.yaml + + # Download license for a customer in a specific app (if you have multiple apps) + replicated customer download-license --app myapp --customer "Acme Inc" --output license.yaml`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.downloadCustomerLicense(cmd, customer, output) + }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVar(&r.args.customerLicenseInspectCustomer, "customer", "", "The Customer Name or ID") - cmd.Flags().StringVarP(&r.args.customerLicenseInspectOutput, "output", "o", "-", "Path to output license to. Defaults to stdout") + cmd.Flags().String("customer", "", "The Customer Name or ID") + cmd.Flags().StringP("output", "o", "-", "Path to output license to. Defaults to stdout") + cmd.MarkFlagRequired("customer") return cmd } -func (r *runners) downloadCustomerLicense(_ *cobra.Command, _ []string) error { - if r.args.customerLicenseInspectCustomer == "" { - return errors.Errorf("missing or invalid parameters: customer") +func (r *runners) downloadCustomerLicense(cmd *cobra.Command, customer string, output string) error { + customerNameOrId, err := cmd.Flags().GetString("customer") + if err != nil { + return errors.Wrap(err, "get customer flag") } - if r.args.customerLicenseInspectOutput == "" { - return errors.Errorf("missing or invalid parameters: output") + if customerNameOrId == "" { + return errors.New("missing or invalid parameters: customer") } - customer, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, r.args.customerLicenseInspectCustomer) + c, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, customer) if err != nil { - return errors.Wrapf(err, "find customer %q", r.args.customerLicenseInspectCustomer) + return errors.Wrapf(err, "find customer %q", customerNameOrId) } - license, err := r.api.DownloadLicense(r.appType, r.appID, customer.ID) + license, err := r.api.DownloadLicense(r.appType, r.appID, c.ID) if err != nil { - return errors.Wrapf(err, "download license for customer %q", customer.Name) + return errors.Wrapf(err, "download license for customer %q", c.Name) } defer r.w.Flush() - if r.args.customerLicenseInspectOutput == "-" { + if output == "-" { _, err = fmt.Fprintln(r.w, string(license)) return err } - return ioutil.WriteFile(r.args.customerLicenseInspectOutput, license, 0644) + return ioutil.WriteFile(output, license, 0644) } diff --git a/cli/cmd/customer_inspect.go b/cli/cmd/customer_inspect.go index 7a2ebf9ea..c93480506 100644 --- a/cli/cmd/customer_inspect.go +++ b/cli/cmd/customer_inspect.go @@ -12,41 +12,66 @@ import ( ) func (r *runners) InitCustomersInspectCommand(parent *cobra.Command) *cobra.Command { + var ( + customer string + outputFormat string + ) cmd := &cobra.Command{ - Use: "inspect", - Short: "Show full details for a customer", - Long: `Show full details for a customer`, - RunE: r.inspectCustomer, + Use: "inspect [flags]", + Short: "Show detailed information about a specific customer", + Long: `The inspect command provides comprehensive details about a customer. + + This command retrieves and displays full information about a specified customer, + including their assigned channels, registry information, and other relevant attributes. + It's useful for getting an in-depth view of a customer's configuration and status. + + You must specify the customer using either their name or ID with the --customer flag.`, + Example: ` # Inspect a customer by ID + replicated customer inspect --customer cus_abcdef123456 + + # Inspect a customer by name + replicated customer inspect --customer "Acme Inc" + + # Inspect a customer and output in JSON format + replicated customer inspect --customer cus_abcdef123456 --output json + + # Inspect a customer for a specific app (if you have multiple apps) + replicated customer inspect --app myapp --customer "Acme Inc"`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.inspectCustomer(cmd, customer, outputFormat) + }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVar(&r.args.customerInspectCustomer, "customer", "", "The Customer Name or ID") - cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)") + cmd.Flags().StringVar(&customer, "customer", "", "The Customer Name or ID") + cmd.Flags().StringVar(&outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)") + + cmd.MarkFlagRequired("customer") return cmd } -func (r *runners) inspectCustomer(_ *cobra.Command, _ []string) error { - if r.args.customerInspectCustomer == "" { +func (r *runners) inspectCustomer(cmd *cobra.Command, customer string, outputFormat string) error { + if customer == "" { return errors.Errorf("missing or invalid parameters: customer") } - customer, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, r.args.customerInspectCustomer) + c, err := r.api.GetCustomerByNameOrId(r.appType, r.appID, customer) if err != nil { - return errors.Wrapf(err, "get customer %q", r.args.customerInspectCustomer) + return errors.Wrapf(err, "get customer %q", customer) } - ch, err := r.customerChannel(customer) + ch, err := r.customerChannel(c) if err != nil { return errors.Wrap(err, "get customer channel") } - regHost, err := r.registryHostname(customer, ch) + regHost, err := r.registryHostname(c, ch) if err != nil { - return errors.Wrapf(err, "get registry hostname for customer %q", r.args.customerInspectCustomer) + return errors.Wrapf(err, "get registry hostname for customer %q", customer) } - if err = print.CustomerAttrs(r.outputFormat, r.w, r.appType, r.appSlug, ch, regHost, customer); err != nil { + if err = print.CustomerAttrs(r.outputFormat, r.w, r.appType, r.appSlug, ch, regHost, c); err != nil { return errors.Wrap(err, "print customer attrs") } diff --git a/cli/cmd/customer_ls.go b/cli/cmd/customer_ls.go index 966be116c..1d2d26f9d 100644 --- a/cli/cmd/customer_ls.go +++ b/cli/cmd/customer_ls.go @@ -7,43 +7,58 @@ import ( ) func (r *runners) InitCustomersLSCommand(parent *cobra.Command) *cobra.Command { - customersCmd := &cobra.Command{ + var ( + appVersion string + includeTest bool + outputFormat string + ) + + customersLsCmd := &cobra.Command{ Use: "ls", - Short: "list customers", - Long: `list customers`, - RunE: r.listCustomers, + Short: "List customers for the current application", + Long: `List customers associated with the current application. + +This command displays information about customers linked to your application. +By default, it shows all non-test customers. You can use flags to: +- Filter customers by a specific app version +- Include test customers in the results +- Change the output format (table or JSON) + +The command requires an app to be set using the --app flag.`, + Example: ` # List all customers for the current application + replicated customer ls --app myapp + # Output results in JSON format + replicated customer ls --app myapp --output json + + # Combine multiple flags + replicated customer ls --app myapp --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.listCustomers(appVersion, includeTest, outputFormat) + }, } - // Example to list customers by app version and app - // replicated customer ls --app --app-version - parent.AddCommand(customersCmd) - customersCmd.Flags().StringVar(&r.args.lsAppVersion, "app-version", "", "List customers and their instances by app version") - customersCmd.Flags().BoolVar(&r.args.customerLsIncludeTest, "include-test", false, "Include test customers in the list") - customersCmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)") - - return customersCmd -} -func (r *runners) listCustomers(_ *cobra.Command, _ []string) error { + parent.AddCommand(customersLsCmd) + customersLsCmd.Flags().StringVar(&appVersion, "app-version", "", "Filter customers by a specific app version") + customersLsCmd.Flags().BoolVar(&includeTest, "include-test", false, "Include test customers in the results") + customersLsCmd.Flags().StringVar(&outputFormat, "output", "table", "Output format: json|table (default: table)") + + return customersLsCmd +} - // get appVersion from flags - lsappVersion := r.args.lsAppVersion - // if appVersion is blank, call ListCustomers - if lsappVersion == "" { - customers, err := r.api.ListCustomers(r.appID, r.appType, r.args.customerLsIncludeTest) +func (r *runners) listCustomers(appVersion string, includeTest bool, outputFormat string) error { + if appVersion == "" { + customers, err := r.api.ListCustomers(r.appID, r.appType, includeTest) if err != nil { return errors.Wrap(err, "list customers") } - return print.Customers(r.outputFormat, r.w, customers) + return print.Customers(outputFormat, r.w, customers) } else { - // call ListCustomersByAppAndVersion - customers, err := r.api.ListCustomersByAppAndVersion(r.appID, lsappVersion, r.appType) - // if err and outputFormat is json, customers should be a blank struct and print will return [] - if err != nil && r.outputFormat == "json" { - return print.CustomersWithInstances(r.outputFormat, r.w, customers) - // error and outputFormat is table, print error + customers, err := r.api.ListCustomersByAppAndVersion(r.appID, appVersion, r.appType) + if err != nil && outputFormat == "json" { + return print.CustomersWithInstances(outputFormat, r.w, customers) } else if err != nil { return errors.Wrap(err, "list customers by app and app version") } - return print.CustomersWithInstances(r.outputFormat, r.w, customers) + return print.CustomersWithInstances(outputFormat, r.w, customers) } } diff --git a/cli/cmd/customer_update.go b/cli/cmd/customer_update.go index e77c8a6e6..084408489 100644 --- a/cli/cmd/customer_update.go +++ b/cli/cmd/customer_update.go @@ -12,35 +12,86 @@ import ( "github.com/spf13/cobra" ) +type updateCustomerOpts struct { + CustomerID string + Name string + CustomID string + Channels []string + DefaultChannel string + ExpiryDuration time.Duration + EnsureChannel bool + IsAirgapEnabled bool + IsGitopsSupported bool + IsSnapshotSupported bool + IsKotsInstallEnabled bool + IsEmbeddedClusterEnabled bool + IsGeoaxisSupported bool + IsHelmVMDownloadEnabled bool + IsIdentityServiceSupported bool + IsSupportBundleUploadEnabled bool + Email string + Type string +} + func (r *runners) InitCustomerUpdateCommand(parent *cobra.Command) *cobra.Command { + opts := updateCustomerOpts{} + var outputFormat string + cmd := &cobra.Command{ - Use: "update --customer --name [options]", - Short: "update a customer", - Long: `update a customer`, - RunE: r.updateCustomer, + Use: "update --customer --name [options]", + Short: "Update an existing customer", + Long: `Update an existing customer's information and settings. + + This command allows you to modify various attributes of a customer, including their name, + custom ID, assigned channels, license type, and feature flags. You can update expiration dates, + enable or disable specific features, and change channel assignments. + + The --customer flag is required to specify which customer to update.`, + Example: ` # Update a customer's name + replicated customer update --customer cus_abcdef123456 --name "New Company Name" + + # Change a customer's channel and make it the default + replicated customer update --customer cus_abcdef123456 --channel stable --default-channel stable + + # Enable airgap installations for a customer + replicated customer update --customer cus_abcdef123456 --airgap + + # Update multiple attributes at once + replicated customer update --customer cus_abcdef123456 --name "Updated Corp" --type paid --channel enterprise --airgap --snapshot + + # Set an expiration date for a customer's license + replicated customer update --customer cus_abcdef123456 --expires-in 8760h + + # Update a customer and output the result in JSON format + replicated customer update --customer cus_abcdef123456 --name "JSON Corp" --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.updateCustomer(cmd, opts, outputFormat) + }, SilenceUsage: false, SilenceErrors: true, // this command uses custom error printing } + parent.AddCommand(cmd) - cmd.Flags().StringVar(&r.args.customerUpdateID, "customer", "", "The ID of the customer to update") - 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().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.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsGitopsSupported, "gitops", false, "If set, the license will allow the GitOps usage.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsSnapshotSupported, "snapshot", false, "If set, the license will allow Snapshots.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsKotsInstallEnabled, "kots-install", true, "If set, the license will allow KOTS install. Otherwise license will allow Helm CLI installs only.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsEmbeddedClusterDownloadEnabled, "embedded-cluster-download", false, "If set, the license will allow embedded cluster downloads.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsGeoaxisSupported, "geo-axis", false, "If set, the license will allow Geo Axis usage.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsHelmVMDownloadEnabled, "helmvm-cluster-download", false, "If set, the license will allow helmvm cluster downloads.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsIdentityServiceSupported, "identity-service", false, "If set, the license will allow Identity Service usage.") - cmd.Flags().BoolVar(&r.args.customerUpdateIsSupportBundleUploadEnabled, "support-bundle-upload", false, "If set, the license will allow uploading support bundles.") - 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.Flags().StringVar(&opts.CustomerID, "customer", "", "The ID of the customer to update") + cmd.Flags().StringVar(&opts.Name, "name", "", "Name of the customer") + cmd.Flags().StringVar(&opts.CustomID, "custom-id", "", "Set a custom customer ID to more easily tie this customer record to your external data systems") + cmd.Flags().StringArrayVar(&opts.Channels, "channel", []string{}, "Release channel to which the customer should be assigned (can be specified multiple times)") + cmd.Flags().StringVar(&opts.DefaultChannel, "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(&opts.ExpiryDuration, "expires-in", 0, "If set, an expiration date will be set on the license. Supports Go durations like '72h' or '3600m'") + cmd.Flags().BoolVar(&opts.EnsureChannel, "ensure-channel", false, "If set, channel will be created if it does not exist.") + cmd.Flags().BoolVar(&opts.IsAirgapEnabled, "airgap", false, "If set, the license will allow airgap installs.") + cmd.Flags().BoolVar(&opts.IsGitopsSupported, "gitops", false, "If set, the license will allow the GitOps usage.") + cmd.Flags().BoolVar(&opts.IsSnapshotSupported, "snapshot", false, "If set, the license will allow Snapshots.") + cmd.Flags().BoolVar(&opts.IsKotsInstallEnabled, "kots-install", true, "If set, the license will allow KOTS install. Otherwise license will allow Helm CLI installs only.") + cmd.Flags().BoolVar(&opts.IsEmbeddedClusterEnabled, "embedded-cluster-download", false, "If set, the license will allow embedded cluster downloads.") + cmd.Flags().BoolVar(&opts.IsGeoaxisSupported, "geo-axis", false, "If set, the license will allow Geo Axis usage.") + cmd.Flags().BoolVar(&opts.IsHelmVMDownloadEnabled, "helmvm-cluster-download", false, "If set, the license will allow helmvm cluster downloads.") + cmd.Flags().BoolVar(&opts.IsIdentityServiceSupported, "identity-service", false, "If set, the license will allow Identity Service usage.") + cmd.Flags().BoolVar(&opts.IsSupportBundleUploadEnabled, "support-bundle-upload", false, "If set, the license will allow uploading support bundles.") + cmd.Flags().StringVar(&opts.Email, "email", "", "Email address of the customer that is to be updated.") + cmd.Flags().StringVar(&opts.Type, "type", "dev", "The license type to update. One of: dev|trial|paid|community|test (default: dev)") + cmd.Flags().StringVar(&outputFormat, "output", "table", "The output format to use. One of: json|table (default: table)") cmd.MarkFlagRequired("customer") cmd.MarkFlagRequired("channel") @@ -49,36 +100,36 @@ func (r *runners) InitCustomerUpdateCommand(parent *cobra.Command) *cobra.Comman return cmd } -func (r *runners) updateCustomer(cmd *cobra.Command, _ []string) (err error) { +func (r *runners) updateCustomer(cmd *cobra.Command, opts updateCustomerOpts, outputFormat string) (err error) { defer func() { printIfError(cmd, err) }() - if r.args.customerUpdateID == "" { + if opts.CustomerID == "" { return errors.Errorf("missing or invalid parameters: customer") } // all of the following validation occurs in the API also, but // we want to fail fast if the user has provided invalid input - if err := validateCustomerType(r.args.customerUpdateType); err != nil { + if err := validateCustomerType(opts.Type); err != nil { return errors.Wrap(err, "validate customer type") } - if r.args.customerUpdateType == "test" && r.args.customerUpdateExpiryDuration > time.Hour*48 { + if opts.Type == "test" && opts.ExpiryDuration > time.Hour*48 { return errors.New("test licenses cannot be updated with an expiration date greater than 48 hours") } - if r.args.customerUpdateType == "paid" { - r.args.customerUpdateType = "prod" + if opts.Type == "paid" { + opts.Type = "prod" } channels := []kotsclient.CustomerChannel{} foundDefaultChannel := false - for _, requestedChannel := range r.args.customerUpdateChannel { + for _, requestedChannel := range opts.Channels { getOrCreateChannelOptions := client.GetOrCreateChannelOptions{ AppID: r.appID, AppType: r.appType, NameOrID: requestedChannel, Description: "", - CreateIfAbsent: r.args.customerCreateEnsureChannel, + CreateIfAbsent: opts.EnsureChannel, } channel, err := r.api.GetOrCreateChannelByName(getOrCreateChannelOptions) @@ -90,7 +141,7 @@ func (r *runners) updateCustomer(cmd *cobra.Command, _ []string) (err error) { ID: channel.ID, } - if r.args.customerUpdateDefaultChannel == requestedChannel { + if opts.DefaultChannel == requestedChannel { customerChannel.IsDefault = true foundDefaultChannel = true } @@ -102,7 +153,7 @@ func (r *runners) updateCustomer(cmd *cobra.Command, _ []string) (err error) { return errors.New("no channels found") } - if r.args.customerUpdateDefaultChannel != "" && !foundDefaultChannel { + if opts.DefaultChannel != "" && !foundDefaultChannel { return errors.New("default channel not found in specified channels") } @@ -115,31 +166,31 @@ func (r *runners) updateCustomer(cmd *cobra.Command, _ []string) (err error) { channels[0] = firstChannel } - opts := kotsclient.UpdateCustomerOpts{ - Name: r.args.customerUpdateName, - CustomID: r.args.customerUpdateCustomID, + updateOpts := kotsclient.UpdateCustomerOpts{ + Name: opts.Name, + CustomID: opts.CustomID, Channels: channels, AppID: r.appID, - ExpiresAtDuration: r.args.customerUpdateExpiryDuration, - IsAirgapEnabled: r.args.customerUpdateIsAirgapEnabled, - IsGitopsSupported: r.args.customerUpdateIsGitopsSupported, - IsSnapshotSupported: r.args.customerUpdateIsSnapshotSupported, - IsKotsInstallEnabled: r.args.customerUpdateIsKotsInstallEnabled, - IsEmbeddedClusterDownloadEnabled: r.args.customerUpdateIsEmbeddedClusterDownloadEnabled, - IsGeoaxisSupported: r.args.customerUpdateIsGeoaxisSupported, - IsHelmVMDownloadEnabled: r.args.customerUpdateIsHelmVMDownloadEnabled, - IsIdentityServiceSupported: r.args.customerUpdateIsIdentityServiceSupported, - IsSupportBundleUploadEnabled: r.args.customerUpdateIsSupportBundleUploadEnabled, - LicenseType: r.args.customerUpdateType, - Email: r.args.customerUpdateEmail, + ExpiresAtDuration: opts.ExpiryDuration, + IsAirgapEnabled: opts.IsAirgapEnabled, + IsGitopsSupported: opts.IsGitopsSupported, + IsSnapshotSupported: opts.IsSnapshotSupported, + IsKotsInstallEnabled: opts.IsKotsInstallEnabled, + IsEmbeddedClusterDownloadEnabled: opts.IsEmbeddedClusterEnabled, + IsGeoaxisSupported: opts.IsGeoaxisSupported, + IsHelmVMDownloadEnabled: opts.IsHelmVMDownloadEnabled, + IsIdentityServiceSupported: opts.IsIdentityServiceSupported, + IsSupportBundleUploadEnabled: opts.IsSupportBundleUploadEnabled, + LicenseType: opts.Type, + Email: opts.Email, } - customer, err := r.api.UpdateCustomer(r.appType, r.args.customerUpdateID, opts) + customer, err := r.api.UpdateCustomer(r.appType, opts.CustomerID, updateOpts) if err != nil { return errors.Wrap(err, "update customer") } - err = print.Customer(r.outputFormat, r.w, customer) + err = print.Customer(outputFormat, r.w, customer) if err != nil { return errors.Wrap(err, "print customer") } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index a544a62bf..17e56f1c5 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -44,8 +44,8 @@ func init() { func GetRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "replicated", - Short: "Manage channels, releases and collectors", - Long: `The replicated CLI allows vendors to manage their apps, channels, releases and collectors.`, + Short: "Manage your Commercial Software Distribution Lifecycle using Replicated", + Long: `The 'replicated' CLI allows Replicated customers (vendors) to manage their Commercial Software Distribution Lifecycle (CSDL) using the Replicated API.`, } rootCmd.PersistentFlags().StringVar(&appSlugOrID, "app", "", "The app slug or app id to use in all calls") rootCmd.PersistentFlags().StringVar(&apiToken, "token", "", "The API token to use to access your app in the Vendor API") diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index eeafab583..8b2bfecf6 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -71,46 +71,7 @@ type runnerArgs struct { updateReleaseYamlFile string updateReleaseChart string - customerLsIncludeTest bool - - customerArchiveNameOrId string - customerCreateName string - customerCreateCustomID string - customerCreateChannel []string - customerCreateDefaultChannel string - customerCreateEnsureChannel bool - customerCreateExpiryDuration time.Duration - customerCreateIsAirgapEnabled bool - customerCreateIsGitopsSupported bool - customerCreateIsSnapshotSupported bool - customerCreateIsKotsInstallEnabled bool - customerCreateIsEmbeddedClusterDownloadEnabled bool - customerCreateIsGeoaxisSupported bool - customerCreateIsHelmVMDownloadEnabled bool - customerCreateIsIdentityServiceSupported bool - customerCreateIsInstallerSupportEnabled bool - customerCreateIsSupportBundleUploadEnabled bool - customerCreateEmail string - customerCreateType string - - customerUpdateID string - customerUpdateName string - customerUpdateCustomID string - customerUpdateChannel []string - customerUpdateDefaultChannel string - customerUpdateEnsureChannel bool - customerUpdateExpiryDuration time.Duration - customerUpdateIsAirgapEnabled bool - customerUpdateIsGitopsSupported bool - customerUpdateIsSnapshotSupported bool - customerUpdateIsKotsInstallEnabled bool - customerUpdateIsEmbeddedClusterDownloadEnabled bool - customerUpdateIsGeoaxisSupported bool - customerUpdateIsHelmVMDownloadEnabled bool - customerUpdateIsIdentityServiceSupported bool - customerUpdateIsSupportBundleUploadEnabled bool - customerUpdateEmail string - customerUpdateType string + customerArchiveNameOrId string instanceInspectCustomer string instanceInspectInstance string @@ -243,8 +204,6 @@ type runnerArgs struct { apiPutBody string apiPatchBody string - customerInspectCustomer string - compatibilityKubernetesDistribution string compatibilityKubernetesVersion string compatibilitySuccess bool