diff --git a/cmd/src/batch_common.go b/cmd/src/batch_common.go index 27b4ada60f..e16aa38fcb 100644 --- a/cmd/src/batch_common.go +++ b/cmd/src/batch_common.go @@ -290,7 +290,7 @@ func executeBatchSpec(ctx context.Context, opts executeBatchSpecOpts) (err error Client: opts.client, }) - ffs, err := svc.DetermineFeatureFlags(ctx) + lr, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx) if err != nil { return err } @@ -521,7 +521,7 @@ func executeBatchSpec(ctx context.Context, opts executeBatchSpecOpts) (err error execUI.CreatingBatchSpec() id, url, err := svc.CreateBatchSpec(ctx, namespace.ID, rawSpec, ids) if err != nil { - return execUI.CreatingBatchSpecError(err) + return execUI.CreatingBatchSpecError(lr.MaxUnlicensedChangesets, err) } previewURL := cfg.Endpoint + url execUI.CreatingBatchSpecSuccess(previewURL) diff --git a/cmd/src/batch_new.go b/cmd/src/batch_new.go index 75fe9a8891..349b590eff 100644 --- a/cmd/src/batch_new.go +++ b/cmd/src/batch_new.go @@ -48,7 +48,7 @@ Examples: Client: cfg.apiClient(apiFlags, flagSet.Output()), }) - ffs, err := svc.DetermineFeatureFlags(ctx) + _, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx) if err != nil { return err } diff --git a/cmd/src/batch_remote.go b/cmd/src/batch_remote.go index dbdcf1c264..7817421ab3 100644 --- a/cmd/src/batch_remote.go +++ b/cmd/src/batch_remote.go @@ -52,7 +52,7 @@ Examples: Client: cfg.apiClient(flags.api, flagSet.Output()), }) - ffs, err := svc.DetermineFeatureFlags(ctx) + _, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx) if err != nil { return err } diff --git a/cmd/src/batch_repositories.go b/cmd/src/batch_repositories.go index f4b3280823..092ae637d2 100644 --- a/cmd/src/batch_repositories.go +++ b/cmd/src/batch_repositories.go @@ -69,7 +69,7 @@ Examples: Client: client, }) - ffs, err := svc.DetermineFeatureFlags(ctx) + _, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx) if err != nil { return err } diff --git a/cmd/src/batch_validate.go b/cmd/src/batch_validate.go index b3268ea758..e2c77c29bc 100644 --- a/cmd/src/batch_validate.go +++ b/cmd/src/batch_validate.go @@ -63,7 +63,7 @@ Examples: Client: cfg.apiClient(apiFlags, flagSet.Output()), }) - ffs, err := svc.DetermineFeatureFlags(ctx) + _, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx) if err != nil { return err } diff --git a/internal/batches/license.go b/internal/batches/license.go new file mode 100644 index 0000000000..2a614ae719 --- /dev/null +++ b/internal/batches/license.go @@ -0,0 +1,5 @@ +package batches + +type LicenseRestrictions struct { + MaxUnlicensedChangesets int +} diff --git a/internal/batches/service/service.go b/internal/batches/service/service.go index 4231b945dd..99d49a9c21 100644 --- a/internal/batches/service/service.go +++ b/internal/batches/service/service.go @@ -43,10 +43,11 @@ func New(opts *Opts) *Service { // The reason we ask for batchChanges here is to surface errors about trying to use batch // changes in an unsupported environment sooner, since the version check is typically the // first thing we do. -const sourcegraphVersionQuery = `query SourcegraphVersion { +const getInstanceInfo = `query InstanceInfo { site { productVersion } + maxUnlicensedChangesets batchChanges(first: 1) { nodes { id @@ -55,32 +56,39 @@ const sourcegraphVersionQuery = `query SourcegraphVersion { } ` -// getSourcegraphVersion queries the Sourcegraph GraphQL API to get the -// current version of the Sourcegraph instance. -func (svc *Service) getSourcegraphVersion(ctx context.Context) (string, error) { +// getSourcegraphVersionAndMaxChangesetsCount queries the Sourcegraph GraphQL API to get the +// current version and max unlicensed changesets count for the Sourcegraph instance. +func (svc *Service) getSourcegraphVersionAndMaxChangesetsCount(ctx context.Context) (string, int, error) { var result struct { - Site struct { + MaxUnlicensedChangesets int + Site struct { ProductVersion string } } - ok, err := svc.client.NewQuery(sourcegraphVersionQuery).Do(ctx, &result) + ok, err := svc.client.NewQuery(getInstanceInfo).Do(ctx, &result) if err != nil || !ok { - return "", err + return "", 0, err } - return result.Site.ProductVersion, err + return result.Site.ProductVersion, result.MaxUnlicensedChangesets, err } -// DetermineFeatureFlags fetches the version of the configured Sourcegraph and -// returns the enabled features. -func (svc *Service) DetermineFeatureFlags(ctx context.Context) (*batches.FeatureFlags, error) { - version, err := svc.getSourcegraphVersion(ctx) +// DetermineLicenseAndFeatureFlags returns the enabled features and license restrictions +// configured for the Sourcegraph instance. +func (svc *Service) DetermineLicenseAndFeatureFlags(ctx context.Context) (*batches.LicenseRestrictions, *batches.FeatureFlags, error) { + version, mc, err := svc.getSourcegraphVersionAndMaxChangesetsCount(ctx) if err != nil { - return nil, errors.Wrap(err, "failed to query Sourcegraph version to check for available features") + return nil, nil, errors.Wrap(err, "failed to query Sourcegraph version and license info for instance") + } + + lr := &batches.LicenseRestrictions{ + MaxUnlicensedChangesets: mc, } + ffs := &batches.FeatureFlags{} - return ffs, ffs.SetFromVersion(version) + return lr, ffs, ffs.SetFromVersion(version) + } const applyBatchChangeMutation = ` diff --git a/internal/batches/ui/exec_ui.go b/internal/batches/ui/exec_ui.go index e9944c17bc..fd12b84173 100644 --- a/internal/batches/ui/exec_ui.go +++ b/internal/batches/ui/exec_ui.go @@ -40,7 +40,7 @@ type ExecUI interface { CreatingBatchSpec() CreatingBatchSpecSuccess(previewURL string) - CreatingBatchSpecError(err error) error + CreatingBatchSpecError(maxUnlicensedCS int, err error) error PreviewBatchSpec(previewURL string) diff --git a/internal/batches/ui/json_lines.go b/internal/batches/ui/json_lines.go index 45475b137e..36ae989570 100644 --- a/internal/batches/ui/json_lines.go +++ b/internal/batches/ui/json_lines.go @@ -149,7 +149,7 @@ func (ui *JSONLines) CreatingBatchSpecSuccess(batchSpecURL string) { }) } -func (ui *JSONLines) CreatingBatchSpecError(err error) error { +func (ui *JSONLines) CreatingBatchSpecError(_ int, err error) error { logOperationFailure(batcheslib.LogEventOperationCreatingBatchSpec, &batcheslib.CreatingBatchSpecMetadata{}) return err } diff --git a/internal/batches/ui/tui.go b/internal/batches/ui/tui.go index af49e98bde..94724909b1 100644 --- a/internal/batches/ui/tui.go +++ b/internal/batches/ui/tui.go @@ -217,8 +217,8 @@ func (ui *TUI) CreatingBatchSpecSuccess(previewURL string) { batchCompletePending(ui.pending, "Creating batch spec on Sourcegraph") } -func (ui *TUI) CreatingBatchSpecError(err error) error { - return prettyPrintBatchUnlicensedError(ui.Out, err) +func (ui *TUI) CreatingBatchSpecError(maxUnlicensedCS int, err error) error { + return prettyPrintBatchUnlicensedError(ui.Out, maxUnlicensedCS, err) } func (ui *TUI) DockerWatchDogWarning(err error) { @@ -307,7 +307,7 @@ func (ui *TUI) RemoteSuccess(url string) { // is, then a better message is output. Regardless, the return value of this // function should be used to replace the original error passed in to ensure // that the displayed output is sensible. -func prettyPrintBatchUnlicensedError(out *output.Output, err error) error { +func prettyPrintBatchUnlicensedError(out *output.Output, maxUnlicensedCS int, err error) error { // Pull apart the error to see if it's a licensing error: if so, we should // display a friendlier and more actionable message than the usual GraphQL // error output. @@ -321,19 +321,19 @@ func prettyPrintBatchUnlicensedError(out *output.Output, err error) error { // verbose mode, but let the original error bubble up rather // than this one. out.Verbosef("Unexpected error parsing the GraphQL error: %v", cerr) - } else if code == "ErrBatchChangesUnlicensed" { + } else if code == "ErrBatchChangesUnlicensed" || code == "ErrBatchChangesOverLimit" { // OK, let's print a better message, then return an // exitCodeError to suppress the normal automatic error block. // Note that we have hand wrapped the output at 80 (printable) // characters: having automatic wrapping some day would be nice, // but this should be sufficient for now. block := out.Block(output.Line("🪙", output.StyleWarning, "Batch Changes is a paid feature of Sourcegraph. All users can create sample")) - block.WriteLine(output.Linef("", output.StyleWarning, "batch changes with up to 10 changesets without a license. Contact Sourcegraph")) + block.WriteLine(output.Linef("", output.StyleWarning, "batch changes with up to %v changesets without a license. Contact Sourcegraph", maxUnlicensedCS)) block.WriteLine(output.Linef("", output.StyleWarning, "sales at %shttps://about.sourcegraph.com/contact/sales/%s to obtain a trial", output.StyleSearchLink, output.StyleWarning)) block.WriteLine(output.Linef("", output.StyleWarning, "license.")) block.Write("") - block.WriteLine(output.Linef("", output.StyleWarning, "To proceed with this batch change, you will need to create 5 or fewer")) - block.WriteLine(output.Linef("", output.StyleWarning, "changesets. To do so, you could try adding %scount:5%s to your", output.StyleSearchAlertProposedQuery, output.StyleWarning)) + block.WriteLine(output.Linef("", output.StyleWarning, "To proceed with this batch change, you will need to create %v or fewer", maxUnlicensedCS)) + block.WriteLine(output.Linef("", output.StyleWarning, "changesets. To do so, you could try adding %scount:%v%s to your", output.StyleSearchAlertProposedQuery, maxUnlicensedCS, output.StyleWarning)) block.WriteLine(output.Linef("", output.StyleWarning, "%srepositoriesMatchingQuery%s search, or reduce the number of changesets in", output.StyleReset, output.StyleWarning)) block.WriteLine(output.Linef("", output.StyleWarning, "%simportChangesets%s.", output.StyleReset, output.StyleWarning)) block.Close()