diff --git a/docs/commands/rhoas_kafka_create.md b/docs/commands/rhoas_kafka_create.md index 4ee666084..d4110dc9f 100644 --- a/docs/commands/rhoas_kafka_create.md +++ b/docs/commands/rhoas_kafka_create.md @@ -30,6 +30,7 @@ $ rhoas kafka create -o yaml ### Options ``` + --billing-model string Billing model to be used --dry-run Validate all user provided arguments without creating the Kafka instance --marketplace string Name of the marketplace where the instance is purchased on --marketplace-account-id string Cloud Account ID for the marketplace diff --git a/go.mod b/go.mod index fbece4083..036f47d8b 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/redhat-developer/app-services-sdk-go/accountmgmt v0.2.0 github.com/redhat-developer/app-services-sdk-go/connectormgmt v0.7.0 github.com/redhat-developer/app-services-sdk-go/kafkainstance v0.6.0 - github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.0 + github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.1 github.com/redhat-developer/app-services-sdk-go/registryinstance v0.3.1 github.com/redhat-developer/app-services-sdk-go/registrymgmt v0.6.1 github.com/redhat-developer/service-binding-operator v0.9.0 diff --git a/go.sum b/go.sum index 10f5d2b34..d7d9a3acc 100644 --- a/go.sum +++ b/go.sum @@ -640,8 +640,8 @@ github.com/redhat-developer/app-services-sdk-go/connectormgmt v0.7.0 h1:GcbNg/Ad github.com/redhat-developer/app-services-sdk-go/connectormgmt v0.7.0/go.mod h1:0WB4LlMmesjBlGKvnMXQ7twPxeSr27f5a+w4QnMoSdQ= github.com/redhat-developer/app-services-sdk-go/kafkainstance v0.6.0 h1:ExEHQaihnPNxN2nKXB0q5nrmSv4p8b3Idzt7TChxv+Q= github.com/redhat-developer/app-services-sdk-go/kafkainstance v0.6.0/go.mod h1:hMpejngP3BFnifCDH1gKRG9cU9Q4lr0WiQaW7A1LYo4= -github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.0 h1:63UhOYB8TozKdnkkws2pXc0D1lEB+K3qX63/OxkjDas= -github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.0/go.mod h1:m+m7d6xkC9WbSxemslyhjv0jVhquWLysRfdh+RQ5hH0= +github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.1 h1:Gcyn2kLlslsVT6T8qoiCJpJFPrnD2i2KIFeKQJrXkTY= +github.com/redhat-developer/app-services-sdk-go/kafkamgmt v0.12.1/go.mod h1:RoPo3tyHjv8apStFNVjChwWYdlWhg6hMzi1IrH3yQX8= github.com/redhat-developer/app-services-sdk-go/registryinstance v0.3.1 h1:xRq5XJzRDs/Z7e/9SDt6zbNRIyesC4LTqN9ajHKwjHo= github.com/redhat-developer/app-services-sdk-go/registryinstance v0.3.1/go.mod h1:Z/gr/snlpsqYg4vftmcx97vCR3qMQJhALGelDHx4pMA= github.com/redhat-developer/app-services-sdk-go/registrymgmt v0.6.1 h1:3sUmQ3nAawsYWg7ZCO2Q8HF2J7MW6YA38h/YFL3ao6o= @@ -933,6 +933,7 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/cmd/kafka/create/api_validators.go b/pkg/cmd/kafka/create/api_validators.go index 882930a85..ff6d18d5c 100644 --- a/pkg/cmd/kafka/create/api_validators.go +++ b/pkg/cmd/kafka/create/api_validators.go @@ -3,6 +3,7 @@ package create import ( "strings" + "github.com/redhat-developer/app-services-cli/pkg/core/cmdutil/flagutil" "github.com/redhat-developer/app-services-cli/pkg/core/localize" "github.com/redhat-developer/app-services-cli/pkg/shared/accountmgmtutil" "github.com/redhat-developer/app-services-cli/pkg/shared/connection" @@ -24,6 +25,8 @@ type ValidatorInput struct { conn connection.Connection } +var validBillingModels []string = []string{accountmgmtutil.QuotaMarketplaceType, accountmgmtutil.QuotaStandardType} + func (input *ValidatorInput) ValidateProviderAndRegion() error { f := input.f f.Logger.Debug("Validating provider and region") @@ -119,3 +122,19 @@ func (input *ValidatorInput) ValidateSize() error { return nil } + +// ValidateBillingModel validates if user provided a supported billing model +func ValidateBillingModel(billingModel string) error { + + if billingModel == "" { + return nil + } + + isValid := flagutil.IsValidInput(billingModel, validBillingModels...) + + if isValid { + return nil + } + + return flagutil.InvalidValueError("billing-model", billingModel, validBillingModels...) +} diff --git a/pkg/cmd/kafka/create/completions.go b/pkg/cmd/kafka/create/completions.go index 284c1833b..dbf1b7ba8 100644 --- a/pkg/cmd/kafka/create/completions.go +++ b/pkg/cmd/kafka/create/completions.go @@ -2,7 +2,6 @@ package create import ( "github.com/redhat-developer/app-services-cli/pkg/shared/accountmgmtutil" - "github.com/redhat-developer/app-services-cli/pkg/shared/connection" "github.com/redhat-developer/app-services-cli/pkg/shared/factory" "github.com/redhat-developer/app-services-cli/pkg/shared/remote" "github.com/spf13/cobra" @@ -28,7 +27,7 @@ func GetCloudProviderRegionCompletionValues(f *factory.Factory, providerID strin } // GetKafkaSizeCompletionValues returns a list of valid kafka sizes for the specified region and ams instance types -func GetKafkaSizeCompletionValues(f *factory.Factory, providerID string, regionId string) (validRegions []string, directive cobra.ShellCompDirective) { +func GetKafkaSizeCompletionValues(f *factory.Factory, providerID string, regionId string) (validSizes []string, directive cobra.ShellCompDirective) { directive = cobra.ShellCompDirectiveNoSpace // We need both values to provide a valid list of sizes @@ -41,33 +40,76 @@ func GetKafkaSizeCompletionValues(f *factory.Factory, providerID string, regionI return nil, directive } - conn, err := f.Connection(connection.DefaultConfigSkipMasAuth) + orgQuota, err := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) if err != nil { return nil, directive } - userInstanceType, _ := accountmgmtutil.GetUserSupportedInstanceType(f.Context, &constants.Kafka.Ams, conn) + userInstanceType, _ := accountmgmtutil.SelectQuotaForUser(f, orgQuota, accountmgmtutil.MarketplaceInfo{}) // Not including quota in this request as it takes very long time to list quota for all regions in suggestion mode - validRegions, _ = FetchValidKafkaSizesLabels(f, providerID, regionId, *userInstanceType) + validSizes, _ = FetchValidKafkaSizesLabels(f, providerID, regionId, *userInstanceType) - return validRegions, cobra.ShellCompDirectiveNoSpace + return validSizes, cobra.ShellCompDirectiveNoSpace } -// GetMarketplaceAcctIdCompletionValues returns a list of valid marketplace account IDs for the organization -func GetMarketplaceAcctIdCompletionValues(f *factory.Factory) (validMarketplaceAcctIDs []string, directive cobra.ShellCompDirective) { +func GetMarketplaceCompletionValues(f *factory.Factory) (validSizes []string, directive cobra.ShellCompDirective) { + directive = cobra.ShellCompDirectiveNoSpace - validMarketplaceAcctIDs, _ = accountmgmtutil.GetValidMarketplaceAcctIDs(f.Context, f.Connection, "") + err, constants := remote.GetRemoteServiceConstants(f.Context, f.Logger) + if err != nil { + return nil, directive + } + + orgQuota, err := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) + if err != nil { + return nil, directive + } + + validMarketPlaces := FetchValidMarketplaces(orgQuota.MarketplaceQuotas) + + return validMarketPlaces, cobra.ShellCompDirectiveNoSpace +} + +func GetMarketplaceAccountCompletionValues(f *factory.Factory, marketplace string) (validMarketplaceAcctIDs []string, directive cobra.ShellCompDirective) { + + directive = cobra.ShellCompDirectiveNoSpace + + if marketplace == "" { + return validMarketplaceAcctIDs, directive + } + + err, constants := remote.GetRemoteServiceConstants(f.Context, f.Logger) + if err != nil { + return nil, directive + } + + orgQuota, err := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) + if err != nil { + return nil, directive + } - return validMarketplaceAcctIDs, directive + validMarketplaceAcctIDs = FetchValidMarketplaceAccounts(orgQuota.MarketplaceQuotas, marketplace) + + return validMarketplaceAcctIDs, cobra.ShellCompDirectiveNoSpace } -// GetMarketplaceCompletionValues returns a list of valid marketplaces for the organization -func GetMarketplaceCompletionValues(f *factory.Factory) (validMarketplaces []string, directive cobra.ShellCompDirective) { +func GetBillingModelCompletionValues(f *factory.Factory) (availableBillingModels []string, directive cobra.ShellCompDirective) { + directive = cobra.ShellCompDirectiveNoSpace - validMarketplaces, _ = accountmgmtutil.GetValidMarketplaces(f.Context, f.Connection) + err, constants := remote.GetRemoteServiceConstants(f.Context, f.Logger) + if err != nil { + return nil, directive + } + + orgQuota, err := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) + if err != nil { + return nil, directive + } + + availableBillingModels = FetchSupportedBillingModels(orgQuota) - return validMarketplaces, directive + return availableBillingModels, directive } diff --git a/pkg/cmd/kafka/create/create.go b/pkg/cmd/kafka/create/create.go index 766a01200..e7b605f29 100644 --- a/pkg/cmd/kafka/create/create.go +++ b/pkg/cmd/kafka/create/create.go @@ -1,6 +1,7 @@ package create import ( + "encoding/json" "fmt" "os" "os/signal" @@ -42,6 +43,8 @@ const ( FlagMarketPlaceAcctID = "marketplace-account-id" // FlagMarketPlace is a flag representing marketplace where the instance is purchased on FlagMarketPlace = "marketplace" + // FlagMarketPlace is a flag representing billing model of the instance + FlagBillingModel = "billing-model" ) type options struct { @@ -52,6 +55,7 @@ type options struct { marketplaceAcctId string marketplace string + billingModel string outputFormat string autoUse bool @@ -93,14 +97,23 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command { } } - if opts.bypassChecks && (opts.marketplace != "" || opts.marketplaceAcctId != "") { + if opts.bypassChecks && (opts.marketplace != "" || opts.marketplaceAcctId != "" || opts.billingModel != "") { return f.Localizer.MustLocalizeError("kafka.create.error.bypassChecks.marketplace") } + if (opts.marketplace != "") != (opts.marketplaceAcctId != "") { + return f.Localizer.MustLocalizeError("kafka.create.error.insufficientMarketplaceInfo") + } + + if opts.billingModel == accountmgmtutil.QuotaStandardType && (opts.marketplaceAcctId != "" || opts.marketplace != "") { + return f.Localizer.MustLocalizeError("kafka.create.error.standard.invalidFlags") + } + if !f.IOStreams.CanPrompt() && opts.name == "" { return f.Localizer.MustLocalizeError("kafka.create.argument.name.error.requiredWhenNonInteractive") } else if opts.name == "" { - if opts.provider != "" || opts.region != "" { + if opts.provider != "" || opts.region != "" || opts.marketplaceAcctId != "" || + opts.marketplace != "" || opts.size != "" || opts.billingModel != "" { return f.Localizer.MustLocalizeError("kafka.create.argument.name.error.requiredWhenNonInteractive") } opts.interactive = true @@ -111,26 +124,6 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command { return flagutil.InvalidValueError("output", opts.outputFormat, validOutputFormats...) } - if !opts.bypassChecks { - validMarketplaces, err := accountmgmtutil.GetValidMarketplaces(f.Context, f.Connection) - if err != nil { - return err - } - - if opts.marketplace != "" && !flagutil.IsValidInput(opts.marketplace, validMarketplaces...) { - return flagutil.InvalidValueError(FlagMarketPlace, opts.marketplace, validMarketplaces...) - } - - validMarketplaceAcctIDs, err := accountmgmtutil.GetValidMarketplaceAcctIDs(f.Context, f.Connection, opts.marketplace) - if err != nil { - return err - } - - if opts.marketplaceAcctId != "" && !flagutil.IsValidInput(opts.marketplaceAcctId, validMarketplaceAcctIDs...) { - return flagutil.InvalidValueError(FlagMarketPlaceAcctID, opts.marketplaceAcctId, validMarketplaceAcctIDs...) - } - } - return runCreate(opts) }, } @@ -147,6 +140,7 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command { flags.BoolVar(&opts.autoUse, "use", true, f.Localizer.MustLocalize("kafka.create.flag.autoUse.description")) flags.BoolVarP(&opts.wait, "wait", "w", false, f.Localizer.MustLocalize("kafka.create.flag.wait.description")) flags.BoolVarP(&opts.dryRun, "dry-run", "", false, f.Localizer.MustLocalize("kafka.create.flag.dryrun.description")) + flags.StringVar(&opts.billingModel, FlagBillingModel, "", f.Localizer.MustLocalize("kafka.create.flag.billingModel.description")) flags.AddBypassTermsCheck(&opts.bypassChecks) _ = cmd.RegisterFlagCompletionFunc(FlagProvider, func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { @@ -161,12 +155,16 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command { return GetKafkaSizeCompletionValues(f, opts.provider, opts.region) }) + _ = cmd.RegisterFlagCompletionFunc(FlagMarketPlace, func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return GetMarketplaceCompletionValues(f) + }) + _ = cmd.RegisterFlagCompletionFunc(FlagMarketPlaceAcctID, func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return GetMarketplaceAcctIdCompletionValues(f) + return GetMarketplaceAccountCompletionValues(f, opts.marketplace) }) - _ = cmd.RegisterFlagCompletionFunc(FlagMarketPlace, func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return GetMarketplaceCompletionValues(f) + _ = cmd.RegisterFlagCompletionFunc(FlagBillingModel, func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return GetBillingModelCompletionValues(f) }) return cmd @@ -195,7 +193,7 @@ func runCreate(opts *options) error { return err } - var userInstanceType *accountmgmtutil.QuotaSpec + var userQuota *accountmgmtutil.QuotaSpec if !opts.bypassChecks { f.Logger.Debug("Checking if terms and conditions have been accepted") // the user must have accepted the terms and conditions from the provider @@ -212,24 +210,45 @@ func runCreate(opts *options) error { return nil } - userInstanceType, err = accountmgmtutil.GetUserSupportedInstanceType(f.Context, &constants.Kafka.Ams, conn) - if err != nil || userInstanceType == nil { - return f.Localizer.MustLocalizeError("kafka.create.error.userInstanceType.notFound") + err = ValidateBillingModel(opts.billingModel) + if err != nil { + return err } } var payload *kafkamgmtclient.KafkaRequestPayload if opts.interactive { f.Logger.Debug() - if userInstanceType == nil { + if opts.bypassChecks { return f.Localizer.MustLocalizeError("kafka.create.error.noInteractiveMode") } - payload, err = promptKafkaPayload(opts, *userInstanceType) + payload, err = promptKafkaPayload(opts, constants) if err != nil { return err } } else { + + orgQuota, newErr := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) + if newErr != nil { + return newErr + } + + marketplaceInfo := accountmgmtutil.MarketplaceInfo{ + BillingModel: opts.billingModel, + Provider: opts.marketplace, + CloudAccountID: opts.marketplaceAcctId, + } + + userQuota, err = accountmgmtutil.SelectQuotaForUser(f, orgQuota, marketplaceInfo) + if err != nil { + return err + } + + userQuotaJSON, _ := json.MarshalIndent(userQuota, "", " ") + f.Logger.Debug("Selected Quota object:") + f.Logger.Debug(string(userQuotaJSON)) + if opts.provider == "" { opts.provider = defaultProvider } @@ -244,11 +263,17 @@ func runCreate(opts *options) error { CloudProvider: &opts.provider, } - if opts.marketplaceAcctId != "" && opts.marketplace != "" { + if userQuota.BillingModel == accountmgmtutil.QuotaMarketplaceType && userQuota.CloudAccounts != nil { + payload.Marketplace = kafkamgmtclient.NullableString{} - payload.Marketplace.Set(&opts.marketplace) + payload.Marketplace.Set((*userQuota.CloudAccounts)[0].CloudProviderId) payload.BillingCloudAccountId = kafkamgmtclient.NullableString{} - payload.BillingCloudAccountId.Set(&opts.marketplaceAcctId) + payload.BillingCloudAccountId.Set((*userQuota.CloudAccounts)[0].CloudAccountId) + } + + if opts.billingModel != "" { + payload.BillingModel = kafkamgmtclient.NullableString{} + payload.BillingModel.Set(&opts.billingModel) } if !opts.bypassChecks { @@ -256,7 +281,7 @@ func runCreate(opts *options) error { provider: opts.provider, region: opts.region, size: opts.size, - userAMSInstanceType: userInstanceType, + userAMSInstanceType: userQuota, f: f, constants: constants, conn: conn, @@ -271,18 +296,20 @@ func runCreate(opts *options) error { return err1 } if opts.size != "" { - sizes, err1 := FetchValidKafkaSizes(opts.f, opts.provider, opts.region, *userInstanceType) + sizes, err1 := FetchValidKafkaSizes(opts.f, opts.provider, opts.region, *userQuota) if err1 != nil { return err1 } printSizeWarningIfNeeded(opts.f, opts.size, sizes) - payload.SetPlan(mapAmsTypeToBackendType(userInstanceType) + "." + opts.size) + payload.SetPlan(mapAmsTypeToBackendType(userQuota) + "." + opts.size) } } } - f.Logger.Debug("Creating kafka instance", payload.Name, payload.GetPlan()) + f.Logger.Debug("Creating kafka instance", payload.Name) + data, _ := json.MarshalIndent(payload, "", " ") + f.Logger.Debug(string(data)) if opts.dryRun { f.Logger.Info(f.Localizer.MustLocalize("kafka.create.log.info.dryRun.success")) @@ -394,18 +421,16 @@ type promptAnswers struct { Size string Region string CloudProvider string + BillingModel string MarketplaceAcctID string Marketplace string } // Show a prompt to allow the user to interactively insert the data for their Kafka // nolint:funlen -func promptKafkaPayload(opts *options, userQuotaType accountmgmtutil.QuotaSpec) (*kafkamgmtclient.KafkaRequestPayload, error) { +func promptKafkaPayload(opts *options, constants *remote.DynamicServiceConstants) (*kafkamgmtclient.KafkaRequestPayload, error) { f := opts.f - accountIDNullable := kafkamgmtclient.NullableString{} - marketplaceProviderNullable := kafkamgmtclient.NullableString{} - validator := &kafkacmdutil.Validator{ Localizer: f.Localizer, Connection: f.Connection, @@ -438,7 +463,78 @@ func promptKafkaPayload(opts *options, userQuotaType accountmgmtutil.QuotaSpec) return nil, err } - regionIDs, err := GetEnabledCloudRegionIDs(opts.f, answers.CloudProvider, &userQuotaType) + orgQuota, err := accountmgmtutil.GetOrgQuotas(f, &constants.Kafka.Ams) + if err != nil { + return nil, err + } + + availableBillingModels := FetchSupportedBillingModels(orgQuota) + + if len(availableBillingModels) > 0 { + if len(availableBillingModels) == 1 { + answers.BillingModel = availableBillingModels[0] + } else { + billingModelPrompt := &survey.Select{ + Message: f.Localizer.MustLocalize("kafka.create.input.billingModel.message"), + Options: availableBillingModels, + } + err = survey.AskOne(billingModelPrompt, &answers.BillingModel) + if err != nil { + return nil, err + } + } + } + + if answers.BillingModel == accountmgmtutil.QuotaMarketplaceType { + validMarketPlaces := FetchValidMarketplaces(orgQuota.MarketplaceQuotas) + if len(validMarketPlaces) == 1 { + answers.Marketplace = validMarketPlaces[0] + } else { + marketplacePrompt := &survey.Select{ + Message: f.Localizer.MustLocalize("kafka.create.input.marketplace.message"), + Options: validMarketPlaces, + } + err = survey.AskOne(marketplacePrompt, &answers.Marketplace) + if err != nil { + return nil, err + } + } + + if len(validMarketPlaces) > 0 { + + validMarketplaceAcctIDs := FetchValidMarketplaceAccounts(orgQuota.MarketplaceQuotas, answers.Marketplace) + + if len(validMarketplaceAcctIDs) == 1 { + answers.MarketplaceAcctID = validMarketplaceAcctIDs[0] + } else { + marketplaceAccountPrompt := &survey.Select{ + Message: f.Localizer.MustLocalize("kafka.create.input.marketplaceAccountID.message"), + Options: validMarketplaceAcctIDs, + } + err = survey.AskOne(marketplaceAccountPrompt, &answers.MarketplaceAcctID) + if err != nil { + return nil, err + } + } + } + } + + marketplaceInfo := accountmgmtutil.MarketplaceInfo{ + BillingModel: answers.BillingModel, + Provider: answers.Marketplace, + CloudAccountID: answers.MarketplaceAcctID, + } + + userQuota, err := accountmgmtutil.SelectQuotaForUser(f, orgQuota, marketplaceInfo) + if err != nil { + return nil, err + } + + userQuotaJSON, _ := json.MarshalIndent(userQuota, "", " ") + f.Logger.Debug("Selected Quota object:") + f.Logger.Debug(string(userQuotaJSON)) + + regionIDs, err := GetEnabledCloudRegionIDs(opts.f, answers.CloudProvider, userQuota) if err != nil { return nil, err } @@ -454,7 +550,7 @@ func promptKafkaPayload(opts *options, userQuotaType accountmgmtutil.QuotaSpec) return nil, err } - sizes, err := FetchValidKafkaSizes(opts.f, answers.CloudProvider, answers.Region, userQuotaType) + sizes, err := FetchValidKafkaSizes(opts.f, answers.CloudProvider, answers.Region, *userQuota) if err != nil { return nil, err } @@ -474,72 +570,36 @@ func promptKafkaPayload(opts *options, userQuotaType accountmgmtutil.QuotaSpec) } } - marketplaces, err := accountmgmtutil.GetValidMarketplaces(f.Context, f.Connection) - if err != nil { - return nil, err - } - - if !opts.bypassChecks && len(marketplaces) > 0 { - if err = promptMarketplaceSelect(f.Localizer, marketplaces, answers); err != nil { - return nil, err - } + accountIDNullable := kafkamgmtclient.NullableString{} + marketplaceProviderNullable := kafkamgmtclient.NullableString{} + billingNullable := kafkamgmtclient.NullableString{} - marketplaceAcctIDs, err := accountmgmtutil.GetValidMarketplaceAcctIDs(f.Context, f.Connection, answers.Marketplace) - if err != nil { - return nil, err - } + if answers.BillingModel != "" { + billingNullable.Set(&answers.BillingModel) + } - if len(marketplaceAcctIDs) > 0 { - if err = promptMarketplaceAcctIDSelect(f.Localizer, marketplaceAcctIDs, answers); err != nil { - return nil, err - } - } + if answers.Marketplace != "" { + marketplaceProviderNullable.Set(&answers.Marketplace) + } + if answers.MarketplaceAcctID != "" { accountIDNullable.Set(&answers.MarketplaceAcctID) - marketplaceProviderNullable.Set(&answers.Marketplace) } payload := &kafkamgmtclient.KafkaRequestPayload{ Name: answers.Name, Region: &answers.Region, CloudProvider: &answers.CloudProvider, + BillingModel: billingNullable, BillingCloudAccountId: accountIDNullable, Marketplace: marketplaceProviderNullable, } printSizeWarningIfNeeded(opts.f, answers.Size, sizes) - payload.SetPlan(mapAmsTypeToBackendType(&userQuotaType) + "." + answers.Size) + payload.SetPlan(mapAmsTypeToBackendType(userQuota) + "." + answers.Size) return payload, nil } -func promptMarketplaceSelect(localizer localize.Localizer, marketplaceAcctIDs []string, answers *promptAnswers) error { - - marketplacePrompt := &survey.Select{ - Message: localizer.MustLocalize("kafka.create.input.marketPlace.message"), - Options: marketplaceAcctIDs, - } - - if err := survey.AskOne(marketplacePrompt, &answers.Marketplace); err != nil { - return err - } - - return nil -} - -func promptMarketplaceAcctIDSelect(localizer localize.Localizer, accountIDs []string, answers *promptAnswers) error { - - accountIDPrompt := &survey.Select{ - Message: localizer.MustLocalize("kafka.create.input.accountID.message", localize.NewEntry("Marketplace", answers.Marketplace)), - Options: accountIDs, - } - - if err := survey.AskOne(accountIDPrompt, &answers.MarketplaceAcctID); err != nil { - return err - } - - return nil -} - func printSizeWarningIfNeeded(f *factory.Factory, selectedSize string, sizes []kafkamgmtclient.SupportedKafkaSize) { for i := range sizes { if sizes[i].GetId() == selectedSize { diff --git a/pkg/cmd/kafka/create/data.go b/pkg/cmd/kafka/create/data.go index eea60feda..a121b6c05 100644 --- a/pkg/cmd/kafka/create/data.go +++ b/pkg/cmd/kafka/create/data.go @@ -28,6 +28,8 @@ func mapAmsTypeToBackendType(amsType *accountmgmtutil.QuotaSpec) CloudProviderId switch amsType.Name { case accountmgmtutil.QuotaStandardType: return StandardType + case accountmgmtutil.QuotaMarketplaceType: + return StandardType case accountmgmtutil.QuotaTrialType: return DeveloperType default: @@ -54,6 +56,57 @@ func FetchValidKafkaSizesLabels(f *factory.Factory, } +func FetchSupportedBillingModels(userQuotas *accountmgmtutil.OrgQuotas) []string { + + billingModels := []string{} + + if len(userQuotas.StandardQuotas) > 0 { + billingModels = append(billingModels, accountmgmtutil.QuotaStandardType) + } + + if len(userQuotas.MarketplaceQuotas) > 0 { + billingModels = append(billingModels, accountmgmtutil.QuotaMarketplaceType) + } + + return billingModels +} + +func FetchValidMarketplaces(amsTypes []accountmgmtutil.QuotaSpec) []string { + + validMarketplaces := []string{} + + for _, quota := range amsTypes { + if quota.CloudAccounts != nil { + for _, cloudAccount := range *quota.CloudAccounts { + validMarketplaces = append(validMarketplaces, *cloudAccount.CloudProviderId) + } + } + } + + return unique(validMarketplaces) +} + +func FetchValidMarketplaceAccounts(amsTypes []accountmgmtutil.QuotaSpec, marketplace string) []string { + + validAccounts := []string{} + + for _, quota := range amsTypes { + if quota.CloudAccounts != nil { + for _, cloudAccount := range *quota.CloudAccounts { + if marketplace != "" { + if cloudAccount.GetCloudProviderId() == marketplace { + validAccounts = append(validAccounts, cloudAccount.GetCloudAccountId()) + } + } else { + validAccounts = append(validAccounts, cloudAccount.GetCloudAccountId()) + } + } + } + } + + return unique(validAccounts) +} + // return list of the valid instance sizes for the specified region and ams instance types func FetchValidKafkaSizes(f *factory.Factory, providerID string, regionId string, amsType accountmgmtutil.QuotaSpec) ([]kafkamgmtclient.SupportedKafkaSize, error) { @@ -168,3 +221,15 @@ func IsRegionAllowed(region *kafkamgmtclient.CloudRegion, userInstanceType *acco } return false } + +func unique(s []string) []string { + inResult := make(map[string]bool) + var result []string + for _, str := range s { + if _, ok := inResult[str]; !ok { + inResult[str] = true + result = append(result, str) + } + } + return result +} diff --git a/pkg/core/localize/locales/en/cmd/kafka.en.toml b/pkg/core/localize/locales/en/cmd/kafka.en.toml index b835ae182..72d9a4dd1 100644 --- a/pkg/core/localize/locales/en/cmd/kafka.en.toml +++ b/pkg/core/localize/locales/en/cmd/kafka.en.toml @@ -289,6 +289,9 @@ one = 'Validate all user provided arguments without creating the Kafka instance' [kafka.create.flag.marketplaceId.description] one = 'Cloud Account ID for the marketplace' +[kafka.create.flag.billingModel.description] +one = 'Billing model to be used' + [kafka.create.flag.marketplaceType.description] one = 'Name of the marketplace where the instance is purchased on' @@ -325,6 +328,15 @@ one = 'Unique name of the Kafka instance' description = 'Input title for Cloud Provider' one = 'Cloud Provider:' +[kafka.create.input.billingModel.message] +one = 'Billing Model:' + +[kafka.create.input.marketplace.message] +one = 'Marketplace:' + +[kafka.create.input.marketplaceAccountID.message] +one = 'Marketplace Account ID:' + [kafka.create.input.cloudRegion.message] description = 'Input title for Cloud Region' one = "Cloud Region:" @@ -352,14 +364,17 @@ one = "Geographical region where the Kafka instance will be deployed" one = 'name is required. Run "rhoas kafka create --name my-kafka"' [kafka.create.error.bypassChecks.marketplace] -one = '"--marketplace" and "--marketplace-account-id" flags are not supported with "--bypass-checks" flag' +one = '"--billing-model", "--marketplace", "--marketplace-account-id" flags are not supported with "--bypass-checks" flag' + +[kafka.create.error.insufficientMarketplaceInfo] +one = '"--marketplace" and "--marketplace-account-id" flags should be supplied together' + +[kafka.create.error.standard.invalidFlags] +one = 'billing model cannot be standard if "--marketplace-account-id" or "--marketplace" are set' [kafka.create.error.conflictError] one = 'Kafka instance "{{.Name}}" already exists' -[kafka.create.error.userInstanceType.notFound] -one = 'Cannot fetch user allowed instance type' - [kafka.create.error.noInteractiveMode] one = 'Interactive mode is not supported when using --bypass-checks flag' @@ -404,6 +419,27 @@ one = ''' provided billing account id and provider are invalid {{.Billing}} ''' +[kafka.create.quota.error.onlyTrialAvailable] +one = "only trial quotas are available, don't specify billing model details" + +[kafka.create.quota.error.noMarketplace] +one = "no marketplace quotas are available" + +[kafka.create.quota.error.noStandard] +one = "no standard quotas are available" + +[kafka.create.quota.error.noBillingModel] +one = 'multiple quota types found, please specify "--billing-model"' + +[kafka.create.quota.error.multipleMarketplaceQuotas] +one = 'multiple marketplace quota types found, please specify "--marketplace" and "--marketplace-account-id"' + +[kafka.create.quota.error.cloudAccountNotFound] +one = 'no marketplace quota found with the specified marketplace provider and account id' + +[kafka.create.quota.error.multipleCloudAccounts] +one = 'multiple cloud accounts found, please specify "--marketplace" and "--marketplace-account-id"' + [kafka.delete.cmd.shortDescription] description = "Short description for command" one = "Delete a Kafka instance" diff --git a/pkg/shared/accountmgmtutil/ams.go b/pkg/shared/accountmgmtutil/ams.go index b267c1f87..22248c557 100644 --- a/pkg/shared/accountmgmtutil/ams.go +++ b/pkg/shared/accountmgmtutil/ams.go @@ -36,20 +36,22 @@ func CheckTermsAccepted(ctx context.Context, spec *remote.AmsConfig, conn connec // QuotaSpec - contains quota name and remaining quota count type QuotaSpec struct { - Name string - Quota int - BillingModel string + Name string + Quota int + BillingModel string + CloudAccounts *[]amsclient.CloudAccount } -func GetUserSupportedInstanceType(ctx context.Context, spec *remote.AmsConfig, conn connection.Connection) (quota *QuotaSpec, err error) { - userInstanceTypes, err := GetUserSupportedInstanceTypes(ctx, spec, conn) - if err != nil { - return nil, err - } - - amsType := PickInstanceType(userInstanceTypes) +type MarketplaceInfo struct { + BillingModel string + Provider string + CloudAccountID string +} - return amsType, nil +type OrgQuotas struct { + StandardQuotas []QuotaSpec + MarketplaceQuotas []QuotaSpec + TrialQuotas []QuotaSpec } func fetchOrgQuotaCost(ctx context.Context, conn connection.Connection) (*amsclient.QuotaCostList, error) { @@ -68,134 +70,212 @@ func fetchOrgQuotaCost(ctx context.Context, conn connection.Connection) (*amscli } -func GetUserSupportedInstanceTypes(ctx context.Context, spec *remote.AmsConfig, conn connection.Connection) (quota []QuotaSpec, err error) { +func GetOrgQuotas(f *factory.Factory, spec *remote.AmsConfig) (*OrgQuotas, error) { - quotaCostGet, err := fetchOrgQuotaCost(ctx, conn) + conn, err := f.Connection(connection.DefaultConfigSkipMasAuth) if err != nil { return nil, err } - var quotas []QuotaSpec + quotaCostGet, err := fetchOrgQuotaCost(f.Context, conn) + if err != nil { + return nil, err + } + + var standardQuotas, marketplaceQuotas, trialQuotas []QuotaSpec for _, quota := range quotaCostGet.GetItems() { quotaResources := quota.GetRelatedResources() for i := range quotaResources { quotaResource := quotaResources[i] if quotaResource.GetResourceName() == spec.ResourceName { if quotaResource.GetProduct() == spec.TrialProductQuotaID { - quotas = append(quotas, QuotaSpec{QuotaTrialType, 0, quotaResource.BillingModel}) + trialQuotas = append(trialQuotas, QuotaSpec{QuotaTrialType, 0, quotaResource.BillingModel, nil}) } else if quotaResource.GetProduct() == spec.InstanceQuotaID { remainingQuota := int(quota.GetAllowed() - quota.GetConsumed()) - quotas = append(quotas, QuotaSpec{QuotaStandardType, remainingQuota, quotaResource.BillingModel}) + if quotaResource.BillingModel == QuotaStandardType { + standardQuotas = append(standardQuotas, QuotaSpec{QuotaStandardType, remainingQuota, quotaResource.BillingModel, nil}) + } else if quotaResource.BillingModel == QuotaMarketplaceType { + marketplaceQuotas = append(marketplaceQuotas, QuotaSpec{QuotaMarketplaceType, remainingQuota, quotaResource.BillingModel, quota.CloudAccounts}) + } } } } } - return BattleOfInstanceBillingModels(quotas), err + availableOrgQuotas := &OrgQuotas{standardQuotas, marketplaceQuotas, trialQuotas} + + return availableOrgQuotas, nil } -// This function selects the billing model that should be used -// It represents some requirement to always use the same standard billing models -// This function should not exist but it does represents some requirement that we cannot do on backend -func BattleOfInstanceBillingModels(quotas []QuotaSpec) []QuotaSpec { - var betterQuotasMap map[string]*QuotaSpec = make(map[string]*QuotaSpec) - alwaysWinsBillingModel := "standard" - for i := 0; i < len(quotas); i++ { - if quotas[i].BillingModel == alwaysWinsBillingModel { - betterQuotasMap[quotas[i].Name] = "as[i] - } else if betterQuotasMap[quotas[i].Name] == nil { - betterQuotasMap[quotas[i].Name] = "as[i] +func SelectQuotaForUser(f *factory.Factory, orgQuota *OrgQuotas, marketplaceInfo MarketplaceInfo) (*QuotaSpec, error) { + if len(orgQuota.StandardQuotas) == 0 && len(orgQuota.MarketplaceQuotas) == 0 { + if marketplaceInfo.BillingModel != "" || marketplaceInfo.Provider != "" { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.onlyTrialAvailable") } + // select a trial quota as all other types are missing + return &orgQuota.TrialQuotas[0], nil } - var betterQuotas []QuotaSpec - for _, v := range betterQuotasMap { - betterQuotas = append(betterQuotas, *v) + + if len(orgQuota.MarketplaceQuotas) == 0 && len(orgQuota.StandardQuotas) > 0 { + if marketplaceInfo.BillingModel == QuotaMarketplaceType || marketplaceInfo.Provider != "" || marketplaceInfo.CloudAccountID != "" { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.noMarketplace") + } + // select a standard quota + return &orgQuota.StandardQuotas[0], nil } - return betterQuotas -} + if len(orgQuota.StandardQuotas) == 0 && len(orgQuota.MarketplaceQuotas) > 0 { + + if marketplaceInfo.BillingModel == QuotaStandardType { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.noStandard") + } -// PickInstanceType - Standard instance always wins! -// This function should not exist but it does represents some requirement -// from business to only pick one instance type when two are presented. -// When standard instance type is present in user instances it should always take precedence -func PickInstanceType(amsTypes []QuotaSpec) *QuotaSpec { - if amsTypes == nil || len(amsTypes) == 0 { - return nil + marketplaceQuota, err := getMarketplaceQuota(f, orgQuota.MarketplaceQuotas, marketplaceInfo) + if err != nil { + return nil, err + } + + marketplaceQuota.CloudAccounts, err = pickCloudAccount(f, marketplaceQuota.CloudAccounts, marketplaceInfo) + if err != nil { + return nil, err + } + + return marketplaceQuota, nil } - for _, amsType := range amsTypes { - if amsType.Name == QuotaStandardType { - return &amsType + if len(orgQuota.StandardQuotas) > 0 && len(orgQuota.MarketplaceQuotas) > 0 { + + if marketplaceInfo.BillingModel == QuotaStandardType { + return &orgQuota.StandardQuotas[0], nil + } else if marketplaceInfo.BillingModel == QuotaMarketplaceType || marketplaceInfo.Provider != "" || marketplaceInfo.CloudAccountID != "" { + marketplaceQuota, err := getMarketplaceQuota(f, orgQuota.MarketplaceQuotas, marketplaceInfo) + if err != nil { + return nil, err + } + + marketplaceQuota.CloudAccounts, err = pickCloudAccount(f, marketplaceQuota.CloudAccounts, marketplaceInfo) + if err != nil { + return nil, err + } + + return marketplaceQuota, nil } + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.noBillingModel") } - // There is chance of having multiple instances in the future - // We will pick the first one as we do not know which one to pick - return &amsTypes[0] + return &orgQuota.TrialQuotas[0], nil } -func GetOrganizationID(ctx context.Context, conn connection.Connection) (accountID string, err error) { - account, _, err := conn.API().AccountMgmt().ApiAccountsMgmtV1CurrentAccountGet(ctx). - Execute() +func getMarketplaceQuota(f *factory.Factory, marketplaceQuotas []QuotaSpec, marketplace MarketplaceInfo) (*QuotaSpec, error) { + if len(marketplaceQuotas) == 1 { + if marketplace.Provider != "" && marketplace.CloudAccountID != "" { + marketplaceQuota, err := pickMarketplaceQuota(f, marketplaceQuotas, marketplace) + if err != nil { + return nil, err + } + return marketplaceQuota, nil + } + return &marketplaceQuotas[0], nil + } else if len(marketplaceQuotas) > 1 && marketplace.Provider == "" && marketplace.CloudAccountID == "" { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.multipleMarketplaceQuotas") + } + + marketplaceQuota, err := pickMarketplaceQuota(f, marketplaceQuotas, marketplace) if err != nil { - return "", err + return nil, err } + return marketplaceQuota, nil - return account.Organization.GetId(), nil } -func GetValidMarketplaceAcctIDs(ctx context.Context, connectionFunc factory.ConnectionFunc, marketplace string) (marketplaceAcctIDs []string, err error) { +func pickMarketplaceQuota(f *factory.Factory, marketplaceQuotas []QuotaSpec, marketplace MarketplaceInfo) (*QuotaSpec, error) { - conn, err := connectionFunc(connection.DefaultConfigSkipMasAuth) - if err != nil { - return nil, err + matchedQuotas := []QuotaSpec{} + + for _, quota := range marketplaceQuotas { + cloudAccounts := *quota.CloudAccounts + for _, cloudAccount := range cloudAccounts { + if *cloudAccount.CloudProviderId == marketplace.Provider && *cloudAccount.CloudAccountId == marketplace.CloudAccountID { + matchedQuotas = append(matchedQuotas, quota) + } + } } - quotaCostGet, err := fetchOrgQuotaCost(ctx, conn) - if err != nil { - return nil, err + if len(matchedQuotas) == 0 { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.cloudAccountNotFound") } - for _, quota := range quotaCostGet.GetItems() { - if len(quota.GetCloudAccounts()) > 0 { - for _, cloudAccount := range quota.GetCloudAccounts() { + return &matchedQuotas[0], nil +} + +func pickCloudAccount(f *factory.Factory, cloudAccounts *[]amsclient.CloudAccount, market MarketplaceInfo) (*[]amsclient.CloudAccount, error) { + + if len(*cloudAccounts) == 1 { + return cloudAccounts, nil + } + + if len(*cloudAccounts) > 2 && market.Provider == "" && market.CloudAccountID == "" { + return nil, f.Localizer.MustLocalizeError("kafka.create.quota.error.multipleCloudAccounts") + } + + var matchedAccounts []amsclient.CloudAccount + + for _, cloudAccount := range *cloudAccounts { + if *cloudAccount.CloudProviderId == market.Provider || *cloudAccount.CloudAccountId == market.CloudAccountID { + matchedAccounts = append(matchedAccounts, cloudAccount) + } + } + + return &matchedAccounts, nil +} + +// FetchValidMarketplaces returns the marketplaces available to the user to create Kafka Instance +func FetchValidMarketplaces(amsTypes []QuotaSpec) []string { + + validMarketplaces := []string{} + + for _, quota := range amsTypes { + if quota.CloudAccounts != nil { + for _, cloudAccount := range *quota.CloudAccounts { + validMarketplaces = append(validMarketplaces, *cloudAccount.CloudProviderId) + } + } + } + + return unique(validMarketplaces) +} + +// FetchValidMarketplaceAccounts returns the cloud accounts available for the specified marketplace +func FetchValidMarketplaceAccounts(amsTypes []QuotaSpec, marketplace string) []string { + + validAccounts := []string{} + + for _, quota := range amsTypes { + if quota.CloudAccounts != nil { + for _, cloudAccount := range *quota.CloudAccounts { if marketplace != "" { if cloudAccount.GetCloudProviderId() == marketplace { - marketplaceAcctIDs = append(marketplaceAcctIDs, cloudAccount.GetCloudAccountId()) + validAccounts = append(validAccounts, cloudAccount.GetCloudAccountId()) } } else { - marketplaceAcctIDs = append(marketplaceAcctIDs, cloudAccount.GetCloudAccountId()) + validAccounts = append(validAccounts, cloudAccount.GetCloudAccountId()) } } } } - return unique(marketplaceAcctIDs), err + return unique(validAccounts) } -func GetValidMarketplaces(ctx context.Context, connectionFunc factory.ConnectionFunc) (marketplaces []string, err error) { - - conn, err := connectionFunc(connection.DefaultConfigSkipMasAuth) - if err != nil { - return nil, err - } - - quotaCostGet, err := fetchOrgQuotaCost(ctx, conn) +func GetOrganizationID(ctx context.Context, conn connection.Connection) (accountID string, err error) { + account, _, err := conn.API().AccountMgmt().ApiAccountsMgmtV1CurrentAccountGet(ctx). + Execute() if err != nil { - return nil, err - } - - for _, quota := range quotaCostGet.GetItems() { - if len(quota.GetCloudAccounts()) > 0 { - for _, cloudAccount := range quota.GetCloudAccounts() { - marketplaces = append(marketplaces, cloudAccount.GetCloudProviderId()) - } - } + return "", err } - return unique(marketplaces), err + return account.Organization.GetId(), nil } func unique(s []string) []string { diff --git a/pkg/shared/accountmgmtutil/api.go b/pkg/shared/accountmgmtutil/api.go index f67b971e4..2ca6ec5d7 100644 --- a/pkg/shared/accountmgmtutil/api.go +++ b/pkg/shared/accountmgmtutil/api.go @@ -4,6 +4,7 @@ package accountmgmtutil type QuotaType = string const ( - QuotaTrialType QuotaType = "trial" - QuotaStandardType QuotaType = "standard" + QuotaTrialType QuotaType = "trial" + QuotaStandardType QuotaType = "standard" + QuotaMarketplaceType QuotaType = "marketplace" )