github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/demo.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package cli
    12  
    13  import (
    14  	"context"
    15  	gosql "database/sql"
    16  	"fmt"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/cockroachdb/cockroach/pkg/cli/cliflags"
    21  	"github.com/cockroachdb/cockroach/pkg/geo/geos"
    22  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    23  	"github.com/cockroachdb/cockroach/pkg/security"
    24  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    25  	"github.com/cockroachdb/cockroach/pkg/util"
    26  	"github.com/cockroachdb/cockroach/pkg/util/log"
    27  	"github.com/cockroachdb/cockroach/pkg/util/uuid"
    28  	"github.com/cockroachdb/cockroach/pkg/workload"
    29  	"github.com/cockroachdb/errors"
    30  	"github.com/spf13/cobra"
    31  	"github.com/spf13/pflag"
    32  )
    33  
    34  var demoCmd = &cobra.Command{
    35  	Use:   "demo",
    36  	Short: "open a demo sql shell",
    37  	Long: `
    38  Start an in-memory, standalone, single-node CockroachDB instance, and open an
    39  interactive SQL prompt to it. Various datasets are available to be preloaded as
    40  subcommands: e.g. "cockroach demo startrek". See --help for a full list.
    41  
    42  By default, the 'movr' dataset is pre-loaded. You can also use --empty
    43  to avoid pre-loading a dataset.
    44  
    45  cockroach demo attempts to connect to a Cockroach Labs server to obtain a
    46  temporary enterprise license for demoing enterprise features and enable
    47  telemetry back to Cockroach Labs. In order to disable this behavior, set the
    48  environment variable "COCKROACH_SKIP_ENABLING_DIAGNOSTIC_REPORTING" to true.
    49  `,
    50  	Example: `  cockroach demo`,
    51  	Args:    cobra.NoArgs,
    52  	RunE: MaybeDecorateGRPCError(func(cmd *cobra.Command, _ []string) error {
    53  		return runDemo(cmd, nil /* gen */)
    54  	}),
    55  }
    56  
    57  const demoOrg = "Cockroach Demo"
    58  
    59  const defaultGeneratorName = "movr"
    60  
    61  const defaultRootPassword = "admin"
    62  
    63  var defaultGenerator workload.Generator
    64  
    65  // maxNodeInitTime is the maximum amount of time to wait for nodes to be connected.
    66  const maxNodeInitTime = 30 * time.Second
    67  
    68  var defaultLocalities = demoLocalityList{
    69  	// Default localities for a 3 node cluster
    70  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "b"}}},
    71  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "c"}}},
    72  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-east1"}, {Key: "az", Value: "d"}}},
    73  	// Default localities for a 6 node cluster
    74  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "a"}}},
    75  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "b"}}},
    76  	{Tiers: []roachpb.Tier{{Key: "region", Value: "us-west1"}, {Key: "az", Value: "c"}}},
    77  	// Default localities for a 9 node cluster
    78  	{Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "b"}}},
    79  	{Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "c"}}},
    80  	{Tiers: []roachpb.Tier{{Key: "region", Value: "europe-west1"}, {Key: "az", Value: "d"}}},
    81  }
    82  
    83  var demoNodeCacheSizeValue = newBytesOrPercentageValue(
    84  	&demoCtx.cacheSize,
    85  	memoryPercentResolver,
    86  )
    87  var demoNodeSQLMemSizeValue = newBytesOrPercentageValue(
    88  	&demoCtx.sqlPoolMemorySize,
    89  	memoryPercentResolver,
    90  )
    91  
    92  type regionPair struct {
    93  	regionA string
    94  	regionB string
    95  }
    96  
    97  var regionToRegionToLatency map[string]map[string]int
    98  
    99  func insertPair(pair regionPair, latency int) {
   100  	regionToLatency, ok := regionToRegionToLatency[pair.regionA]
   101  	if !ok {
   102  		regionToLatency = make(map[string]int)
   103  		regionToRegionToLatency[pair.regionA] = regionToLatency
   104  	}
   105  	regionToLatency[pair.regionB] = latency
   106  }
   107  
   108  func init() {
   109  	regionToRegionToLatency = make(map[string]map[string]int)
   110  	// Latencies collected from http://cloudping.co on 2019-09-11.
   111  	for pair, latency := range map[regionPair]int{
   112  		{regionA: "us-east1", regionB: "us-west1"}:     66,
   113  		{regionA: "us-east1", regionB: "europe-west1"}: 64,
   114  		{regionA: "us-west1", regionB: "europe-west1"}: 146,
   115  	} {
   116  		insertPair(pair, latency)
   117  		insertPair(regionPair{
   118  			regionA: pair.regionB,
   119  			regionB: pair.regionA,
   120  		}, latency)
   121  	}
   122  }
   123  
   124  func init() {
   125  	for _, meta := range workload.Registered() {
   126  		gen := meta.New()
   127  
   128  		if meta.Name == defaultGeneratorName {
   129  			// Save the default for use in the top-level 'demo' command
   130  			// without argument.
   131  			defaultGenerator = gen
   132  		}
   133  
   134  		var genFlags *pflag.FlagSet
   135  		if f, ok := gen.(workload.Flagser); ok {
   136  			genFlags = f.Flags().FlagSet
   137  		}
   138  
   139  		genDemoCmd := &cobra.Command{
   140  			Use:   meta.Name,
   141  			Short: meta.Description,
   142  			Args:  cobra.ArbitraryArgs,
   143  			RunE: MaybeDecorateGRPCError(func(cmd *cobra.Command, _ []string) error {
   144  				return runDemo(cmd, gen)
   145  			}),
   146  		}
   147  		if !meta.PublicFacing {
   148  			genDemoCmd.Hidden = true
   149  		}
   150  		demoCmd.AddCommand(genDemoCmd)
   151  		genDemoCmd.Flags().AddFlagSet(genFlags)
   152  	}
   153  }
   154  
   155  // GetAndApplyLicense is not implemented in order to keep OSS/BSL builds successful.
   156  // The cliccl package sets this function if enterprise features are available to demo.
   157  var GetAndApplyLicense func(dbConn *gosql.DB, clusterID uuid.UUID, org string) (bool, error)
   158  
   159  func incrementTelemetryCounters(cmd *cobra.Command) {
   160  	incrementDemoCounter(demo)
   161  	if flagSetForCmd(cmd).Lookup(cliflags.DemoNodes.Name).Changed {
   162  		incrementDemoCounter(nodes)
   163  	}
   164  	if demoCtx.localities != nil {
   165  		incrementDemoCounter(demoLocality)
   166  	}
   167  	if demoCtx.runWorkload {
   168  		incrementDemoCounter(withLoad)
   169  	}
   170  	if demoCtx.geoPartitionedReplicas {
   171  		incrementDemoCounter(geoPartitionedReplicas)
   172  	}
   173  }
   174  
   175  func checkDemoConfiguration(
   176  	cmd *cobra.Command, gen workload.Generator,
   177  ) (workload.Generator, error) {
   178  	if gen == nil && !demoCtx.useEmptyDatabase {
   179  		// Use a default dataset unless prevented by --empty.
   180  		gen = defaultGenerator
   181  	}
   182  
   183  	// Make sure that the user didn't request a workload and an empty database.
   184  	if demoCtx.runWorkload && demoCtx.useEmptyDatabase {
   185  		return nil, errors.New("cannot run a workload against an empty database")
   186  	}
   187  
   188  	// Make sure the number of nodes is valid.
   189  	if demoCtx.nodes <= 0 {
   190  		return nil, errors.Newf("--nodes has invalid value (expected positive, got %d)", demoCtx.nodes)
   191  	}
   192  
   193  	// If artificial latencies were requested, then the user cannot supply their own localities.
   194  	if demoCtx.simulateLatency && demoCtx.localities != nil {
   195  		return nil, errors.New("--global cannot be used with --demo-locality")
   196  	}
   197  
   198  	demoCtx.disableTelemetry = cluster.TelemetryOptOut()
   199  	// disableLicenseAcquisition can also be set by the the user as an
   200  	// input flag, so make sure it include it when considering the final
   201  	// value of disableLicenseAcquisition.
   202  	demoCtx.disableLicenseAcquisition =
   203  		demoCtx.disableTelemetry || (GetAndApplyLicense == nil) || demoCtx.disableLicenseAcquisition
   204  
   205  	if demoCtx.geoPartitionedReplicas {
   206  		geoFlag := "--" + cliflags.DemoGeoPartitionedReplicas.Name
   207  		if demoCtx.disableLicenseAcquisition {
   208  			return nil, errors.Newf("enterprise features are needed for this demo (%s)", geoFlag)
   209  		}
   210  
   211  		// Make sure that the user didn't request to have a topology and an empty database.
   212  		if demoCtx.useEmptyDatabase {
   213  			return nil, errors.New("cannot setup geo-partitioned replicas topology on an empty database")
   214  		}
   215  
   216  		// Make sure that the Movr database is selected when automatically partitioning.
   217  		if gen == nil || gen.Meta().Name != "movr" {
   218  			return nil, errors.Newf("%s must be used with the Movr dataset", geoFlag)
   219  		}
   220  
   221  		// If the geo-partitioned replicas flag was given and the demo localities have changed, throw an error.
   222  		if demoCtx.localities != nil {
   223  			return nil, errors.Newf("--demo-locality cannot be used with %s", geoFlag)
   224  		}
   225  
   226  		// If the geo-partitioned replicas flag was given and the nodes have changed, throw an error.
   227  		if flagSetForCmd(cmd).Lookup(cliflags.DemoNodes.Name).Changed {
   228  			if demoCtx.nodes != 9 {
   229  				return nil, errors.Newf("--nodes with a value different from 9 cannot be used with %s", geoFlag)
   230  			}
   231  		} else {
   232  			const msg = `#
   233  # --geo-partitioned replicas operates on a 9 node cluster.
   234  # The cluster size has been changed from the default to 9 nodes.`
   235  			fmt.Println(msg)
   236  			demoCtx.nodes = 9
   237  		}
   238  
   239  		// If geo-partition-replicas is requested, make sure the workload has a Partitioning step.
   240  		configErr := errors.Newf(
   241  			"workload %s is not configured to have a partitioning step", gen.Meta().Name)
   242  		hookser, ok := gen.(workload.Hookser)
   243  		if !ok {
   244  			return nil, configErr
   245  		}
   246  		if hookser.Hooks().Partition == nil {
   247  			return nil, configErr
   248  		}
   249  	}
   250  
   251  	return gen, nil
   252  }
   253  
   254  func runDemo(cmd *cobra.Command, gen workload.Generator) (err error) {
   255  	if gen, err = checkDemoConfiguration(cmd, gen); err != nil {
   256  		return err
   257  	}
   258  	// Record some telemetry about what flags are being used.
   259  	incrementTelemetryCounters(cmd)
   260  
   261  	ctx := context.Background()
   262  
   263  	if err := checkTzDatabaseAvailability(ctx); err != nil {
   264  		return err
   265  	}
   266  
   267  	loc, err := geos.EnsureInit(geos.EnsureInitErrorDisplayPrivate, demoCtx.geoLibsDir)
   268  	if err != nil {
   269  		log.Infof(ctx, "could not initialize GEOS - geospatial functions may not be available: %v", err)
   270  	} else {
   271  		log.Infof(ctx, "GEOS initialized at %s", loc)
   272  	}
   273  
   274  	c, err := setupTransientCluster(ctx, cmd, gen)
   275  	defer c.cleanup(ctx)
   276  	if err != nil {
   277  		return checkAndMaybeShout(err)
   278  	}
   279  	demoCtx.transientCluster = &c
   280  
   281  	checkInteractive()
   282  
   283  	if cliCtx.isInteractive {
   284  		fmt.Printf(`#
   285  # Welcome to the CockroachDB demo database!
   286  #
   287  # You are connected to a temporary, in-memory CockroachDB cluster of %d node%s.
   288  `, demoCtx.nodes, util.Pluralize(int64(demoCtx.nodes)))
   289  
   290  		if demoCtx.disableTelemetry {
   291  			fmt.Println("#\n# Telemetry and automatic license acquisition disabled by configuration.")
   292  		} else if demoCtx.disableLicenseAcquisition {
   293  			fmt.Println("#\n# Enterprise features disabled by OSS-only build.")
   294  		} else {
   295  			fmt.Println("#\n# This demo session will attempt to enable enterprise features\n" +
   296  				"# by acquiring a temporary license from Cockroach Labs in the background.\n" +
   297  				"# To disable this behavior, set the environment variable\n" +
   298  				"# COCKROACH_SKIP_ENABLING_DIAGNOSTIC_REPORTING=true.")
   299  		}
   300  	}
   301  
   302  	// Start license acquisition in the background.
   303  	licenseDone, err := c.acquireDemoLicense(ctx)
   304  	if err != nil {
   305  		return checkAndMaybeShout(err)
   306  	}
   307  
   308  	// Initialize the workload, if requested.
   309  	if err := c.setupWorkload(ctx, gen, licenseDone); err != nil {
   310  		return checkAndMaybeShout(err)
   311  	}
   312  
   313  	if cliCtx.isInteractive {
   314  		if gen != nil {
   315  			fmt.Printf("#\n# The cluster has been preloaded with the %q dataset\n# (%s).\n",
   316  				gen.Meta().Name, gen.Meta().Description)
   317  		}
   318  
   319  		fmt.Println(`#
   320  # Reminder: your changes to data stored in the demo session will not be saved!
   321  #
   322  # Connection parameters:`)
   323  		var nodeList strings.Builder
   324  		c.listDemoNodes(&nodeList, true /* justOne */)
   325  		fmt.Println("#", strings.ReplaceAll(nodeList.String(), "\n", "\n# "))
   326  
   327  		if !demoCtx.insecure {
   328  			fmt.Printf(
   329  				"# The user %q with password %q has been created. Use it to access the Web UI!\n#\n",
   330  				security.RootUser,
   331  				defaultRootPassword,
   332  			)
   333  		}
   334  		// If we didn't launch a workload, we still need to inform the
   335  		// user if the license check fails. Do this asynchronously and print
   336  		// the final error if any.
   337  
   338  		// It's ok to do this twice (if workload setup already waited) because
   339  		// then the error return is guaranteed to be nil.
   340  		go func() {
   341  			if err := waitForLicense(licenseDone); err != nil {
   342  				_ = checkAndMaybeShout(err)
   343  			}
   344  		}()
   345  	} else {
   346  		// If we are not running an interactive shell, we need to wait to ensure
   347  		// that license acquisition is successful. If license acquisition is
   348  		// disabled, then a read on this channel will return immediately.
   349  		if err := waitForLicense(licenseDone); err != nil {
   350  			return checkAndMaybeShout(err)
   351  		}
   352  	}
   353  
   354  	conn := makeSQLConn(c.connURL)
   355  	defer conn.Close()
   356  
   357  	return runClient(cmd, conn)
   358  }
   359  
   360  func waitForLicense(licenseDone <-chan error) error {
   361  	err := <-licenseDone
   362  	return err
   363  }